feat(onboarding): implement upgrade step completion tracking

- Introduced `OnboardingMutations` to manage the completion of upgrade onboarding steps.
- Added `completeUpgradeStep` mutation to mark specific onboarding steps as completed, returning relevant upgrade information.
- Enhanced `UpgradeInfo` type to include completed steps for better tracking of user progress during OS upgrades.
- Updated `OsVersionTracker` to persist completed onboarding steps alongside OS version tracking.
- Integrated new GraphQL queries and mutations in the frontend to support the onboarding process.

This update improves the user experience by providing a structured way to track and manage onboarding steps during OS upgrades, ensuring users receive appropriate guidance throughout the process.
This commit is contained in:
Eli Bosley
2025-10-13 11:27:53 -04:00
parent 68cd5e4f47
commit f5564a4e23
20 changed files with 689 additions and 189 deletions

View File

@@ -5,6 +5,5 @@
"ssoSubIds": [],
"plugins": [
"unraid-api-plugin-connect"
],
"lastSeenOsVersion": "6.11.2"
]
}

View File

@@ -781,6 +781,70 @@ type Settings implements Node {
api: ApiConfig!
}
type CoreVersions {
"""Unraid version"""
unraid: String
"""Unraid API version"""
api: String
"""Kernel version"""
kernel: String
}
type PackageVersions {
"""OpenSSL version"""
openssl: String
"""Node.js version"""
node: String
"""npm version"""
npm: String
"""pm2 version"""
pm2: String
"""Git version"""
git: String
"""nginx version"""
nginx: String
"""PHP version"""
php: String
"""Docker version"""
docker: String
}
type UpgradeInfo {
"""Whether the OS version has changed since last boot"""
isUpgrade: Boolean!
"""Previous OS version before upgrade"""
previousVersion: String
"""Current OS version"""
currentVersion: String
"""Onboarding step identifiers completed for the current OS version"""
completedSteps: [String!]!
}
type InfoVersions implements Node {
id: PrefixedID!
"""Core system versions"""
core: CoreVersions!
"""Software package versions"""
packages: PackageVersions
"""OS upgrade information"""
upgrade: UpgradeInfo!
}
type RCloneDrive {
"""Provider name"""
name: String!
@@ -1029,6 +1093,20 @@ input DeleteRCloneRemoteInput {
name: String!
}
"""Onboarding related mutations"""
type OnboardingMutations {
"""
Mark an upgrade onboarding step as completed for the current OS version
"""
completeUpgradeStep(input: CompleteUpgradeStepInput!): UpgradeInfo!
}
"""Input for marking an upgrade onboarding step as completed"""
input CompleteUpgradeStepInput {
"""Identifier of the onboarding step to mark completed"""
stepId: String!
}
type Config implements Node {
id: PrefixedID!
valid: Boolean
@@ -1927,67 +2005,6 @@ type InfoBaseboard implements Node {
memSlots: Float
}
type CoreVersions {
"""Unraid version"""
unraid: String
"""Unraid API version"""
api: String
"""Kernel version"""
kernel: String
}
type PackageVersions {
"""OpenSSL version"""
openssl: String
"""Node.js version"""
node: String
"""npm version"""
npm: String
"""pm2 version"""
pm2: String
"""Git version"""
git: String
"""nginx version"""
nginx: String
"""PHP version"""
php: String
"""Docker version"""
docker: String
}
type UpgradeInfo {
"""Whether the OS version has changed since last boot"""
isUpgrade: Boolean!
"""Previous OS version before upgrade"""
previousVersion: String
"""Current OS version"""
currentVersion: String
}
type InfoVersions implements Node {
id: PrefixedID!
"""Core system versions"""
core: CoreVersions!
"""Software package versions"""
packages: PackageVersions
"""OS upgrade information"""
upgrade: UpgradeInfo!
}
type Info implements Node {
id: PrefixedID!
@@ -2709,6 +2726,7 @@ type Mutation {
apiKey: ApiKeyMutations!
customization: CustomizationMutations!
rclone: RCloneMutations!
onboarding: OnboardingMutations!
createDockerFolder(name: String!, parentId: String, childrenIds: [String!]): ResolvedOrganizerV1!
setDockerFolderChildren(folderId: String, childrenIds: [String!]!): ResolvedOrganizerV1!
deleteDockerEntries(entryIds: [String!]!): ResolvedOrganizerV1!

View File

@@ -434,6 +434,12 @@ export type CloudResponse = {
status: Scalars['String']['output'];
};
/** Input for marking an upgrade onboarding step as completed */
export type CompleteUpgradeStepInput = {
/** Identifier of the onboarding step to mark completed */
stepId: Scalars['String']['input'];
};
export type Config = Node & {
__typename?: 'Config';
error?: Maybe<Scalars['String']['output']>;
@@ -1464,6 +1470,7 @@ export type Mutation = {
moveDockerItemsToPosition: ResolvedOrganizerV1;
/** Creates a notification if an equivalent unread notification does not already exist. */
notifyIfUnique?: Maybe<Notification>;
onboarding: OnboardingMutations;
parityCheck: ParityCheckMutations;
rclone: RCloneMutations;
/** Reads each notification to recompute & update the overview. */
@@ -1774,6 +1781,19 @@ export type OidcSessionValidation = {
valid: Scalars['Boolean']['output'];
};
/** Onboarding related mutations */
export type OnboardingMutations = {
__typename?: 'OnboardingMutations';
/** Mark an upgrade onboarding step as completed for the current OS version */
completeUpgradeStep: UpgradeInfo;
};
/** Onboarding related mutations */
export type OnboardingMutationsCompleteUpgradeStepArgs = {
input: CompleteUpgradeStepInput;
};
export type Owner = {
__typename?: 'Owner';
avatar: Scalars['String']['output'];
@@ -2614,6 +2634,8 @@ export type UpdateSystemTimeInput = {
export type UpgradeInfo = {
__typename?: 'UpgradeInfo';
/** Onboarding step identifiers completed for the current OS version */
completedSteps: Array<Scalars['String']['output']>;
/** Current OS version */
currentVersion?: Maybe<Scalars['String']['output']>;
/** Whether the OS version has changed since last boot */

View File

@@ -8,7 +8,7 @@ import { csvStringToArray } from '@unraid/shared/util/data.js';
import { isConnectPluginInstalled } from '@app/connect-plugin-cleanup.js';
import { API_VERSION, PATHS_CONFIG_MODULES } from '@app/environment.js';
import { OsVersionTrackerModule } from '@app/unraid-api/config/os-version-tracker.module.js';
import { OnboardingTrackerModule } from '@app/unraid-api/config/onboarding-tracker.module.js';
export { type ApiConfig };
@@ -120,8 +120,8 @@ export class ApiConfigPersistence
// apiConfig should be registered in root config in app.module.ts, not here.
@Module({
imports: [OsVersionTrackerModule],
imports: [OnboardingTrackerModule],
providers: [ApiConfigPersistence],
exports: [ApiConfigPersistence, OsVersionTrackerModule],
exports: [ApiConfigPersistence, OnboardingTrackerModule],
})
export class ApiConfigModule {}

View File

@@ -9,7 +9,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { API_VERSION, PATHS_CONFIG_MODULES } from '@app/environment.js';
import { ApiConfigPersistence, loadApiConfig } from '@app/unraid-api/config/api-config.module.js';
import { OsVersionTracker } from '@app/unraid-api/config/os-version-tracker.module.js';
import { OnboardingTracker } from '@app/unraid-api/config/onboarding-tracker.module.js';
vi.mock('@app/core/utils/files/file-exists.js', () => ({
fileExists: vi.fn(),
@@ -104,16 +104,20 @@ describe('ApiConfigPersistence', () => {
});
});
describe('OsVersionTracker', () => {
const trackerPath = path.join(PATHS_CONFIG_MODULES, 'os-version-tracker.json');
describe('OnboardingTracker', () => {
const trackerPath = path.join(PATHS_CONFIG_MODULES, 'onboarding-tracker.json');
let configService: ConfigService;
let setMock: ReturnType<typeof vi.fn>;
let configStore: Record<string, unknown>;
beforeEach(() => {
setMock = vi.fn();
configStore = {};
setMock = vi.fn((key: string, value: unknown) => {
configStore[key] = value;
});
configService = {
set: setMock,
get: vi.fn(),
get: vi.fn((key: string) => configStore[key]),
getOrThrow: vi.fn(),
} as any;
@@ -121,7 +125,7 @@ describe('OsVersionTracker', () => {
mockAtomicWriteFile.mockReset();
});
it('persists current version when tracker is missing', async () => {
it('defers persisting last seen version until shutdown', async () => {
mockReadFile.mockImplementation(async (filePath) => {
if (filePath === '/etc/unraid-version') {
return 'version="7.2.0-beta.3.4"\n';
@@ -129,12 +133,17 @@ describe('OsVersionTracker', () => {
throw Object.assign(new Error('Not found'), { code: 'ENOENT' });
});
const tracker = new OsVersionTracker(configService);
const tracker = new OnboardingTracker(configService);
await tracker.onApplicationBootstrap();
expect(setMock).toHaveBeenCalledWith('api.currentOsVersion', '7.2.0-beta.3.4');
expect(setMock).toHaveBeenCalledWith('onboardingTracker.currentVersion', '7.2.0-beta.3.4');
expect(setMock).toHaveBeenCalledWith('store.emhttp.var.version', '7.2.0-beta.3.4');
expect(setMock).toHaveBeenCalledWith('api.lastSeenOsVersion', undefined);
expect(setMock).toHaveBeenCalledWith('onboardingTracker.lastTrackedVersion', undefined);
expect(setMock).toHaveBeenCalledWith('onboardingTracker.completedSteps', {});
expect(configStore['api.lastSeenOsVersion']).toBeUndefined();
expect(mockAtomicWriteFile).not.toHaveBeenCalled();
await tracker.onApplicationShutdown();
expect(mockAtomicWriteFile).toHaveBeenCalledWith(
trackerPath,
@@ -152,30 +161,130 @@ describe('OsVersionTracker', () => {
return JSON.stringify({
lastTrackedVersion: '6.12.0',
updatedAt: '2024-01-01T00:00:00.000Z',
completedSteps: {
timezone: {
version: '6.12.0',
completedAt: '2024-01-02T00:00:00.000Z',
},
},
});
}
throw Object.assign(new Error('Not found'), { code: 'ENOENT' });
});
const tracker = new OsVersionTracker(configService);
const tracker = new OnboardingTracker(configService);
await tracker.onApplicationBootstrap();
expect(setMock).toHaveBeenCalledWith('api.currentOsVersion', '6.12.0');
expect(setMock).toHaveBeenCalledWith('onboardingTracker.currentVersion', '6.12.0');
expect(setMock).toHaveBeenCalledWith('store.emhttp.var.version', '6.12.0');
expect(setMock).toHaveBeenCalledWith('api.lastSeenOsVersion', '6.12.0');
expect(setMock).toHaveBeenCalledWith('onboardingTracker.lastTrackedVersion', '6.12.0');
expect(setMock).toHaveBeenCalledWith(
'onboardingTracker.completedSteps',
expect.objectContaining({
timezone: expect.objectContaining({ version: '6.12.0' }),
})
);
expect(configStore['api.lastSeenOsVersion']).toBe('6.12.0');
expect(mockAtomicWriteFile).not.toHaveBeenCalled();
await tracker.onApplicationShutdown();
expect(mockAtomicWriteFile).not.toHaveBeenCalled();
});
it('keeps previous version available to signal upgrade until shutdown', async () => {
mockReadFile.mockImplementation(async (filePath) => {
if (filePath === '/etc/unraid-version') {
return 'version="7.1.0"\n';
}
if (filePath === trackerPath) {
return JSON.stringify({
lastTrackedVersion: '7.0.0',
updatedAt: '2025-01-01T00:00:00.000Z',
completedSteps: {},
});
}
throw Object.assign(new Error('Not found'), { code: 'ENOENT' });
});
const tracker = new OnboardingTracker(configService);
await tracker.onApplicationBootstrap();
const snapshot = tracker.getUpgradeSnapshot();
expect(snapshot.currentVersion).toBe('7.1.0');
expect(snapshot.lastTrackedVersion).toBe('7.0.0');
expect(snapshot.completedSteps).toEqual([]);
expect(configStore['onboardingTracker.lastTrackedVersion']).toBe('7.0.0');
expect(configStore['store.emhttp.var.version']).toBe('7.1.0');
expect(configStore['onboardingTracker.completedSteps']).toEqual({});
expect(configStore['api.lastSeenOsVersion']).toBe('7.0.0');
expect(mockAtomicWriteFile).not.toHaveBeenCalled();
});
it('handles missing version file gracefully', async () => {
mockReadFile.mockRejectedValue(new Error('permission denied'));
const tracker = new OsVersionTracker(configService);
const tracker = new OnboardingTracker(configService);
await tracker.onApplicationBootstrap();
expect(setMock).toHaveBeenCalledWith('api.currentOsVersion', undefined);
expect(setMock).toHaveBeenCalledWith('onboardingTracker.currentVersion', undefined);
expect(setMock).toHaveBeenCalledWith('store.emhttp.var.version', undefined);
expect(setMock).toHaveBeenCalledWith('api.lastSeenOsVersion', undefined);
expect(setMock).toHaveBeenCalledWith('onboardingTracker.lastTrackedVersion', undefined);
expect(setMock).toHaveBeenCalledWith('onboardingTracker.completedSteps', {});
expect(mockAtomicWriteFile).not.toHaveBeenCalled();
expect(configStore['api.lastSeenOsVersion']).toBeUndefined();
});
it('marks onboarding steps complete for the current version without clearing upgrade flag', async () => {
mockReadFile.mockImplementation(async (filePath) => {
if (filePath === '/etc/unraid-version') {
return 'version="7.2.0"\n';
}
if (filePath === trackerPath) {
return JSON.stringify({
lastTrackedVersion: '6.12.0',
updatedAt: '2025-01-01T00:00:00.000Z',
completedSteps: {},
});
}
throw Object.assign(new Error('Not found'), { code: 'ENOENT' });
});
const tracker = new OnboardingTracker(configService);
await tracker.onApplicationBootstrap();
expect(configStore['store.emhttp.var.version']).toBe('7.2.0');
expect(configStore['onboardingTracker.lastTrackedVersion']).toBe('6.12.0');
expect(configStore['api.lastSeenOsVersion']).toBe('6.12.0');
setMock.mockClear();
mockAtomicWriteFile.mockReset();
const snapshot = await tracker.markStepCompleted('timezone');
expect(snapshot.currentVersion).toBe('7.2.0');
expect(snapshot.completedSteps).toContain('timezone');
expect(snapshot.lastTrackedVersion).toBe('6.12.0');
expect(mockAtomicWriteFile).toHaveBeenCalledWith(
trackerPath,
expect.stringContaining('"timezone"'),
{ mode: 0o644 }
);
expect(setMock).toHaveBeenCalledWith(
'onboardingTracker.completedSteps',
expect.objectContaining({
timezone: expect.objectContaining({ version: '7.2.0' }),
})
);
expect(setMock).toHaveBeenCalledWith('onboardingTracker.lastTrackedVersion', '6.12.0');
const postSnapshot = tracker.getUpgradeSnapshot();
expect(postSnapshot.lastTrackedVersion).toBe('6.12.0');
});
});

View File

@@ -0,0 +1,224 @@
import {
Injectable,
Logger,
Module,
OnApplicationBootstrap,
OnApplicationShutdown,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { readFile } from 'fs/promises';
import path from 'path';
import { writeFile } from 'atomically';
import { PATHS_CONFIG_MODULES } from '@app/environment.js';
const TRACKER_FILE_NAME = 'onboarding-tracker.json';
const LEGACY_TRACKER_FILE_NAME = 'os-version-tracker.json';
const CONFIG_PREFIX = 'onboardingTracker';
const OS_VERSION_FILE_PATH = '/etc/unraid-version';
type CompletedStepState = {
version: string;
completedAt: string;
};
type TrackerState = {
lastTrackedVersion?: string;
updatedAt?: string;
completedSteps?: Record<string, CompletedStepState>;
};
export type UpgradeProgressSnapshot = {
currentVersion?: string;
lastTrackedVersion?: string;
completedSteps: string[];
};
@Injectable()
export class OnboardingTracker implements OnApplicationBootstrap, OnApplicationShutdown {
private readonly logger = new Logger(OnboardingTracker.name);
private readonly trackerPath = path.join(PATHS_CONFIG_MODULES, TRACKER_FILE_NAME);
private readonly legacyTrackerPath = path.join(PATHS_CONFIG_MODULES, LEGACY_TRACKER_FILE_NAME);
private state: TrackerState = {};
private sessionLastTrackedVersion?: string;
private currentVersion?: string;
constructor(private readonly configService: ConfigService) {}
async onApplicationBootstrap() {
this.currentVersion = await this.readCurrentVersion();
if (!this.currentVersion) {
this.state = {};
this.sessionLastTrackedVersion = undefined;
this.syncConfig(undefined);
return;
}
const previousState = await this.readTrackerState();
this.state = previousState ?? {};
this.sessionLastTrackedVersion = previousState?.lastTrackedVersion;
this.syncConfig(this.currentVersion);
}
async onApplicationShutdown() {
if (!this.currentVersion) {
return;
}
await this.ensureStateLoaded();
if (this.state.lastTrackedVersion === this.currentVersion) {
return;
}
const updatedState: TrackerState = {
...this.state,
lastTrackedVersion: this.currentVersion,
updatedAt: new Date().toISOString(),
};
await this.writeTrackerState(updatedState);
this.sessionLastTrackedVersion = this.currentVersion;
}
getUpgradeSnapshot(): UpgradeProgressSnapshot {
const currentVersion =
this.configService.get<string>(`${CONFIG_PREFIX}.currentVersion`) ??
this.configService.get<string>('store.emhttp.var.version') ??
undefined;
const lastTrackedVersion =
this.configService.get<string>(`${CONFIG_PREFIX}.lastTrackedVersion`) ?? undefined;
const completedSteps =
currentVersion && this.state.completedSteps
? this.completedStepsForVersion(currentVersion)
: [];
return {
currentVersion,
lastTrackedVersion,
completedSteps,
};
}
async markStepCompleted(stepId: string): Promise<UpgradeProgressSnapshot> {
const currentVersion =
this.configService.get<string>(`${CONFIG_PREFIX}.currentVersion`) ??
this.configService.get<string>('store.emhttp.var.version') ??
undefined;
if (!currentVersion) {
this.logger.warn(
`Unable to mark onboarding step '${stepId}' as completed; current OS version unknown`
);
return this.getUpgradeSnapshot();
}
await this.ensureStateLoaded();
const completedSteps = this.state.completedSteps ?? {};
const existing = completedSteps[stepId];
if (existing?.version === currentVersion) {
return this.getUpgradeSnapshot();
}
completedSteps[stepId] = {
version: currentVersion,
completedAt: new Date().toISOString(),
};
const updatedState: TrackerState = {
...this.state,
completedSteps,
updatedAt: new Date().toISOString(),
};
await this.writeTrackerState(updatedState);
this.syncConfig(currentVersion);
return this.getUpgradeSnapshot();
}
private async ensureStateLoaded() {
if (Object.keys(this.state).length > 0) {
return;
}
this.state = (await this.readTrackerState()) ?? {};
}
private completedStepsForVersion(version: string): string[] {
const completedEntries = this.state.completedSteps ?? {};
return Object.entries(completedEntries)
.filter(([, value]) => value?.version === version)
.map(([stepId]) => stepId);
}
private syncConfig(currentVersion: string | undefined) {
const completedStepsMap = this.state.completedSteps ?? {};
this.configService.set(`${CONFIG_PREFIX}.currentVersion`, currentVersion);
this.configService.set('store.emhttp.var.version', currentVersion);
this.configService.set(`${CONFIG_PREFIX}.lastTrackedVersion`, this.sessionLastTrackedVersion);
this.configService.set(`${CONFIG_PREFIX}.completedSteps`, completedStepsMap);
this.configService.set('api.lastSeenOsVersion', this.sessionLastTrackedVersion);
}
private async readCurrentVersion(): Promise<string | undefined> {
try {
const contents = await readFile(OS_VERSION_FILE_PATH, 'utf8');
const match = contents.match(/^\s*version\s*=\s*"([^"]+)"\s*$/m);
return match?.[1]?.trim() || undefined;
} catch (error) {
this.logger.error(error, `Failed to read current OS version from ${OS_VERSION_FILE_PATH}`);
return undefined;
}
}
private async readTrackerState(): Promise<TrackerState | undefined> {
try {
const content = await readFile(this.trackerPath, 'utf8');
return JSON.parse(content) as TrackerState;
} catch (error) {
this.logger.debug(error, `Unable to read onboarding tracker state at ${this.trackerPath}`);
const legacyState = await this.readLegacyTrackerState();
if (legacyState) {
return legacyState;
}
return undefined;
}
}
private async readLegacyTrackerState(): Promise<TrackerState | undefined> {
try {
const content = await readFile(this.legacyTrackerPath, 'utf8');
this.logger.log(
`Loaded legacy onboarding tracker state from ${LEGACY_TRACKER_FILE_NAME}; will persist to ${TRACKER_FILE_NAME}`
);
return JSON.parse(content) as TrackerState;
} catch (error) {
this.logger.debug(
error,
`Unable to read legacy onboarding tracker state at ${this.legacyTrackerPath}`
);
return undefined;
}
}
private async writeTrackerState(state: TrackerState): Promise<void> {
try {
await writeFile(this.trackerPath, JSON.stringify(state, null, 2), { mode: 0o644 });
this.state = state;
} catch (error) {
this.logger.error(error, 'Failed to persist onboarding tracker state');
}
}
}
@Module({
providers: [OnboardingTracker],
exports: [OnboardingTracker],
})
export class OnboardingTrackerModule {}

View File

@@ -1,83 +0,0 @@
import { Injectable, Logger, Module, OnApplicationBootstrap } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { readFile } from 'fs/promises';
import path from 'path';
import { writeFile } from 'atomically';
import { PATHS_CONFIG_MODULES } from '@app/environment.js';
const OS_VERSION_FILE_PATH = '/etc/unraid-version';
const VERSION_TRACKER_FILE = 'os-version-tracker.json';
type OsVersionTrackerState = {
lastTrackedVersion?: string;
updatedAt?: string;
};
@Injectable()
export class OsVersionTracker implements OnApplicationBootstrap {
private readonly logger = new Logger(OsVersionTracker.name);
private readonly trackerPath = path.join(PATHS_CONFIG_MODULES, VERSION_TRACKER_FILE);
constructor(private readonly configService: ConfigService) {}
async onApplicationBootstrap() {
const currentVersion = await this.readCurrentVersion();
if (!currentVersion) {
this.configService.set('api.currentOsVersion', undefined);
this.configService.set('store.emhttp.var.version', undefined);
this.configService.set('api.lastSeenOsVersion', undefined);
return;
}
const previousState = await this.readTrackerState();
const lastTrackedVersion = previousState?.lastTrackedVersion;
this.configService.set('api.currentOsVersion', currentVersion);
this.configService.set('store.emhttp.var.version', currentVersion);
this.configService.set('api.lastSeenOsVersion', lastTrackedVersion);
if (lastTrackedVersion !== currentVersion) {
await this.writeTrackerState({
lastTrackedVersion: currentVersion,
updatedAt: new Date().toISOString(),
});
}
}
private async readCurrentVersion(): Promise<string | undefined> {
try {
const contents = await readFile(OS_VERSION_FILE_PATH, 'utf8');
const match = contents.match(/^\s*version\s*=\s*"([^"]+)"\s*$/m);
return match?.[1]?.trim() || undefined;
} catch (error) {
this.logger.error(error, `Failed to read current OS version from ${OS_VERSION_FILE_PATH}`);
return undefined;
}
}
private async readTrackerState(): Promise<OsVersionTrackerState | undefined> {
try {
const content = await readFile(this.trackerPath, 'utf8');
return JSON.parse(content) as OsVersionTrackerState;
} catch (error) {
this.logger.debug(error, `Unable to read OS version tracker state at ${this.trackerPath}`);
return undefined;
}
}
private async writeTrackerState(state: OsVersionTrackerState): Promise<void> {
try {
await writeFile(this.trackerPath, JSON.stringify(state, null, 2), { mode: 0o644 });
} catch (error) {
this.logger.error(error, 'Failed to persist OS version tracker state');
}
}
}
@Module({
providers: [OsVersionTracker],
exports: [OsVersionTracker],
})
export class OsVersionTrackerModule {}

View File

@@ -51,6 +51,12 @@ export class UpgradeInfo {
@Field(() => String, { nullable: true, description: 'Current OS version' })
currentVersion?: string;
@Field(() => [String], {
description: 'Onboarding step identifiers completed for the current OS version',
defaultValue: [],
})
completedSteps!: string[];
}
@ObjectType({ implements: () => Node })

View File

@@ -49,8 +49,23 @@ export class VersionsResolver {
@ResolveField(() => UpgradeInfo)
upgrade(): UpgradeInfo {
const currentVersion = this.configService.get<string>('store.emhttp.var.version');
const lastSeenVersion = this.configService.get<string>('api.lastSeenOsVersion');
const currentVersion =
this.configService.get<string>('onboardingTracker.currentVersion') ??
this.configService.get<string>('store.emhttp.var.version');
const lastSeenVersion =
this.configService.get<string>('onboardingTracker.lastTrackedVersion') ??
this.configService.get<string>('api.lastSeenOsVersion');
const completedStepsMap =
this.configService.get<Record<string, { version: string }>>(
'onboardingTracker.completedSteps'
) ?? {};
const completedSteps =
currentVersion && completedStepsMap
? Object.entries(completedStepsMap)
.filter(([, value]) => value?.version === currentVersion)
.map(([stepId]) => stepId)
: [];
const isUpgrade = Boolean(
lastSeenVersion && currentVersion && lastSeenVersion !== currentVersion
@@ -60,6 +75,7 @@ export class VersionsResolver {
isUpgrade,
previousVersion: isUpgrade ? lastSeenVersion : undefined,
currentVersion: currentVersion || undefined,
completedSteps,
};
}
}

View File

@@ -1,5 +1,6 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { UpgradeInfo } from '@app/unraid-api/graph/resolvers/info/versions/versions.model.js';
import { RCloneRemote } from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js';
/**
@@ -45,6 +46,16 @@ export class RCloneMutations {
deleteRCloneRemote!: boolean;
}
@ObjectType({
description: 'Onboarding related mutations',
})
export class OnboardingMutations {
@Field(() => UpgradeInfo, {
description: 'Mark an upgrade onboarding step as completed for the current OS version',
})
completeUpgradeStep!: UpgradeInfo;
}
@ObjectType()
export class RootMutations {
@Field(() => ArrayMutations, { description: 'Array related mutations' })
@@ -67,4 +78,7 @@ export class RootMutations {
@Field(() => RCloneMutations, { description: 'RClone related mutations' })
rclone: RCloneMutations = new RCloneMutations();
@Field(() => OnboardingMutations, { description: 'Onboarding related mutations' })
onboarding: OnboardingMutations = new OnboardingMutations();
}

View File

@@ -5,6 +5,7 @@ import {
ArrayMutations,
CustomizationMutations,
DockerMutations,
OnboardingMutations,
ParityCheckMutations,
RCloneMutations,
RootMutations,
@@ -47,4 +48,9 @@ export class RootMutationsResolver {
rclone(): RCloneMutations {
return new RCloneMutations();
}
@Mutation(() => OnboardingMutations, { name: 'onboarding' })
onboarding(): OnboardingMutations {
return new OnboardingMutations();
}
}

View File

@@ -0,0 +1,9 @@
import { Field, InputType } from '@nestjs/graphql';
@InputType({
description: 'Input for marking an upgrade onboarding step as completed',
})
export class CompleteUpgradeStepInput {
@Field(() => String, { description: 'Identifier of the onboarding step to mark completed' })
stepId!: string;
}

View File

@@ -0,0 +1,54 @@
import { ConfigService } from '@nestjs/config';
import { Args, ResolveField, Resolver } from '@nestjs/graphql';
import { OnboardingTracker } from '@app/unraid-api/config/onboarding-tracker.module.js';
import { UpgradeInfo } from '@app/unraid-api/graph/resolvers/info/versions/versions.model.js';
import { OnboardingMutations } from '@app/unraid-api/graph/resolvers/mutation/mutation.model.js';
import { CompleteUpgradeStepInput } from '@app/unraid-api/graph/resolvers/onboarding/onboarding.model.js';
@Resolver(() => OnboardingMutations)
export class OnboardingMutationsResolver {
constructor(
private readonly onboardingTracker: OnboardingTracker,
private readonly configService: ConfigService
) {}
@ResolveField(() => UpgradeInfo, {
description: 'Marks an upgrade onboarding step as completed for the current OS version',
})
async completeUpgradeStep(@Args('input') input: CompleteUpgradeStepInput): Promise<UpgradeInfo> {
await this.onboardingTracker.markStepCompleted(input.stepId);
return this.buildUpgradeInfo();
}
private buildUpgradeInfo(): UpgradeInfo {
const currentVersion =
this.configService.get<string>('onboardingTracker.currentVersion') ??
this.configService.get<string>('store.emhttp.var.version');
const lastSeenVersion =
this.configService.get<string>('onboardingTracker.lastTrackedVersion') ??
this.configService.get<string>('api.lastSeenOsVersion');
const completedStepsMap =
this.configService.get<Record<string, { version: string }>>(
'onboardingTracker.completedSteps'
) ?? {};
const completedSteps =
currentVersion && completedStepsMap
? Object.entries(completedStepsMap)
.filter(([, value]) => value?.version === currentVersion)
.map(([stepId]) => stepId)
: [];
const isUpgrade = Boolean(
lastSeenVersion && currentVersion && lastSeenVersion !== currentVersion
);
return {
isUpgrade,
previousVersion: isUpgrade ? lastSeenVersion : undefined,
currentVersion: currentVersion || undefined,
completedSteps,
};
}
}

View File

@@ -17,6 +17,7 @@ import { MetricsModule } from '@app/unraid-api/graph/resolvers/metrics/metrics.m
import { RootMutationsResolver } from '@app/unraid-api/graph/resolvers/mutation/mutation.resolver.js';
import { NotificationsModule } from '@app/unraid-api/graph/resolvers/notifications/notifications.module.js';
import { NotificationsResolver } from '@app/unraid-api/graph/resolvers/notifications/notifications.resolver.js';
import { OnboardingMutationsResolver } from '@app/unraid-api/graph/resolvers/onboarding/onboarding.mutation.js';
import { OnlineResolver } from '@app/unraid-api/graph/resolvers/online/online.resolver.js';
import { OwnerResolver } from '@app/unraid-api/graph/resolvers/owner/owner.resolver.js';
import { RCloneModule } from '@app/unraid-api/graph/resolvers/rclone/rclone.module.js';
@@ -63,6 +64,7 @@ import { MeResolver } from '@app/unraid-api/graph/user/user.resolver.js';
NotificationsResolver,
OnlineResolver,
OwnerResolver,
OnboardingMutationsResolver,
RegistrationResolver,
RootMutationsResolver,
ServerResolver,

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { computed, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useMutation } from '@vue/apollo-composable';
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
import { BrandButton, Dialog } from '@unraid/ui';
@@ -12,6 +13,7 @@ import ActivationPartnerLogo from '~/components/Activation/ActivationPartnerLogo
import ActivationPluginsStep from '~/components/Activation/ActivationPluginsStep.vue';
import ActivationSteps from '~/components/Activation/ActivationSteps.vue';
import ActivationTimezoneStep from '~/components/Activation/ActivationTimezoneStep.vue';
import { COMPLETE_UPGRADE_STEP_MUTATION } from '~/components/Activation/completeUpgradeStep.mutation';
import { useActivationCodeDataStore } from '~/components/Activation/store/activationCodeData';
import { useActivationCodeModalStore } from '~/components/Activation/store/activationCodeModal';
import { useUpgradeOnboardingStore } from '~/components/Activation/store/upgradeOnboarding';
@@ -26,6 +28,7 @@ const { partnerInfo, activationCode, isFreshInstall } = storeToRefs(useActivatio
const upgradeStore = useUpgradeOnboardingStore();
const { shouldShowUpgradeOnboarding, upgradeSteps, currentVersion, previousVersion } =
storeToRefs(upgradeStore);
const { refetchUpgradeInfo } = upgradeStore;
const purchaseStore = usePurchaseStore();
useThemeStore();
@@ -131,7 +134,33 @@ const closeModal = () => {
}
};
const goToNextStep = () => {
const { mutate: completeUpgradeStepMutation } = useMutation(COMPLETE_UPGRADE_STEP_MUTATION);
const markUpgradeStepCompleted = async (stepId: string | null) => {
if (!isUpgradeMode.value || !stepId) return;
try {
await completeUpgradeStepMutation({ input: { stepId } });
await refetchUpgradeInfo();
} catch (error) {
console.error('[ActivationModal] Failed to mark upgrade step completed', error);
}
};
const goToNextStep = async () => {
if (isUpgradeMode.value) {
await markUpgradeStepCompleted(currentStep.value);
if (upgradeSteps.value.length === 0) {
closeModal();
currentStepIndex.value = 0;
return;
}
currentStepIndex.value = Math.min(currentStepIndex.value, upgradeSteps.value.length - 1);
return;
}
if (currentStepIndex.value < availableSteps.value.length - 1) {
currentStepIndex.value++;
} else if (hasActivationCode.value && !isUpgradeMode.value) {
@@ -149,21 +178,21 @@ const goToPreviousStep = () => {
const canGoBack = computed(() => currentStepIndex.value > 0);
const handleTimezoneComplete = () => {
const handleTimezoneComplete = async () => {
console.log('[ActivationModal] Timezone complete, moving to next step');
goToNextStep();
await goToNextStep();
};
const handleTimezoneSkip = () => {
goToNextStep();
const handleTimezoneSkip = async () => {
await goToNextStep();
};
const handlePluginsComplete = () => {
goToNextStep();
const handlePluginsComplete = async () => {
await goToNextStep();
};
const handlePluginsSkip = () => {
goToNextStep();
const handlePluginsSkip = async () => {
await goToNextStep();
};
const currentStepConfig = computed(() => {
@@ -172,6 +201,21 @@ const currentStepConfig = computed(() => {
}
return upgradeSteps.value[currentStepIndex.value];
});
watch(
() => upgradeSteps.value.length,
(length) => {
if (!isUpgradeMode.value) return;
if (length === 0) {
currentStepIndex.value = 0;
return;
}
if (currentStepIndex.value >= length) {
currentStepIndex.value = 0;
}
}
);
</script>
<template>

View File

@@ -0,0 +1,14 @@
import { graphql } from '~/composables/gql';
export const COMPLETE_UPGRADE_STEP_MUTATION = graphql(/* GraphQL */ `
mutation CompleteUpgradeStep($input: CompleteUpgradeStepInput!) {
onboarding {
completeUpgradeStep(input: $input) {
isUpgrade
previousVersion
currentVersion
completedSteps
}
}
}
`);

View File

@@ -11,11 +11,11 @@ import { UPGRADE_INFO_QUERY } from '~/components/Activation/upgradeInfo.query';
const UPGRADE_ONBOARDING_HIDDEN_KEY = 'upgrade-onboarding-hidden';
export const useUpgradeOnboardingStore = defineStore('upgradeOnboarding', () => {
const { result: upgradeInfoResult, loading: upgradeInfoLoading } = useQuery(
UPGRADE_INFO_QUERY,
{},
{ errorPolicy: 'all' }
);
const {
result: upgradeInfoResult,
loading: upgradeInfoLoading,
refetch,
} = useQuery(UPGRADE_INFO_QUERY, {}, { errorPolicy: 'all' });
const isHidden = useSessionStorage<boolean>(UPGRADE_ONBOARDING_HIDDEN_KEY, false);
@@ -26,21 +26,28 @@ export const useUpgradeOnboardingStore = defineStore('upgradeOnboarding', () =>
const currentVersion = computed(
() => upgradeInfoResult.value?.info?.versions?.upgrade?.currentVersion
);
const completedSteps = computed(
() => upgradeInfoResult.value?.info?.versions?.upgrade?.completedSteps ?? []
);
const upgradeSteps = ref<ReleaseStepConfig[]>([]);
const allUpgradeSteps = ref<ReleaseStepConfig[]>([]);
watch(
[isUpgrade, previousVersion, currentVersion],
async ([isUpgradeValue, prevVersion, currVersion]) => {
if (isUpgradeValue && prevVersion && currVersion) {
upgradeSteps.value = await getUpgradeSteps(prevVersion, currVersion);
allUpgradeSteps.value = await getUpgradeSteps(prevVersion, currVersion);
} else {
upgradeSteps.value = [];
allUpgradeSteps.value = [];
}
},
{ immediate: true }
);
const upgradeSteps = computed(() =>
allUpgradeSteps.value.filter((step) => !completedSteps.value.includes(step.id))
);
const shouldShowUpgradeOnboarding = computed(() => {
return !isHidden.value && isUpgrade.value && upgradeSteps.value.length > 0;
});
@@ -54,9 +61,11 @@ export const useUpgradeOnboardingStore = defineStore('upgradeOnboarding', () =>
isUpgrade,
previousVersion,
currentVersion,
completedSteps,
upgradeSteps,
shouldShowUpgradeOnboarding,
isHidden,
setIsHidden,
refetchUpgradeInfo: refetch,
};
});

View File

@@ -10,6 +10,7 @@ export const UPGRADE_INFO_QUERY = graphql(/* GraphQL */ `
isUpgrade
previousVersion
currentVersion
completedSteps
}
}
}

View File

@@ -14,11 +14,12 @@ import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-
* Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
*/
type Documents = {
"\n mutation CompleteUpgradeStep($input: CompleteUpgradeStepInput!) {\n onboarding {\n completeUpgradeStep(input: $input) {\n isUpgrade\n previousVersion\n currentVersion\n completedSteps\n }\n }\n }\n": typeof types.CompleteUpgradeStepDocument,
"\n query PartnerInfo {\n publicPartnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n": typeof types.PartnerInfoDocument,
"\n query PublicWelcomeData {\n publicPartnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n isInitialSetup\n }\n": typeof types.PublicWelcomeDataDocument,
"\n query ActivationCode {\n vars {\n regState\n }\n customization {\n activationCode {\n code\n partnerName\n serverName\n sysModel\n comment\n header\n headermetacolor\n background\n showBannerGradient\n theme\n }\n partnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n }\n": typeof types.ActivationCodeDocument,
"\n mutation UpdateSystemTime($input: UpdateSystemTimeInput!) {\n updateSystemTime(input: $input) {\n currentTime\n timeZone\n useNtp\n ntpServers\n }\n }\n": typeof types.UpdateSystemTimeDocument,
"\n query UpgradeInfo {\n info {\n id\n versions {\n id\n upgrade {\n isUpgrade\n previousVersion\n currentVersion\n }\n }\n }\n }\n": typeof types.UpgradeInfoDocument,
"\n query UpgradeInfo {\n info {\n id\n versions {\n id\n upgrade {\n isUpgrade\n previousVersion\n currentVersion\n completedSteps\n }\n }\n }\n }\n": typeof types.UpgradeInfoDocument,
"\n query GetApiKeyCreationFormSchema {\n getApiKeyCreationFormSchema {\n id\n dataSchema\n uiSchema\n values\n }\n }\n": typeof types.GetApiKeyCreationFormSchemaDocument,
"\n mutation CreateApiKey($input: CreateApiKeyInput!) {\n apiKey {\n create(input: $input) {\n ...ApiKey\n }\n }\n }\n": typeof types.CreateApiKeyDocument,
"\n mutation UpdateApiKey($input: UpdateApiKeyInput!) {\n apiKey {\n update(input: $input) {\n ...ApiKey\n }\n }\n }\n": typeof types.UpdateApiKeyDocument,
@@ -86,11 +87,12 @@ type Documents = {
"\n query getTheme {\n publicTheme {\n name\n showBannerImage\n showBannerGradient\n headerBackgroundColor\n showHeaderDescription\n headerPrimaryTextColor\n headerSecondaryTextColor\n }\n }\n": typeof types.GetThemeDocument,
};
const documents: Documents = {
"\n mutation CompleteUpgradeStep($input: CompleteUpgradeStepInput!) {\n onboarding {\n completeUpgradeStep(input: $input) {\n isUpgrade\n previousVersion\n currentVersion\n completedSteps\n }\n }\n }\n": types.CompleteUpgradeStepDocument,
"\n query PartnerInfo {\n publicPartnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n": types.PartnerInfoDocument,
"\n query PublicWelcomeData {\n publicPartnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n isInitialSetup\n }\n": types.PublicWelcomeDataDocument,
"\n query ActivationCode {\n vars {\n regState\n }\n customization {\n activationCode {\n code\n partnerName\n serverName\n sysModel\n comment\n header\n headermetacolor\n background\n showBannerGradient\n theme\n }\n partnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n }\n": types.ActivationCodeDocument,
"\n mutation UpdateSystemTime($input: UpdateSystemTimeInput!) {\n updateSystemTime(input: $input) {\n currentTime\n timeZone\n useNtp\n ntpServers\n }\n }\n": types.UpdateSystemTimeDocument,
"\n query UpgradeInfo {\n info {\n id\n versions {\n id\n upgrade {\n isUpgrade\n previousVersion\n currentVersion\n }\n }\n }\n }\n": types.UpgradeInfoDocument,
"\n query UpgradeInfo {\n info {\n id\n versions {\n id\n upgrade {\n isUpgrade\n previousVersion\n currentVersion\n completedSteps\n }\n }\n }\n }\n": types.UpgradeInfoDocument,
"\n query GetApiKeyCreationFormSchema {\n getApiKeyCreationFormSchema {\n id\n dataSchema\n uiSchema\n values\n }\n }\n": types.GetApiKeyCreationFormSchemaDocument,
"\n mutation CreateApiKey($input: CreateApiKeyInput!) {\n apiKey {\n create(input: $input) {\n ...ApiKey\n }\n }\n }\n": types.CreateApiKeyDocument,
"\n mutation UpdateApiKey($input: UpdateApiKeyInput!) {\n apiKey {\n update(input: $input) {\n ...ApiKey\n }\n }\n }\n": types.UpdateApiKeyDocument,
@@ -172,6 +174,10 @@ const documents: Documents = {
*/
export function graphql(source: string): unknown;
/**
* 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 CompleteUpgradeStep($input: CompleteUpgradeStepInput!) {\n onboarding {\n completeUpgradeStep(input: $input) {\n isUpgrade\n previousVersion\n currentVersion\n completedSteps\n }\n }\n }\n"): (typeof documents)["\n mutation CompleteUpgradeStep($input: CompleteUpgradeStepInput!) {\n onboarding {\n completeUpgradeStep(input: $input) {\n isUpgrade\n previousVersion\n currentVersion\n completedSteps\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -191,7 +197,7 @@ export function graphql(source: "\n mutation UpdateSystemTime($input: UpdateSys
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query UpgradeInfo {\n info {\n id\n versions {\n id\n upgrade {\n isUpgrade\n previousVersion\n currentVersion\n }\n }\n }\n }\n"): (typeof documents)["\n query UpgradeInfo {\n info {\n id\n versions {\n id\n upgrade {\n isUpgrade\n previousVersion\n currentVersion\n }\n }\n }\n }\n"];
export function graphql(source: "\n query UpgradeInfo {\n info {\n id\n versions {\n id\n upgrade {\n isUpgrade\n previousVersion\n currentVersion\n completedSteps\n }\n }\n }\n }\n"): (typeof documents)["\n query UpgradeInfo {\n info {\n id\n versions {\n id\n upgrade {\n isUpgrade\n previousVersion\n currentVersion\n completedSteps\n }\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

@@ -434,6 +434,12 @@ export type CloudResponse = {
status: Scalars['String']['output'];
};
/** Input for marking an upgrade onboarding step as completed */
export type CompleteUpgradeStepInput = {
/** Identifier of the onboarding step to mark completed */
stepId: Scalars['String']['input'];
};
export type Config = Node & {
__typename?: 'Config';
error?: Maybe<Scalars['String']['output']>;
@@ -1464,6 +1470,7 @@ export type Mutation = {
moveDockerItemsToPosition: ResolvedOrganizerV1;
/** Creates a notification if an equivalent unread notification does not already exist. */
notifyIfUnique?: Maybe<Notification>;
onboarding: OnboardingMutations;
parityCheck: ParityCheckMutations;
rclone: RCloneMutations;
/** Reads each notification to recompute & update the overview. */
@@ -1774,6 +1781,19 @@ export type OidcSessionValidation = {
valid: Scalars['Boolean']['output'];
};
/** Onboarding related mutations */
export type OnboardingMutations = {
__typename?: 'OnboardingMutations';
/** Mark an upgrade onboarding step as completed for the current OS version */
completeUpgradeStep: UpgradeInfo;
};
/** Onboarding related mutations */
export type OnboardingMutationsCompleteUpgradeStepArgs = {
input: CompleteUpgradeStepInput;
};
export type Owner = {
__typename?: 'Owner';
avatar: Scalars['String']['output'];
@@ -2614,6 +2634,8 @@ export type UpdateSystemTimeInput = {
export type UpgradeInfo = {
__typename?: 'UpgradeInfo';
/** Onboarding step identifiers completed for the current OS version */
completedSteps: Array<Scalars['String']['output']>;
/** Current OS version */
currentVersion?: Maybe<Scalars['String']['output']>;
/** Whether the OS version has changed since last boot */
@@ -2913,6 +2935,13 @@ export enum RegistrationType {
UNLEASHED = 'UNLEASHED'
}
export type CompleteUpgradeStepMutationVariables = Exact<{
input: CompleteUpgradeStepInput;
}>;
export type CompleteUpgradeStepMutation = { __typename?: 'Mutation', onboarding: { __typename?: 'OnboardingMutations', completeUpgradeStep: { __typename?: 'UpgradeInfo', isUpgrade: boolean, previousVersion?: string | null, currentVersion?: string | null, completedSteps: Array<string> } } };
export type PartnerInfoQueryVariables = Exact<{ [key: string]: never; }>;
@@ -2938,7 +2967,7 @@ export type UpdateSystemTimeMutation = { __typename?: 'Mutation', updateSystemTi
export type UpgradeInfoQueryVariables = Exact<{ [key: string]: never; }>;
export type UpgradeInfoQuery = { __typename?: 'Query', info: { __typename?: 'Info', id: string, versions: { __typename?: 'InfoVersions', id: string, upgrade: { __typename?: 'UpgradeInfo', isUpgrade: boolean, previousVersion?: string | null, currentVersion?: string | null } } } };
export type UpgradeInfoQuery = { __typename?: 'Query', info: { __typename?: 'Info', id: string, versions: { __typename?: 'InfoVersions', id: string, upgrade: { __typename?: 'UpgradeInfo', isUpgrade: boolean, previousVersion?: string | null, currentVersion?: string | null, completedSteps: Array<string> } } } };
export type GetApiKeyCreationFormSchemaQueryVariables = Exact<{ [key: string]: never; }>;
@@ -3385,11 +3414,12 @@ export const ApiKeyFragmentDoc = {"kind":"Document","definitions":[{"kind":"Frag
export const NotificationFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Notification"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"importance"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"formattedTimestamp"}}]}}]} as unknown as DocumentNode<NotificationFragmentFragment, unknown>;
export const NotificationCountFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationCountFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationCounts"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"info"}},{"kind":"Field","name":{"kind":"Name","value":"warning"}},{"kind":"Field","name":{"kind":"Name","value":"alert"}}]}}]} as unknown as DocumentNode<NotificationCountFragmentFragment, unknown>;
export const PartialCloudFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PartialCloud"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Cloud"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"minigraphql"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"relay"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode<PartialCloudFragment, unknown>;
export const CompleteUpgradeStepDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CompleteUpgradeStep"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CompleteUpgradeStepInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"onboarding"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"completeUpgradeStep"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"isUpgrade"}},{"kind":"Field","name":{"kind":"Name","value":"previousVersion"}},{"kind":"Field","name":{"kind":"Name","value":"currentVersion"}},{"kind":"Field","name":{"kind":"Name","value":"completedSteps"}}]}}]}}]}}]} as unknown as DocumentNode<CompleteUpgradeStepMutation, CompleteUpgradeStepMutationVariables>;
export const PartnerInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PartnerInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"publicPartnerInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasPartnerLogo"}},{"kind":"Field","name":{"kind":"Name","value":"partnerName"}},{"kind":"Field","name":{"kind":"Name","value":"partnerUrl"}},{"kind":"Field","name":{"kind":"Name","value":"partnerLogoUrl"}}]}}]}}]} as unknown as DocumentNode<PartnerInfoQuery, PartnerInfoQueryVariables>;
export const PublicWelcomeDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PublicWelcomeData"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"publicPartnerInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasPartnerLogo"}},{"kind":"Field","name":{"kind":"Name","value":"partnerName"}},{"kind":"Field","name":{"kind":"Name","value":"partnerUrl"}},{"kind":"Field","name":{"kind":"Name","value":"partnerLogoUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isInitialSetup"}}]}}]} as unknown as DocumentNode<PublicWelcomeDataQuery, PublicWelcomeDataQueryVariables>;
export const ActivationCodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ActivationCode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"regState"}}]}},{"kind":"Field","name":{"kind":"Name","value":"customization"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activationCode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"partnerName"}},{"kind":"Field","name":{"kind":"Name","value":"serverName"}},{"kind":"Field","name":{"kind":"Name","value":"sysModel"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"header"}},{"kind":"Field","name":{"kind":"Name","value":"headermetacolor"}},{"kind":"Field","name":{"kind":"Name","value":"background"}},{"kind":"Field","name":{"kind":"Name","value":"showBannerGradient"}},{"kind":"Field","name":{"kind":"Name","value":"theme"}}]}},{"kind":"Field","name":{"kind":"Name","value":"partnerInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasPartnerLogo"}},{"kind":"Field","name":{"kind":"Name","value":"partnerName"}},{"kind":"Field","name":{"kind":"Name","value":"partnerUrl"}},{"kind":"Field","name":{"kind":"Name","value":"partnerLogoUrl"}}]}}]}}]}}]} as unknown as DocumentNode<ActivationCodeQuery, ActivationCodeQueryVariables>;
export const UpdateSystemTimeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSystemTime"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateSystemTimeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateSystemTime"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currentTime"}},{"kind":"Field","name":{"kind":"Name","value":"timeZone"}},{"kind":"Field","name":{"kind":"Name","value":"useNtp"}},{"kind":"Field","name":{"kind":"Name","value":"ntpServers"}}]}}]}}]} as unknown as DocumentNode<UpdateSystemTimeMutation, UpdateSystemTimeMutationVariables>;
export const UpgradeInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UpgradeInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"upgrade"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"isUpgrade"}},{"kind":"Field","name":{"kind":"Name","value":"previousVersion"}},{"kind":"Field","name":{"kind":"Name","value":"currentVersion"}}]}}]}}]}}]}}]} as unknown as DocumentNode<UpgradeInfoQuery, UpgradeInfoQueryVariables>;
export const UpgradeInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UpgradeInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"upgrade"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"isUpgrade"}},{"kind":"Field","name":{"kind":"Name","value":"previousVersion"}},{"kind":"Field","name":{"kind":"Name","value":"currentVersion"}},{"kind":"Field","name":{"kind":"Name","value":"completedSteps"}}]}}]}}]}}]}}]} as unknown as DocumentNode<UpgradeInfoQuery, UpgradeInfoQueryVariables>;
export const GetApiKeyCreationFormSchemaDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetApiKeyCreationFormSchema"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getApiKeyCreationFormSchema"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSchema"}},{"kind":"Field","name":{"kind":"Name","value":"uiSchema"}},{"kind":"Field","name":{"kind":"Name","value":"values"}}]}}]}}]} as unknown as DocumentNode<GetApiKeyCreationFormSchemaQuery, GetApiKeyCreationFormSchemaQueryVariables>;
export const CreateApiKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateApiKey"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateApiKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ApiKey"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApiKey"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApiKey"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"roles"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resource"}},{"kind":"Field","name":{"kind":"Name","value":"actions"}}]}}]}}]} as unknown as DocumentNode<CreateApiKeyMutation, CreateApiKeyMutationVariables>;
export const UpdateApiKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateApiKey"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateApiKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"update"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ApiKey"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApiKey"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApiKey"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"roles"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resource"}},{"kind":"Field","name":{"kind":"Name","value":"actions"}}]}}]}}]} as unknown as DocumentNode<UpdateApiKeyMutation, UpdateApiKeyMutationVariables>;