feat(unraid-plugins): add query for installed Unraid OS plugins and enhance plugin management

- Introduced a new GraphQL query `installedUnraidPlugins` to list installed Unraid OS plugins by their .plg filenames.
- Updated the `UnraidPluginsResolver` to include the new query and implemented the corresponding service method to read plugin files from the filesystem.
- Enhanced the `ActivationPluginsStep` component to utilize the new query, improving the user experience by dynamically displaying installed plugins.
- Added a new GraphQL query file for `INSTALLED_UNRAID_PLUGINS_QUERY` to facilitate fetching installed plugins in the frontend.

These changes enhance the plugin management capabilities, providing users with better visibility and control over installed plugins.
This commit is contained in:
Eli Bosley
2025-12-30 10:08:05 -05:00
parent a963f41ce9
commit 92bc43295c
13 changed files with 209 additions and 23 deletions

View File

@@ -2909,6 +2909,9 @@ type Query {
"""List all tracked plugin installation operations"""
pluginInstallOperations: [PluginInstallOperation!]!
"""List installed Unraid OS plugins by .plg filename"""
installedUnraidPlugins: [String!]!
"""List all installed plugins with their metadata"""
plugins: [Plugin!]!
remoteAccess: RemoteAccess!

View File

@@ -38,6 +38,17 @@ export class UnraidPluginsResolver {
return this.pluginsService.listOperations();
}
@Query(() => [String], {
description: 'List installed Unraid OS plugins by .plg filename',
})
@UsePermissions({
action: AuthAction.READ_ANY,
resource: Resource.CONFIG,
})
async installedUnraidPlugins(): Promise<string[]> {
return this.pluginsService.listInstalledPlugins();
}
@Subscription(() => PluginInstallEvent, {
name: 'pluginInstallUpdates',
resolve: (payload: { pluginInstallUpdates: PluginInstallEvent }) => payload.pluginInstallUpdates,

View File

@@ -1,3 +1,4 @@
import { ConfigService } from '@nestjs/config';
import EventEmitter from 'node:events';
import { PassThrough } from 'node:stream';
@@ -24,7 +25,7 @@ describe('UnraidPluginsService', () => {
let currentProcess: MockExecaProcess;
beforeEach(() => {
service = new UnraidPluginsService();
service = new UnraidPluginsService(new ConfigService());
currentProcess = new MockExecaProcess();
currentProcess.all.setEncoding('utf-8');
mockExeca.mockReset();

View File

@@ -1,5 +1,8 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { randomUUID } from 'node:crypto';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import type { ExecaError } from 'execa';
import { execa } from 'execa';
@@ -40,6 +43,8 @@ export class UnraidPluginsService {
private readonly operations = new Map<string, OperationState>();
private readonly MAX_OUTPUT_LINES = 500;
constructor(private readonly configService: ConfigService) {}
async installPlugin(input: InstallPluginInput): Promise<PluginInstallOperation> {
const id = randomUUID();
const createdAt = new Date();
@@ -103,6 +108,27 @@ export class UnraidPluginsService {
return this.toGraphqlOperation(operation);
}
async listInstalledPlugins(): Promise<string[]> {
const paths = this.configService.get<Record<string, string>>('store.paths', {});
const dynamixBase = paths?.['dynamix-base'] ?? '/boot/config/plugins/dynamix';
const pluginsDir = path.resolve(dynamixBase, '..');
try {
const entries = await fs.readdir(pluginsDir, { withFileTypes: true });
return entries
.filter((entry) => entry.isFile() && entry.name.endsWith('.plg'))
.map((entry) => entry.name);
} catch (error: unknown) {
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
this.logger.warn(`Plugin directory not found at ${pluginsDir}.`);
return [];
}
this.logger.error('Failed to read plugin directory.', error);
return [];
}
}
getOperation(id: string): PluginInstallOperation | null {
const operation = this.operations.get(id);
if (!operation) {

View File

@@ -1,3 +1,4 @@
import { ref } from 'vue';
import { flushPromises, mount } from '@vue/test-utils';
import { beforeEach, describe, expect, it, vi } from 'vitest';
@@ -7,6 +8,7 @@ import { PluginInstallStatus } from '~/composables/gql/graphql';
import { createTestI18n } from '../../utils/i18n';
const installPluginMock = vi.fn();
const useQueryMock = vi.fn();
vi.mock('@unraid/ui', () => ({
BrandButton: {
@@ -22,9 +24,23 @@ vi.mock('~/components/Activation/usePluginInstaller', () => ({
}),
}));
vi.mock('@vue/apollo-composable', async () => {
const actual =
await vi.importActual<typeof import('@vue/apollo-composable')>('@vue/apollo-composable');
return {
...actual,
useQuery: useQueryMock,
};
});
describe('ActivationPluginsStep', () => {
beforeEach(() => {
installPluginMock.mockReset();
useQueryMock.mockReturnValue({
result: ref({ installedUnraidPlugins: [] }),
loading: ref(false),
error: ref(null),
});
});
const mountComponent = (overrides: Record<string, unknown> = {}) => {

View File

@@ -18,6 +18,7 @@ defineProps<{
allowSkip?: boolean;
showKeyfileHint?: boolean;
showActivationCodeHint?: boolean;
isSavingStep?: boolean;
}>();
const { t } = useI18n();
@@ -33,12 +34,20 @@ const { t } = useI18n();
<div class="flex flex-col">
<div class="mx-auto mb-10 flex gap-4">
<BrandButton v-if="canGoBack" :text="t('common.back')" variant="outline" @click="onBack?.()" />
<BrandButton
v-if="canGoBack"
:text="t('common.back')"
variant="outline"
:disabled="isSavingStep"
@click="onBack?.()"
/>
<BrandButton
:text="t('activation.activationModal.activateNow')"
:icon-right="ArrowTopRightOnSquareIcon"
:href="activateHref"
:external="activateExternal"
:disabled="isSavingStep"
:loading="isSavingStep"
/>
</div>
@@ -63,9 +72,15 @@ const { t } = useI18n();
v-if="allowSkip"
:text="t('activation.skipForNow')"
variant="underline"
:disabled="isSavingStep"
@click="onComplete?.()"
/>
<BrandButton v-for="button in docsButtons" :key="button.text" v-bind="button" />
<BrandButton
v-for="button in docsButtons"
:key="button.text"
v-bind="button"
:disabled="isSavingStep"
/>
</div>
</div>
</div>

View File

@@ -275,6 +275,7 @@ const currentStepConfig = computed(() => {
});
const isCurrentStepSaved = computed(() => currentStepConfig.value?.completed ?? false);
const isStepSaving = computed(() => stepSaveState.value === 'saving');
const currentStepProps = computed<Record<string, unknown>>(() => {
const step = currentStep.value;
@@ -291,6 +292,7 @@ const currentStepProps = computed<Record<string, unknown>>(() => {
onBack: goToPreviousStep,
showBack: canGoBack.value,
isCompleted: isCurrentStepCompleted,
isSavingStep: isStepSaving.value,
};
switch (step) {
@@ -409,7 +411,7 @@ watch(
<ActivationSteps
:steps="allUpgradeSteps"
:active-step-index="currentDynamicStepIndex"
:on-step-click="goToStep"
:on-step-click="isStepSaving ? undefined : goToStep"
class="mt-6"
/>
</div>

View File

@@ -1,9 +1,11 @@
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue';
import { computed, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useQuery } from '@vue/apollo-composable';
import { BrandButton } from '@unraid/ui';
import { INSTALLED_UNRAID_PLUGINS_QUERY } from '~/components/Activation/graphql/installedPlugins.query';
import usePluginInstaller from '~/components/Activation/usePluginInstaller';
import { PluginInstallStatus } from '~/composables/gql/graphql';
@@ -14,6 +16,7 @@ export interface Props {
showSkip?: boolean;
showBack?: boolean;
isRequired?: boolean;
isSavingStep?: boolean;
}
const props = defineProps<Props>();
@@ -26,6 +29,13 @@ interface Plugin {
url: string;
}
const normalizePluginFileName = (value: string) => value.trim().toLowerCase();
const getPluginFileName = (url: string) => {
const parts = url.split('/');
return parts[parts.length - 1] ?? url;
};
const availablePlugins: Plugin[] = [
{
id: 'community-apps',
@@ -67,14 +77,71 @@ const pluginStates = reactive<Record<string, PluginState>>(
);
const selectedPlugins = ref<Set<string>>(new Set());
const installedPluginIds = ref<Set<string>>(new Set());
const isInstalling = ref(false);
const error = ref<string | null>(null);
const installationFinished = ref(false);
const { result: installedPluginsResult } = useQuery(INSTALLED_UNRAID_PLUGINS_QUERY, null, {
fetchPolicy: 'network-only',
});
const { installPlugin } = usePluginInstaller();
const INSTALL_TIMEOUT_MS = 60000;
const installablePlugins = computed(() =>
availablePlugins.filter(
(plugin) => selectedPlugins.value.has(plugin.id) && !installedPluginIds.value.has(plugin.id)
)
);
const hasInstallableSelection = computed(() => installablePlugins.value.length > 0);
const isPluginInstalled = (pluginId: string) => installedPluginIds.value.has(pluginId);
const isBusy = computed(() => isInstalling.value || (props.isSavingStep ?? false));
const applyInstalledPlugins = (installedPlugins: string[] | null | undefined) => {
if (!Array.isArray(installedPlugins)) {
return;
}
const installedFiles = new Set(installedPlugins.map((name) => normalizePluginFileName(name)));
const nextInstalledIds = new Set<string>();
for (const plugin of availablePlugins) {
const fileName = normalizePluginFileName(getPluginFileName(plugin.url));
if (installedFiles.has(fileName)) {
nextInstalledIds.add(plugin.id);
}
}
installedPluginIds.value = nextInstalledIds;
if (nextInstalledIds.size > 0) {
const nextSelected = new Set(selectedPlugins.value);
for (const id of nextInstalledIds) {
nextSelected.add(id);
}
selectedPlugins.value = nextSelected;
}
for (const id of nextInstalledIds) {
const state = pluginStates[id];
if (state && state.status !== 'installing') {
state.status = 'success';
}
}
};
watch(
() => installedPluginsResult.value?.installedUnraidPlugins,
(installedPlugins) => {
applyInstalledPlugins(installedPlugins);
},
{ immediate: true }
);
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => {
@@ -113,6 +180,10 @@ const resetCompletionState = () => {
};
const togglePlugin = (pluginId: string) => {
if (installedPluginIds.value.has(pluginId) || isBusy.value) {
return;
}
const next = new Set(selectedPlugins.value);
if (next.has(pluginId)) {
next.delete(pluginId);
@@ -130,7 +201,8 @@ const togglePlugin = (pluginId: string) => {
};
const handleInstall = async () => {
if (selectedPlugins.value.size === 0) {
const pluginsToInstall = installablePlugins.value;
if (pluginsToInstall.length === 0) {
installationFinished.value = true;
return;
}
@@ -141,8 +213,6 @@ const handleInstall = async () => {
let hadError = false;
try {
const pluginsToInstall = availablePlugins.filter((p) => selectedPlugins.value.has(p.id));
for (const plugin of pluginsToInstall) {
const state = pluginStates[plugin.id];
state.status = 'installing';
@@ -190,6 +260,9 @@ const handleInstall = async () => {
t('activation.pluginsStep.pluginInstalledMessage', { name: plugin.name })
);
state.status = 'success';
const nextInstalled = new Set(installedPluginIds.value);
nextInstalled.add(plugin.id);
installedPluginIds.value = nextInstalled;
}
installationFinished.value = pluginsToInstall.every((plugin) =>
@@ -216,12 +289,12 @@ const handleBack = () => {
};
const handlePrimaryAction = async () => {
if (installationFinished.value || selectedPlugins.value.size === 0) {
if (installationFinished.value || !hasInstallableSelection.value) {
props.onComplete();
return;
}
if (!isInstalling.value) {
if (!isBusy.value) {
await handleInstall();
}
};
@@ -230,14 +303,14 @@ const primaryButtonText = computed(() => {
if (installationFinished.value) {
return t('common.continue');
}
if (selectedPlugins.value.size > 0) {
if (hasInstallableSelection.value) {
return t('activation.pluginsStep.installSelected');
}
return t('common.continue');
});
const isPrimaryActionDisabled = computed(() => {
if (isInstalling.value) {
if (isBusy.value) {
return true;
}
@@ -279,7 +352,7 @@ const isPrimaryActionDisabled = computed(() => {
:id="plugin.id"
type="checkbox"
:checked="selectedPlugins.has(plugin.id)"
:disabled="isInstalling"
:disabled="isBusy || isPluginInstalled(plugin.id)"
@change="() => togglePlugin(plugin.id)"
class="text-primary focus:ring-primary h-5 w-5 cursor-pointer rounded border-gray-300 focus:ring-2"
/>
@@ -329,7 +402,7 @@ const isPrimaryActionDisabled = computed(() => {
v-if="onBack && showBack"
:text="t('common.back')"
variant="outline"
:disabled="isInstalling"
:disabled="isBusy"
@click="handleBack"
/>
<div class="flex-1" />
@@ -337,13 +410,13 @@ const isPrimaryActionDisabled = computed(() => {
v-if="onSkip && showSkip"
:text="t('common.skip')"
variant="outline"
:disabled="isInstalling"
:disabled="isBusy"
@click="handleSkip"
/>
<BrandButton
:text="primaryButtonText"
:disabled="isPrimaryActionDisabled"
:loading="isInstalling"
:loading="isBusy"
@click="handlePrimaryAction"
/>
</div>

View File

@@ -14,6 +14,7 @@ export interface Props {
onBack?: () => void;
showSkip?: boolean;
showBack?: boolean;
isSavingStep?: boolean;
}
const props = defineProps<Props>();
@@ -135,6 +136,8 @@ const handleSkip = () => {
const handleBack = () => {
props.onBack?.();
};
const isBusy = computed(() => isSaving.value || (props.isSavingStep ?? false));
</script>
<template>
@@ -162,7 +165,7 @@ const handleBack = () => {
v-if="onBack && showBack"
:text="t('common.back')"
variant="outline"
:disabled="isSaving"
:disabled="isBusy"
@click="handleBack"
/>
<div class="flex-1" />
@@ -170,13 +173,13 @@ const handleBack = () => {
v-if="onSkip && showSkip"
:text="t('common.skip')"
variant="outline"
:disabled="isSaving"
:disabled="isBusy"
@click="handleSkip"
/>
<BrandButton
:text="t('common.continue')"
:disabled="!selectedTimeZone || isSaving"
:loading="isSaving"
:disabled="!selectedTimeZone || isBusy"
:loading="isBusy"
@click="handleSubmit"
/>
</div>

View File

@@ -18,6 +18,7 @@ export interface Props {
showBack?: boolean;
// For redirecting to login page after welcome
redirectToLogin?: boolean;
isSavingStep?: boolean;
}
const props = defineProps<Props>();
@@ -59,6 +60,8 @@ const buttonText = computed<string>(() => {
return t('activation.welcomeModal.getStarted');
});
const isBusy = computed(() => props.isSavingStep ?? false);
const handleComplete = () => {
if (props.redirectToLogin) {
// Redirect to login page for password creation
@@ -78,11 +81,23 @@ const handleComplete = () => {
</div>
<div class="flex space-x-4">
<BrandButton v-if="showBack" :text="t('common.back')" variant="outline" @click="onBack" />
<BrandButton
v-if="showBack"
:text="t('common.back')"
variant="outline"
:disabled="isBusy"
@click="onBack"
/>
<BrandButton v-if="showSkip" :text="t('common.skip')" variant="outline" @click="onSkip" />
<BrandButton
v-if="showSkip"
:text="t('common.skip')"
variant="outline"
:disabled="isBusy"
@click="onSkip"
/>
<BrandButton :text="buttonText" @click="handleComplete" />
<BrandButton :text="buttonText" :disabled="isBusy" :loading="isBusy" @click="handleComplete" />
</div>
</div>
</template>

View File

@@ -0,0 +1,7 @@
import { graphql } from '@/composables/gql/gql';
export const INSTALLED_UNRAID_PLUGINS_QUERY = graphql(/* GraphQL */ `
query InstalledUnraidPlugins {
installedUnraidPlugins
}
`);

View File

@@ -20,6 +20,7 @@ type Documents = {
"\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 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 onboardingState {\n registrationState\n isRegistered\n isFreshInstall\n isInitialSetup\n hasActivationCode\n activationRequired\n }\n }\n }\n": typeof types.ActivationCodeDocument,
"\n mutation InstallPlugin($input: InstallPluginInput!) {\n unraidPlugins {\n installPlugin(input: $input) {\n id\n url\n name\n status\n createdAt\n updatedAt\n finishedAt\n output\n }\n }\n }\n": typeof types.InstallPluginDocument,
"\n query InstalledUnraidPlugins {\n installedUnraidPlugins\n }\n": typeof types.InstalledUnraidPluginsDocument,
"\n query PluginInstallOperation($operationId: ID!) {\n pluginInstallOperation(operationId: $operationId) {\n id\n url\n name\n status\n createdAt\n updatedAt\n finishedAt\n output\n }\n }\n": typeof types.PluginInstallOperationDocument,
"\n subscription PluginInstallUpdates($operationId: ID!) {\n pluginInstallUpdates(operationId: $operationId) {\n operationId\n status\n output\n timestamp\n }\n }\n": typeof types.PluginInstallUpdatesDocument,
"\n query TimeZoneOptions {\n timeZoneOptions {\n value\n label\n }\n }\n": typeof types.TimeZoneOptionsDocument,
@@ -97,6 +98,7 @@ const documents: Documents = {
"\n query PublicWelcomeData {\n publicPartnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n isInitialSetup\n }\n": types.PublicWelcomeDataDocument,
"\n query ActivationCode {\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 onboardingState {\n registrationState\n isRegistered\n isFreshInstall\n isInitialSetup\n hasActivationCode\n activationRequired\n }\n }\n }\n": types.ActivationCodeDocument,
"\n mutation InstallPlugin($input: InstallPluginInput!) {\n unraidPlugins {\n installPlugin(input: $input) {\n id\n url\n name\n status\n createdAt\n updatedAt\n finishedAt\n output\n }\n }\n }\n": types.InstallPluginDocument,
"\n query InstalledUnraidPlugins {\n installedUnraidPlugins\n }\n": types.InstalledUnraidPluginsDocument,
"\n query PluginInstallOperation($operationId: ID!) {\n pluginInstallOperation(operationId: $operationId) {\n id\n url\n name\n status\n createdAt\n updatedAt\n finishedAt\n output\n }\n }\n": types.PluginInstallOperationDocument,
"\n subscription PluginInstallUpdates($operationId: ID!) {\n pluginInstallUpdates(operationId: $operationId) {\n operationId\n status\n output\n timestamp\n }\n }\n": types.PluginInstallUpdatesDocument,
"\n query TimeZoneOptions {\n timeZoneOptions {\n value\n label\n }\n }\n": types.TimeZoneOptionsDocument,
@@ -206,6 +208,10 @@ export function graphql(source: "\n query ActivationCode {\n customization {
* 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 InstallPlugin($input: InstallPluginInput!) {\n unraidPlugins {\n installPlugin(input: $input) {\n id\n url\n name\n status\n createdAt\n updatedAt\n finishedAt\n output\n }\n }\n }\n"): (typeof documents)["\n mutation InstallPlugin($input: InstallPluginInput!) {\n unraidPlugins {\n installPlugin(input: $input) {\n id\n url\n name\n status\n createdAt\n updatedAt\n finishedAt\n output\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 query InstalledUnraidPlugins {\n installedUnraidPlugins\n }\n"): (typeof documents)["\n query InstalledUnraidPlugins {\n installedUnraidPlugins\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

View File

@@ -2116,6 +2116,8 @@ export type Query = {
/** Get the actual permissions that would be granted by a set of roles */
getPermissionsForRoles: Array<Permission>;
info: Info;
/** List installed Unraid OS plugins by .plg filename */
installedUnraidPlugins: Array<Scalars['String']['output']>;
isInitialSetup: Scalars['Boolean']['output'];
isSSOEnabled: Scalars['Boolean']['output'];
logFile: LogFileContent;
@@ -3185,6 +3187,11 @@ export type InstallPluginMutationVariables = Exact<{
export type InstallPluginMutation = { __typename?: 'Mutation', unraidPlugins: { __typename?: 'UnraidPluginsMutations', installPlugin: { __typename?: 'PluginInstallOperation', id: string, url: string, name?: string | null, status: PluginInstallStatus, createdAt: string, updatedAt?: string | null, finishedAt?: string | null, output: Array<string> } } };
export type InstalledUnraidPluginsQueryVariables = Exact<{ [key: string]: never; }>;
export type InstalledUnraidPluginsQuery = { __typename?: 'Query', installedUnraidPlugins: Array<string> };
export type PluginInstallOperationQueryVariables = Exact<{
operationId: Scalars['ID']['input'];
}>;
@@ -3662,6 +3669,7 @@ export const PartnerInfoDocument = {"kind":"Document","definitions":[{"kind":"Op
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":"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"}}]}},{"kind":"Field","name":{"kind":"Name","value":"onboardingState"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"registrationState"}},{"kind":"Field","name":{"kind":"Name","value":"isRegistered"}},{"kind":"Field","name":{"kind":"Name","value":"isFreshInstall"}},{"kind":"Field","name":{"kind":"Name","value":"isInitialSetup"}},{"kind":"Field","name":{"kind":"Name","value":"hasActivationCode"}},{"kind":"Field","name":{"kind":"Name","value":"activationRequired"}}]}}]}}]}}]} as unknown as DocumentNode<ActivationCodeQuery, ActivationCodeQueryVariables>;
export const InstallPluginDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InstallPlugin"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"InstallPluginInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unraidPlugins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"installPlugin"},"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":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"finishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"output"}}]}}]}}]}}]} as unknown as DocumentNode<InstallPluginMutation, InstallPluginMutationVariables>;
export const InstalledUnraidPluginsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"InstalledUnraidPlugins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"installedUnraidPlugins"}}]}}]} as unknown as DocumentNode<InstalledUnraidPluginsQuery, InstalledUnraidPluginsQueryVariables>;
export const PluginInstallOperationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PluginInstallOperation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"operationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pluginInstallOperation"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"operationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"operationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"finishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"output"}}]}}]}}]} as unknown as DocumentNode<PluginInstallOperationQuery, PluginInstallOperationQueryVariables>;
export const PluginInstallUpdatesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"PluginInstallUpdates"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"operationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pluginInstallUpdates"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"operationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"operationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"operationId"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"output"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}}]}}]}}]} as unknown as DocumentNode<PluginInstallUpdatesSubscription, PluginInstallUpdatesSubscriptionVariables>;
export const TimeZoneOptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"TimeZoneOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timeZoneOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"label"}}]}}]}}]} as unknown as DocumentNode<TimeZoneOptionsQuery, TimeZoneOptionsQueryVariables>;