Skip to content

Commit 3bf110c

Browse files
committed
feat: save column visibility preferences across visits
1 parent e29e8d1 commit 3bf110c

File tree

12 files changed

+342
-77
lines changed

12 files changed

+342
-77
lines changed

api/generated-schema.graphql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2466,6 +2466,7 @@ type Mutation {
24662466
moveDockerItemsToPosition(sourceEntryIds: [String!]!, destinationFolderId: String!, position: Float!): ResolvedOrganizerV1!
24672467
renameDockerFolder(folderId: String!, newName: String!): ResolvedOrganizerV1!
24682468
createDockerFolderWithItems(name: String!, parentId: String, sourceEntryIds: [String!], position: Float): ResolvedOrganizerV1!
2469+
updateDockerViewPreferences(viewId: String = "default", prefs: JSON!): ResolvedOrganizerV1!
24692470
syncDockerTemplatePaths: DockerTemplateSyncResult!
24702471
refreshDockerDigests: Boolean!
24712472

api/src/unraid-api/cli/generated/graphql.ts

Lines changed: 90 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,7 @@ export enum ContainerPortType {
525525

526526
export enum ContainerState {
527527
EXITED = 'EXITED',
528+
PAUSED = 'PAUSED',
528529
RUNNING = 'RUNNING'
529530
}
530531

@@ -720,6 +721,7 @@ export type DockerContainer = Node & {
720721
sizeRootFs?: Maybe<Scalars['BigInt']['output']>;
721722
state: ContainerState;
722723
status: Scalars['String']['output'];
724+
templatePath?: Maybe<Scalars['String']['output']>;
723725
};
724726

725727
export type DockerContainerOverviewForm = {
@@ -732,10 +734,21 @@ export type DockerContainerOverviewForm = {
732734

733735
export type DockerMutations = {
734736
__typename?: 'DockerMutations';
737+
/** Pause (Suspend) a container */
738+
pause: DockerContainer;
735739
/** Start a container */
736740
start: DockerContainer;
737741
/** Stop a container */
738742
stop: DockerContainer;
743+
/** Unpause (Resume) a container */
744+
unpause: DockerContainer;
745+
/** Update a container to the latest image */
746+
updateContainer: DockerContainer;
747+
};
748+
749+
750+
export type DockerMutationsPauseArgs = {
751+
id: Scalars['PrefixedID']['input'];
739752
};
740753

741754

@@ -748,6 +761,16 @@ export type DockerMutationsStopArgs = {
748761
id: Scalars['PrefixedID']['input'];
749762
};
750763

764+
765+
export type DockerMutationsUnpauseArgs = {
766+
id: Scalars['PrefixedID']['input'];
767+
};
768+
769+
770+
export type DockerMutationsUpdateContainerArgs = {
771+
id: Scalars['PrefixedID']['input'];
772+
};
773+
751774
export type DockerNetwork = Node & {
752775
__typename?: 'DockerNetwork';
753776
attachable: Scalars['Boolean']['output'];
@@ -767,6 +790,14 @@ export type DockerNetwork = Node & {
767790
scope: Scalars['String']['output'];
768791
};
769792

793+
export type DockerTemplateSyncResult = {
794+
__typename?: 'DockerTemplateSyncResult';
795+
errors: Array<Scalars['String']['output']>;
796+
matched: Scalars['Int']['output'];
797+
scanned: Scalars['Int']['output'];
798+
skipped: Scalars['Int']['output'];
799+
};
800+
770801
export type DynamicRemoteAccessStatus = {
771802
__typename?: 'DynamicRemoteAccessStatus';
772803
/** The type of dynamic remote access that is enabled */
@@ -812,6 +843,21 @@ export type FlashBackupStatus = {
812843
status: Scalars['String']['output'];
813844
};
814845

846+
export type FlatOrganizerEntry = {
847+
__typename?: 'FlatOrganizerEntry';
848+
childrenIds: Array<Scalars['String']['output']>;
849+
depth: Scalars['Float']['output'];
850+
hasChildren: Scalars['Boolean']['output'];
851+
icon?: Maybe<Scalars['String']['output']>;
852+
id: Scalars['String']['output'];
853+
meta?: Maybe<DockerContainer>;
854+
name: Scalars['String']['output'];
855+
parentId?: Maybe<Scalars['String']['output']>;
856+
path: Array<Scalars['String']['output']>;
857+
position: Scalars['Float']['output'];
858+
type: Scalars['String']['output'];
859+
};
860+
815861
export type FormSchema = {
816862
/** The data schema for the form */
817863
dataSchema: Scalars['JSON']['output'];
@@ -1236,6 +1282,7 @@ export type Mutation = {
12361282
connectSignIn: Scalars['Boolean']['output'];
12371283
connectSignOut: Scalars['Boolean']['output'];
12381284
createDockerFolder: ResolvedOrganizerV1;
1285+
createDockerFolderWithItems: ResolvedOrganizerV1;
12391286
/** Creates a new notification record */
12401287
createNotification: Notification;
12411288
/** Deletes all archived notifications on server. */
@@ -1247,20 +1294,26 @@ export type Mutation = {
12471294
/** Initiates a flash drive backup using a configured remote. */
12481295
initiateFlashBackup: FlashBackupStatus;
12491296
moveDockerEntriesToFolder: ResolvedOrganizerV1;
1297+
moveDockerItemsToPosition: ResolvedOrganizerV1;
1298+
/** Creates a notification if an equivalent unread notification does not already exist. */
1299+
notifyIfUnique?: Maybe<Notification>;
12501300
parityCheck: ParityCheckMutations;
12511301
rclone: RCloneMutations;
12521302
/** Reads each notification to recompute & update the overview. */
12531303
recalculateOverview: NotificationOverview;
12541304
refreshDockerDigests: Scalars['Boolean']['output'];
12551305
/** Remove one or more plugins from the API. Returns false if restart was triggered automatically, true if manual restart is required. */
12561306
removePlugin: Scalars['Boolean']['output'];
1307+
renameDockerFolder: ResolvedOrganizerV1;
12571308
setDockerFolderChildren: ResolvedOrganizerV1;
12581309
setupRemoteAccess: Scalars['Boolean']['output'];
1310+
syncDockerTemplatePaths: DockerTemplateSyncResult;
12591311
unarchiveAll: NotificationOverview;
12601312
unarchiveNotifications: NotificationOverview;
12611313
/** Marks a notification as unread. */
12621314
unreadNotification: Notification;
12631315
updateApiSettings: ConnectSettingsValues;
1316+
updateDockerViewPreferences: ResolvedOrganizerV1;
12641317
updateSettings: UpdateSettingsResponse;
12651318
vm: VmMutations;
12661319
};
@@ -1303,6 +1356,14 @@ export type MutationCreateDockerFolderArgs = {
13031356
};
13041357

13051358

1359+
export type MutationCreateDockerFolderWithItemsArgs = {
1360+
name: Scalars['String']['input'];
1361+
parentId?: InputMaybe<Scalars['String']['input']>;
1362+
position?: InputMaybe<Scalars['Float']['input']>;
1363+
sourceEntryIds?: InputMaybe<Array<Scalars['String']['input']>>;
1364+
};
1365+
1366+
13061367
export type MutationCreateNotificationArgs = {
13071368
input: NotificationData;
13081369
};
@@ -1335,11 +1396,29 @@ export type MutationMoveDockerEntriesToFolderArgs = {
13351396
};
13361397

13371398

1399+
export type MutationMoveDockerItemsToPositionArgs = {
1400+
destinationFolderId: Scalars['String']['input'];
1401+
position: Scalars['Float']['input'];
1402+
sourceEntryIds: Array<Scalars['String']['input']>;
1403+
};
1404+
1405+
1406+
export type MutationNotifyIfUniqueArgs = {
1407+
input: NotificationData;
1408+
};
1409+
1410+
13381411
export type MutationRemovePluginArgs = {
13391412
input: PluginManagementInput;
13401413
};
13411414

13421415

1416+
export type MutationRenameDockerFolderArgs = {
1417+
folderId: Scalars['String']['input'];
1418+
newName: Scalars['String']['input'];
1419+
};
1420+
1421+
13431422
export type MutationSetDockerFolderChildrenArgs = {
13441423
childrenIds: Array<Scalars['String']['input']>;
13451424
folderId?: InputMaybe<Scalars['String']['input']>;
@@ -1371,6 +1450,12 @@ export type MutationUpdateApiSettingsArgs = {
13711450
};
13721451

13731452

1453+
export type MutationUpdateDockerViewPreferencesArgs = {
1454+
prefs: Scalars['JSON']['input'];
1455+
viewId?: InputMaybe<Scalars['String']['input']>;
1456+
};
1457+
1458+
13741459
export type MutationUpdateSettingsArgs = {
13751460
input: Scalars['JSON']['input'];
13761461
};
@@ -1446,6 +1531,8 @@ export type Notifications = Node & {
14461531
list: Array<Notification>;
14471532
/** A cached overview of the notifications in the system & their severity. */
14481533
overview: NotificationOverview;
1534+
/** Deduplicated list of unread warning and alert notifications, sorted latest first. */
1535+
warningsAndAlerts: Array<Notification>;
14491536
};
14501537

14511538

@@ -1511,22 +1598,6 @@ export type OidcSessionValidation = {
15111598
valid: Scalars['Boolean']['output'];
15121599
};
15131600

1514-
export type OrganizerContainerResource = {
1515-
__typename?: 'OrganizerContainerResource';
1516-
id: Scalars['String']['output'];
1517-
meta?: Maybe<DockerContainer>;
1518-
name: Scalars['String']['output'];
1519-
type: Scalars['String']['output'];
1520-
};
1521-
1522-
export type OrganizerResource = {
1523-
__typename?: 'OrganizerResource';
1524-
id: Scalars['String']['output'];
1525-
meta?: Maybe<Scalars['JSON']['output']>;
1526-
name: Scalars['String']['output'];
1527-
type: Scalars['String']['output'];
1528-
};
1529-
15301601
export type Owner = {
15311602
__typename?: 'Owner';
15321603
avatar: Scalars['String']['output'];
@@ -1901,16 +1972,6 @@ export type RemoveRoleFromApiKeyInput = {
19011972
role: Role;
19021973
};
19031974

1904-
export type ResolvedOrganizerEntry = OrganizerContainerResource | OrganizerResource | ResolvedOrganizerFolder;
1905-
1906-
export type ResolvedOrganizerFolder = {
1907-
__typename?: 'ResolvedOrganizerFolder';
1908-
children: Array<ResolvedOrganizerEntry>;
1909-
id: Scalars['String']['output'];
1910-
name: Scalars['String']['output'];
1911-
type: Scalars['String']['output'];
1912-
};
1913-
19141975
export type ResolvedOrganizerV1 = {
19151976
__typename?: 'ResolvedOrganizerV1';
19161977
version: Scalars['Float']['output'];
@@ -1919,10 +1980,11 @@ export type ResolvedOrganizerV1 = {
19191980

19201981
export type ResolvedOrganizerView = {
19211982
__typename?: 'ResolvedOrganizerView';
1983+
flatEntries: Array<FlatOrganizerEntry>;
19221984
id: Scalars['String']['output'];
19231985
name: Scalars['String']['output'];
19241986
prefs?: Maybe<Scalars['JSON']['output']>;
1925-
root: ResolvedOrganizerEntry;
1987+
rootId: Scalars['String']['output'];
19261988
};
19271989

19281990
/** Available resources for permissions */
@@ -2068,6 +2130,7 @@ export type Subscription = {
20682130
logFile: LogFileContent;
20692131
notificationAdded: Notification;
20702132
notificationsOverview: NotificationOverview;
2133+
notificationsWarningsAndAlerts: Array<Notification>;
20712134
ownerSubscription: Owner;
20722135
parityHistorySubscription: ParityCheck;
20732136
serversSubscription: Server;

api/src/unraid-api/graph/resolvers/docker/docker.resolver.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Args, Info, Mutation, Query, ResolveField, Resolver } from '@nestjs/gra
33
import type { GraphQLResolveInfo } from 'graphql';
44
import { AuthAction, Resource } from '@unraid/shared/graphql.model.js';
55
import { UsePermissions } from '@unraid/shared/use-permissions.directive.js';
6+
import { GraphQLJSON } from 'graphql-scalars';
67

78
import { UseFeatureFlag } from '@app/unraid-api/decorators/use-feature-flag.decorator.js';
89
import { DockerFormService } from '@app/unraid-api/graph/resolvers/docker/docker-form.service.js';
@@ -215,6 +216,23 @@ export class DockerResolver {
215216
return this.dockerOrganizerService.resolveOrganizer(organizer);
216217
}
217218

219+
@UseFeatureFlag('ENABLE_NEXT_DOCKER_RELEASE')
220+
@UsePermissions({
221+
action: AuthAction.UPDATE_ANY,
222+
resource: Resource.DOCKER,
223+
})
224+
@Mutation(() => ResolvedOrganizerV1)
225+
public async updateDockerViewPreferences(
226+
@Args('viewId', { nullable: true, defaultValue: 'default' }) viewId: string,
227+
@Args('prefs', { type: () => GraphQLJSON }) prefs: Record<string, unknown>
228+
) {
229+
const organizer = await this.dockerOrganizerService.updateViewPreferences({
230+
viewId,
231+
prefs,
232+
});
233+
return this.dockerOrganizerService.resolveOrganizer(organizer);
234+
}
235+
218236
@UseFeatureFlag('ENABLE_NEXT_DOCKER_RELEASE')
219237
@UsePermissions({
220238
action: AuthAction.READ_ANY,

api/src/unraid-api/graph/resolvers/docker/organizer/docker-organizer.service.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,4 +344,24 @@ export class DockerOrganizerService {
344344
this.dockerConfigService.replaceConfig(validated);
345345
return validated;
346346
}
347+
348+
async updateViewPreferences(params: {
349+
viewId?: string;
350+
prefs: Record<string, unknown>;
351+
}): Promise<OrganizerV1> {
352+
const { viewId = DEFAULT_ORGANIZER_VIEW_ID, prefs } = params;
353+
const organizer = await this.syncAndGetOrganizer();
354+
const newOrganizer = structuredClone(organizer);
355+
356+
const view = newOrganizer.views[viewId];
357+
if (!view) {
358+
throw new AppError(`View '${viewId}' not found`);
359+
}
360+
361+
view.prefs = prefs;
362+
363+
const validated = await this.dockerConfigService.validate(newOrganizer);
364+
this.dockerConfigService.replaceConfig(validated);
365+
return validated;
366+
}
347367
}

web/src/components/Docker/DockerContainerManagement.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ const { result, loading, refetch } = useQuery<{
162162
id: string;
163163
name: string;
164164
rootId: string;
165+
prefs?: Record<string, unknown> | null;
165166
flatEntries: FlatOrganizerEntry[];
166167
}>;
167168
};
@@ -174,6 +175,7 @@ const { result, loading, refetch } = useQuery<{
174175
175176
const flatEntries = computed(() => result.value?.docker?.organizer?.views?.[0]?.flatEntries || []);
176177
const rootFolderId = computed(() => result.value?.docker?.organizer?.views?.[0]?.rootId || 'root');
178+
const viewPrefs = computed(() => result.value?.docker?.organizer?.views?.[0]?.prefs || null);
177179
178180
const containers = computed<DockerContainer[]>(() => result.value?.docker?.containers || []);
179181
@@ -270,6 +272,7 @@ const isDetailsDisabled = computed(() => props.disabled || isSwitching.value);
270272
:containers="containers"
271273
:flat-entries="flatEntries"
272274
:root-folder-id="rootFolderId"
275+
:view-prefs="viewPrefs"
273276
:loading="loading"
274277
:active-id="activeId"
275278
:selected-ids="selectedIds"

web/src/components/Docker/DockerContainerOverview.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const { result, loading, error, refetch } = useQuery<{
1717
id: string;
1818
name: string;
1919
rootId: string;
20+
prefs?: Record<string, unknown> | null;
2021
flatEntries: FlatOrganizerEntry[];
2122
}>;
2223
};
@@ -28,6 +29,7 @@ const { result, loading, error, refetch } = useQuery<{
2829
const containers = computed<DockerContainer[]>(() => []);
2930
const flatEntries = computed(() => result.value?.docker?.organizer?.views?.[0]?.flatEntries || []);
3031
const rootFolderId = computed(() => result.value?.docker?.organizer?.views?.[0]?.rootId || 'root');
32+
const viewPrefs = computed(() => result.value?.docker?.organizer?.views?.[0]?.prefs || null);
3133
3234
const handleRefresh = async () => {
3335
await refetch({ skipCache: true });
@@ -50,6 +52,7 @@ const handleRefresh = async () => {
5052
:containers="containers"
5153
:flat-entries="flatEntries"
5254
:root-folder-id="rootFolderId"
55+
:view-prefs="viewPrefs"
5356
:loading="loading"
5457
@created-folder="handleRefresh"
5558
/>

0 commit comments

Comments
 (0)