feat: update all containers bulk action

This commit is contained in:
Pujit Mehrotra
2025-11-17 16:03:37 -05:00
parent cf743d573b
commit 12c8c12404
9 changed files with 181 additions and 24 deletions

View File

@@ -16,6 +16,7 @@ import { SET_DOCKER_FOLDER_CHILDREN } from '@/components/Docker/docker-set-folde
import { START_DOCKER_CONTAINER } from '@/components/Docker/docker-start-container.mutation';
import { STOP_DOCKER_CONTAINER } from '@/components/Docker/docker-stop-container.mutation';
import { UNPAUSE_DOCKER_CONTAINER } from '@/components/Docker/docker-unpause-container.mutation';
import { UPDATE_ALL_DOCKER_CONTAINERS } from '@/components/Docker/docker-update-all-containers.mutation';
import { UPDATE_DOCKER_CONTAINERS } from '@/components/Docker/docker-update-containers.mutation';
import { ContainerState } from '@/composables/gql/graphql';
import { useContainerActions } from '@/composables/useContainerActions';
@@ -311,12 +312,19 @@ const dockerSearchAccessor = (row: TreeRow<DockerContainer>): unknown[] => {
const dockerFilterHelpText = `Filter by ${searchableKeys.join(', ')}`;
const { treeData, entryParentById, folderChildrenIds, parentById, positionById, getRowById } =
useTreeData<DockerContainer>({
flatEntries: flatEntriesRef,
flatData: containersRef,
buildFlatRow: toContainerTreeRow,
});
const {
treeData,
entryParentById,
folderChildrenIds,
parentById,
positionById,
getRowById,
flattenRows,
} = useTreeData<DockerContainer>({
flatEntries: flatEntriesRef,
flatData: containersRef,
buildFlatRow: toContainerTreeRow,
});
const { visibleFolders, expandedFolders, toggleExpandFolder, setExpandedFolders } = useFolderTree({
flatEntries: flatEntriesRef,
@@ -718,6 +726,9 @@ const { mutate: stopContainerMutation } = useMutation(STOP_DOCKER_CONTAINER);
const { mutate: pauseContainerMutation } = useMutation(PAUSE_DOCKER_CONTAINER);
const { mutate: unpauseContainerMutation } = useMutation(UNPAUSE_DOCKER_CONTAINER);
const { mutate: updateContainersMutation } = useMutation(UPDATE_DOCKER_CONTAINERS);
const { mutate: updateAllContainersMutation, loading: updatingAllContainers } = useMutation(
UPDATE_ALL_DOCKER_CONTAINERS
);
const { mutate: refreshDockerDigestsMutation, loading: checkingForUpdates } =
useMutation(REFRESH_DOCKER_DIGESTS);
@@ -745,8 +756,17 @@ function getContainerRows(ids: string[]): TreeRow<DockerContainer>[] {
return rows;
}
const allContainerRows = computed<TreeRow<DockerContainer>[]>(() => {
return flattenRows(treeData.value, 'container') as TreeRow<DockerContainer>[];
});
const updateCandidateRows = computed<TreeRow<DockerContainer>[]>(() =>
allContainerRows.value.filter((row) => Boolean(row.meta?.isUpdateAvailable))
);
const selectedContainerRows = computed(() => getContainerRows(props.selectedIds));
const hasSelectedContainers = computed(() => selectedContainerRows.value.length > 0);
const hasSelectedEntries = computed(() => props.selectedIds.length > 0);
function setRowsUpdating(rows: TreeRow<DockerContainer>[], updating: boolean) {
if (!rows.length) return;
const next = new Set(updatingRowIds.value);
@@ -840,6 +860,40 @@ async function handleBulkUpdateContainers(rows: TreeRow<DockerContainer>[]) {
}
}
async function handleUpdateAllContainers() {
const rows = updateCandidateRows.value;
const entryIds = Array.from(new Set(rows.map((row) => row.id)));
if (rows.length) {
setRowsUpdating(rows, true);
setRowsBusy(entryIds, true);
}
try {
const response = await updateAllContainersMutation(
{},
{
refetchQueries: [{ query: GET_DOCKER_CONTAINERS, variables: { skipCache: true } }],
awaitRefetchQueries: true,
}
);
const count = response?.data?.docker?.updateAllContainers?.length ?? 0;
if (count > 0) {
showToast(`Successfully updated ${count} container${count === 1 ? '' : 's'}`);
} else {
showToast('No containers had updates available');
}
} catch (error) {
window.toast?.error?.('Failed to update containers', {
description: error instanceof Error ? error.message : 'Unknown error',
});
} finally {
if (rows.length) {
setRowsBusy(entryIds, false);
setRowsUpdating(rows, false);
}
}
}
const folderOps = useFolderOperations({
rootFolderId,
folderChildrenIds,
@@ -968,12 +1022,6 @@ function handleBulkAction(action: string) {
void handleBulkUpdateContainers(containerRows);
return;
}
if (action === 'Check for updates') {
const containerRows = getContainerRows(ids);
if (!containerRows.length) return;
void handleCheckForUpdates(containerRows);
return;
}
showToast(`${action} (${ids.length})`);
}
@@ -1029,11 +1077,28 @@ async function handleRowContextMenu(payload: {
}
const bulkItems = computed<DropdownMenuItems>(() => [
[
{
label: 'Check for updates',
icon: 'i-lucide-refresh-cw',
as: 'button',
disabled: checkingForUpdates.value,
onSelect: () => void handleCheckForUpdates(allContainerRows.value),
},
{
label: 'Update all',
icon: 'i-lucide-rotate-ccw',
as: 'button',
disabled: updatingAllContainers.value,
onSelect: () => void handleUpdateAllContainers(),
},
],
[
{
label: 'Move to folder',
icon: 'i-lucide-folder',
as: 'button',
disabled: !hasSelectedEntries.value,
onSelect: () => folderOps.openMoveModal(getSelectedEntryIds()),
},
{
@@ -1043,13 +1108,6 @@ const bulkItems = computed<DropdownMenuItems>(() => [
disabled: !hasSelectedContainers.value,
onSelect: () => handleBulkAction('Update containers'),
},
{
label: 'Check for updates',
icon: 'i-lucide-refresh-cw',
as: 'button',
disabled: checkingForUpdates.value || !hasSelectedContainers.value,
onSelect: () => handleBulkAction('Check for updates'),
},
{
label: 'Start / Stop',
icon: 'i-lucide-power',
@@ -1184,7 +1242,6 @@ function getRowActionItems(row: TreeRow<DockerContainer>): DropdownMenuItems {
variant="outline"
:size="compact ? 'sm' : 'md'"
trailing-icon="i-lucide-chevron-down"
:disabled="count === 0"
>
Actions ({{ count }})
</UButton>

View File

@@ -0,0 +1,15 @@
import { gql } from '@apollo/client';
export const UPDATE_ALL_DOCKER_CONTAINERS = gql`
mutation UpdateAllDockerContainers {
docker {
updateAllContainers {
id
names
state
isUpdateAvailable
isRebuildReady
}
}
}
`;

View File

@@ -44,6 +44,7 @@ type Documents = {
"\n mutation StartDockerContainer($id: PrefixedID!) {\n docker {\n start(id: $id) {\n id\n names\n state\n }\n }\n }\n": typeof types.StartDockerContainerDocument,
"\n mutation StopDockerContainer($id: PrefixedID!) {\n docker {\n stop(id: $id) {\n id\n names\n state\n }\n }\n }\n": typeof types.StopDockerContainerDocument,
"\n mutation UnpauseDockerContainer($id: PrefixedID!) {\n docker {\n unpause(id: $id) {\n id\n names\n state\n }\n }\n }\n": typeof types.UnpauseDockerContainerDocument,
"\n mutation UpdateAllDockerContainers {\n docker {\n updateAllContainers {\n id\n names\n state\n isUpdateAvailable\n isRebuildReady\n }\n }\n }\n": typeof types.UpdateAllDockerContainersDocument,
"\n mutation UpdateDockerAutostartConfiguration(\n $entries: [DockerAutostartEntryInput!]!\n $persistUserPreferences: Boolean\n ) {\n docker {\n updateAutostartConfiguration(entries: $entries, persistUserPreferences: $persistUserPreferences)\n }\n }\n": typeof types.UpdateDockerAutostartConfigurationDocument,
"\n mutation UpdateDockerContainer($id: PrefixedID!) {\n docker {\n updateContainer(id: $id) {\n id\n names\n state\n isUpdateAvailable\n isRebuildReady\n }\n }\n }\n": typeof types.UpdateDockerContainerDocument,
"\n mutation UpdateDockerContainers($ids: [PrefixedID!]!) {\n docker {\n updateContainers(ids: $ids) {\n id\n names\n state\n isUpdateAvailable\n isRebuildReady\n }\n }\n }\n": typeof types.UpdateDockerContainersDocument,
@@ -112,6 +113,7 @@ const documents: Documents = {
"\n mutation StartDockerContainer($id: PrefixedID!) {\n docker {\n start(id: $id) {\n id\n names\n state\n }\n }\n }\n": types.StartDockerContainerDocument,
"\n mutation StopDockerContainer($id: PrefixedID!) {\n docker {\n stop(id: $id) {\n id\n names\n state\n }\n }\n }\n": types.StopDockerContainerDocument,
"\n mutation UnpauseDockerContainer($id: PrefixedID!) {\n docker {\n unpause(id: $id) {\n id\n names\n state\n }\n }\n }\n": types.UnpauseDockerContainerDocument,
"\n mutation UpdateAllDockerContainers {\n docker {\n updateAllContainers {\n id\n names\n state\n isUpdateAvailable\n isRebuildReady\n }\n }\n }\n": types.UpdateAllDockerContainersDocument,
"\n mutation UpdateDockerAutostartConfiguration(\n $entries: [DockerAutostartEntryInput!]!\n $persistUserPreferences: Boolean\n ) {\n docker {\n updateAutostartConfiguration(entries: $entries, persistUserPreferences: $persistUserPreferences)\n }\n }\n": types.UpdateDockerAutostartConfigurationDocument,
"\n mutation UpdateDockerContainer($id: PrefixedID!) {\n docker {\n updateContainer(id: $id) {\n id\n names\n state\n isUpdateAvailable\n isRebuildReady\n }\n }\n }\n": types.UpdateDockerContainerDocument,
"\n mutation UpdateDockerContainers($ids: [PrefixedID!]!) {\n docker {\n updateContainers(ids: $ids) {\n id\n names\n state\n isUpdateAvailable\n isRebuildReady\n }\n }\n }\n": types.UpdateDockerContainersDocument,
@@ -284,6 +286,10 @@ export function graphql(source: "\n mutation StopDockerContainer($id: PrefixedI
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation UnpauseDockerContainer($id: PrefixedID!) {\n docker {\n unpause(id: $id) {\n id\n names\n state\n }\n }\n }\n"): (typeof documents)["\n mutation UnpauseDockerContainer($id: PrefixedID!) {\n docker {\n unpause(id: $id) {\n id\n names\n state\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation UpdateAllDockerContainers {\n docker {\n updateAllContainers {\n id\n names\n state\n isUpdateAvailable\n isRebuildReady\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateAllDockerContainers {\n docker {\n updateAllContainers {\n id\n names\n state\n isUpdateAvailable\n isRebuildReady\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

View File

@@ -560,6 +560,17 @@ export type CpuLoad = {
percentUser: Scalars['Float']['output'];
};
export type CpuPackages = Node & {
__typename?: 'CpuPackages';
id: Scalars['PrefixedID']['output'];
/** Power draw per package (W) */
power: Array<Scalars['Float']['output']>;
/** Temperature per package (°C) */
temp: Array<Scalars['Float']['output']>;
/** Total CPU package power draw (W) */
totalPower: Scalars['Float']['output'];
};
export type CpuUtilization = Node & {
__typename?: 'CpuUtilization';
/** CPU load for each core */
@@ -782,6 +793,8 @@ export type DockerMutations = {
stop: DockerContainer;
/** Unpause (Resume) a container */
unpause: DockerContainer;
/** Update all containers that have available updates */
updateAllContainers: Array<DockerContainer>;
/** Update auto-start configuration for Docker containers */
updateAutostartConfiguration: Scalars['Boolean']['output'];
/** Update a container to the latest image */
@@ -995,6 +1008,7 @@ export type InfoCpu = Node & {
manufacturer?: Maybe<Scalars['String']['output']>;
/** CPU model */
model?: Maybe<Scalars['String']['output']>;
packages: CpuPackages;
/** Number of physical processors */
processors?: Maybe<Scalars['Int']['output']>;
/** CPU revision */
@@ -1011,6 +1025,8 @@ export type InfoCpu = Node & {
stepping?: Maybe<Scalars['Int']['output']>;
/** Number of CPU threads */
threads?: Maybe<Scalars['Int']['output']>;
/** Per-package array of core/thread pairs, e.g. [[[0,1],[2,3]], [[4,5],[6,7]]] */
topology: Array<Array<Array<Scalars['Int']['output']>>>;
/** CPU vendor */
vendor?: Maybe<Scalars['String']['output']>;
/** CPU voltage */
@@ -2202,6 +2218,7 @@ export type Subscription = {
parityHistorySubscription: ParityCheck;
serversSubscription: Server;
systemMetricsCpu: CpuUtilization;
systemMetricsCpuTelemetry: CpuPackages;
systemMetricsMemory: MemoryUtilization;
upsUpdates: UpsDevice;
};
@@ -2929,6 +2946,11 @@ export type UnpauseDockerContainerMutationVariables = Exact<{
export type UnpauseDockerContainerMutation = { __typename?: 'Mutation', docker: { __typename?: 'DockerMutations', unpause: { __typename?: 'DockerContainer', id: string, names: Array<string>, state: ContainerState } } };
export type UpdateAllDockerContainersMutationVariables = Exact<{ [key: string]: never; }>;
export type UpdateAllDockerContainersMutation = { __typename?: 'Mutation', docker: { __typename?: 'DockerMutations', updateAllContainers: Array<{ __typename?: 'DockerContainer', id: string, names: Array<string>, state: ContainerState, isUpdateAvailable?: boolean | null, isRebuildReady?: boolean | null }> } };
export type UpdateDockerAutostartConfigurationMutationVariables = Exact<{
entries: Array<DockerAutostartEntryInput> | DockerAutostartEntryInput;
persistUserPreferences?: InputMaybe<Scalars['Boolean']['input']>;
@@ -3199,6 +3221,7 @@ export const SetDockerFolderChildrenDocument = {"kind":"Document","definitions":
export const StartDockerContainerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"StartDockerContainer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PrefixedID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"docker"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"start"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"names"}},{"kind":"Field","name":{"kind":"Name","value":"state"}}]}}]}}]}}]} as unknown as DocumentNode<StartDockerContainerMutation, StartDockerContainerMutationVariables>;
export const StopDockerContainerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"StopDockerContainer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PrefixedID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"docker"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stop"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"names"}},{"kind":"Field","name":{"kind":"Name","value":"state"}}]}}]}}]}}]} as unknown as DocumentNode<StopDockerContainerMutation, StopDockerContainerMutationVariables>;
export const UnpauseDockerContainerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UnpauseDockerContainer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PrefixedID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"docker"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unpause"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"names"}},{"kind":"Field","name":{"kind":"Name","value":"state"}}]}}]}}]}}]} as unknown as DocumentNode<UnpauseDockerContainerMutation, UnpauseDockerContainerMutationVariables>;
export const UpdateAllDockerContainersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateAllDockerContainers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"docker"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateAllContainers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"names"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"isUpdateAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"isRebuildReady"}}]}}]}}]}}]} as unknown as DocumentNode<UpdateAllDockerContainersMutation, UpdateAllDockerContainersMutationVariables>;
export const UpdateDockerAutostartConfigurationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateDockerAutostartConfiguration"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"entries"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DockerAutostartEntryInput"}}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"persistUserPreferences"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"docker"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateAutostartConfiguration"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"entries"},"value":{"kind":"Variable","name":{"kind":"Name","value":"entries"}}},{"kind":"Argument","name":{"kind":"Name","value":"persistUserPreferences"},"value":{"kind":"Variable","name":{"kind":"Name","value":"persistUserPreferences"}}}]}]}}]}}]} as unknown as DocumentNode<UpdateDockerAutostartConfigurationMutation, UpdateDockerAutostartConfigurationMutationVariables>;
export const UpdateDockerContainerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateDockerContainer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PrefixedID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"docker"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateContainer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"names"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"isUpdateAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"isRebuildReady"}}]}}]}}]}}]} as unknown as DocumentNode<UpdateDockerContainerMutation, UpdateDockerContainerMutationVariables>;
export const UpdateDockerContainersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateDockerContainers"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"ids"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PrefixedID"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"docker"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateContainers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"ids"},"value":{"kind":"Variable","name":{"kind":"Name","value":"ids"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"names"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"isUpdateAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"isRebuildReady"}}]}}]}}]}}]} as unknown as DocumentNode<UpdateDockerContainersMutation, UpdateDockerContainersMutationVariables>;