From 92bc43295c27651edd3e7ba82f9155ca6d714593 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 30 Dec 2025 10:08:05 -0500 Subject: [PATCH] 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. --- api/generated-schema.graphql | 3 + .../unraid-plugins/unraid-plugins.resolver.ts | 11 +++ .../unraid-plugins.service.spec.ts | 3 +- .../unraid-plugins/unraid-plugins.service.ts | 26 +++++ .../Activation/ActivationPluginsStep.test.ts | 16 +++ .../Activation/ActivationLicenseStep.vue | 19 +++- .../components/Activation/ActivationModal.vue | 4 +- .../Activation/ActivationPluginsStep.vue | 97 ++++++++++++++++--- .../Activation/ActivationTimezoneStep.vue | 11 ++- .../Activation/ActivationWelcomeStep.vue | 21 +++- .../graphql/installedPlugins.query.ts | 7 ++ web/src/composables/gql/gql.ts | 6 ++ web/src/composables/gql/graphql.ts | 8 ++ 13 files changed, 209 insertions(+), 23 deletions(-) create mode 100644 web/src/components/Activation/graphql/installedPlugins.query.ts diff --git a/api/generated-schema.graphql b/api/generated-schema.graphql index 109e36eca..34d928878 100644 --- a/api/generated-schema.graphql +++ b/api/generated-schema.graphql @@ -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! diff --git a/api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.resolver.ts b/api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.resolver.ts index c94de5cc5..610002915 100644 --- a/api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.resolver.ts +++ b/api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.resolver.ts @@ -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 { + return this.pluginsService.listInstalledPlugins(); + } + @Subscription(() => PluginInstallEvent, { name: 'pluginInstallUpdates', resolve: (payload: { pluginInstallUpdates: PluginInstallEvent }) => payload.pluginInstallUpdates, diff --git a/api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.spec.ts b/api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.spec.ts index e0c2c8150..cd7c048a2 100644 --- a/api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.spec.ts +++ b/api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.spec.ts @@ -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(); diff --git a/api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.ts b/api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.ts index dc9896652..222444f44 100644 --- a/api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.ts +++ b/api/src/unraid-api/graph/resolvers/unraid-plugins/unraid-plugins.service.ts @@ -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(); private readonly MAX_OUTPUT_LINES = 500; + constructor(private readonly configService: ConfigService) {} + async installPlugin(input: InstallPluginInput): Promise { const id = randomUUID(); const createdAt = new Date(); @@ -103,6 +108,27 @@ export class UnraidPluginsService { return this.toGraphqlOperation(operation); } + async listInstalledPlugins(): Promise { + const paths = this.configService.get>('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) { diff --git a/web/__test__/components/Activation/ActivationPluginsStep.test.ts b/web/__test__/components/Activation/ActivationPluginsStep.test.ts index b020b0792..d310f457b 100644 --- a/web/__test__/components/Activation/ActivationPluginsStep.test.ts +++ b/web/__test__/components/Activation/ActivationPluginsStep.test.ts @@ -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('@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 = {}) => { diff --git a/web/src/components/Activation/ActivationLicenseStep.vue b/web/src/components/Activation/ActivationLicenseStep.vue index 22527bb11..752fb9ec6 100644 --- a/web/src/components/Activation/ActivationLicenseStep.vue +++ b/web/src/components/Activation/ActivationLicenseStep.vue @@ -18,6 +18,7 @@ defineProps<{ allowSkip?: boolean; showKeyfileHint?: boolean; showActivationCodeHint?: boolean; + isSavingStep?: boolean; }>(); const { t } = useI18n(); @@ -33,12 +34,20 @@ const { t } = useI18n();
- +
@@ -63,9 +72,15 @@ const { t } = useI18n(); v-if="allowSkip" :text="t('activation.skipForNow')" variant="underline" + :disabled="isSavingStep" @click="onComplete?.()" /> - +
diff --git a/web/src/components/Activation/ActivationModal.vue b/web/src/components/Activation/ActivationModal.vue index 484697dd6..cdf557ca6 100644 --- a/web/src/components/Activation/ActivationModal.vue +++ b/web/src/components/Activation/ActivationModal.vue @@ -275,6 +275,7 @@ const currentStepConfig = computed(() => { }); const isCurrentStepSaved = computed(() => currentStepConfig.value?.completed ?? false); +const isStepSaving = computed(() => stepSaveState.value === 'saving'); const currentStepProps = computed>(() => { const step = currentStep.value; @@ -291,6 +292,7 @@ const currentStepProps = computed>(() => { onBack: goToPreviousStep, showBack: canGoBack.value, isCompleted: isCurrentStepCompleted, + isSavingStep: isStepSaving.value, }; switch (step) { @@ -409,7 +411,7 @@ watch( diff --git a/web/src/components/Activation/ActivationPluginsStep.vue b/web/src/components/Activation/ActivationPluginsStep.vue index 26f99424b..80b9e3178 100644 --- a/web/src/components/Activation/ActivationPluginsStep.vue +++ b/web/src/components/Activation/ActivationPluginsStep.vue @@ -1,9 +1,11 @@ diff --git a/web/src/components/Activation/graphql/installedPlugins.query.ts b/web/src/components/Activation/graphql/installedPlugins.query.ts new file mode 100644 index 000000000..6a1833166 --- /dev/null +++ b/web/src/components/Activation/graphql/installedPlugins.query.ts @@ -0,0 +1,7 @@ +import { graphql } from '@/composables/gql/gql'; + +export const INSTALLED_UNRAID_PLUGINS_QUERY = graphql(/* GraphQL */ ` + query InstalledUnraidPlugins { + installedUnraidPlugins + } +`); diff --git a/web/src/composables/gql/gql.ts b/web/src/composables/gql/gql.ts index ad9d745b9..0a4a3cbf8 100644 --- a/web/src/composables/gql/gql.ts +++ b/web/src/composables/gql/gql.ts @@ -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. */ diff --git a/web/src/composables/gql/graphql.ts b/web/src/composables/gql/graphql.ts index fc55ad3df..fdef03926 100644 --- a/web/src/composables/gql/graphql.ts +++ b/web/src/composables/gql/graphql.ts @@ -2116,6 +2116,8 @@ export type Query = { /** Get the actual permissions that would be granted by a set of roles */ getPermissionsForRoles: Array; info: Info; + /** List installed Unraid OS plugins by .plg filename */ + installedUnraidPlugins: Array; 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 } } }; +export type InstalledUnraidPluginsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type InstalledUnraidPluginsQuery = { __typename?: 'Query', installedUnraidPlugins: Array }; + 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; 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; 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; +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; 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; 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; 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;