From 30b4b5c9027a9f193bd16e0879e617ab262ae929 Mon Sep 17 00:00:00 2001 From: logonoff Date: Sat, 10 Jan 2026 11:46:39 -0500 Subject: [PATCH] CONSOLE-3769: Refactor weird `getResourceSidebarSamples` --- .../locales/en/console-shared.json | 28 +- .../components/editor/CodeEditorSidebar.tsx | 6 +- .../formik-fields/CodeEditorField.tsx | 20 +- .../src/hooks/useResourceSidebarSamples.ts | 378 ++++++++++++++++ .../console-shared/src/utils/index.ts | 1 - .../console-shared/src/utils/sample-utils.ts | 405 ------------------ frontend/public/components/edit-yaml.tsx | 36 +- .../sidebars/resource-sidebar-samples.tsx | 6 +- .../components/sidebars/resource-sidebar.tsx | 2 +- 9 files changed, 422 insertions(+), 460 deletions(-) create mode 100644 frontend/packages/console-shared/src/hooks/useResourceSidebarSamples.ts delete mode 100644 frontend/packages/console-shared/src/utils/sample-utils.ts diff --git a/frontend/packages/console-shared/locales/en/console-shared.json b/frontend/packages/console-shared/locales/en/console-shared.json index 0a7e48539e4..dcb66499230 100644 --- a/frontend/packages/console-shared/locales/en/console-shared.json +++ b/frontend/packages/console-shared/locales/en/console-shared.json @@ -253,19 +253,6 @@ "This console plugin will be able to provide a custom interface and run any Kubernetes command as the logged in user. Make sure you trust it before enabling.": "This console plugin will be able to provide a custom interface and run any Kubernetes command as the logged in user. Make sure you trust it before enabling.", "OLSConfig": "OLSConfig", "OLSConfigs": "OLSConfigs", - "{{label}} details": "{{label}} details", - "Provisioned as node": "Provisioned as node", - "Select options": "Select options", - "Select input": "Select input", - "Pod": "Pod", - "Pods": "Pods", - "Scaled to 0": "Scaled to 0", - "Scaling to {{podSubTitle}}": "Scaling to {{podSubTitle}}", - "Autoscaled": "Autoscaled", - "to 0": "to 0", - "Autoscaling": "Autoscaling", - "to {{count}} Pod_one": "to {{count}} Pod", - "to {{count}} Pod_other": "to {{count}} Pods", "Allow reading Nodes in the core API groups (for ClusterRoleBinding)": "Allow reading Nodes in the core API groups (for ClusterRoleBinding)", "This \"ClusterRole\" is allowed to read the resource \"Nodes\" in the core group (because a Node is cluster-scoped, this must be bound with a \"ClusterRoleBinding\" to be effective).": "This \"ClusterRole\" is allowed to read the resource \"Nodes\" in the core group (because a Node is cluster-scoped, this must be bound with a \"ClusterRoleBinding\" to be effective).", "\"GET/POST\" requests to non-resource endpoint and all subpaths (for ClusterRoleBinding)": "\"GET/POST\" requests to non-resource endpoint and all subpaths (for ClusterRoleBinding)", @@ -308,10 +295,23 @@ "Provides a list of all the available user perspectives which are shown in the perspective dropdown. The perspectives must be added below spec customization.": "Provides a list of all the available user perspectives which are shown in the perspective dropdown. The perspectives must be added below spec customization.", "Add pinned resources": "Add pinned resources", "Provides a list of resources to be pinned on the Developer perspective navigation. The pinned resources must be added below spec customization perspectives.": "Provides a list of resources to be pinned on the Developer perspective navigation. The pinned resources must be added below spec customization perspectives.", - "Set maxUnavaliable to 0": "Set maxUnavaliable to 0", + "Set maxUnavailable to 0": "Set maxUnavailable to 0", "An eviction is allowed if at most 0 pods selected by \"selector\" are unavailable after the eviction.": "An eviction is allowed if at most 0 pods selected by \"selector\" are unavailable after the eviction.", "Set minAvailable to 25%": "Set minAvailable to 25%", "An eviction is allowed if at least 25% of pods selected by \"selector\" will still be available after the eviction.": "An eviction is allowed if at least 25% of pods selected by \"selector\" will still be available after the eviction.", + "{{label}} details": "{{label}} details", + "Provisioned as node": "Provisioned as node", + "Select options": "Select options", + "Select input": "Select input", + "Pod": "Pod", + "Pods": "Pods", + "Scaled to 0": "Scaled to 0", + "Scaling to {{podSubTitle}}": "Scaling to {{podSubTitle}}", + "Autoscaled": "Autoscaled", + "to 0": "to 0", + "Autoscaling": "Autoscaling", + "to {{count}} Pod_one": "to {{count}} Pod", + "to {{count}} Pod_other": "to {{count}} Pods", "Helm Release": "Helm Release", "Name must consist of lower-case letters, numbers and hyphens. It must start with a letter and end with a letter or number.": "Name must consist of lower-case letters, numbers and hyphens. It must start with a letter and end with a letter or number.", "Cannot be longer than {{characterCount}} characters.": "Cannot be longer than {{characterCount}} characters.", diff --git a/frontend/packages/console-shared/src/components/editor/CodeEditorSidebar.tsx b/frontend/packages/console-shared/src/components/editor/CodeEditorSidebar.tsx index 3fea4f0d3b7..d066731f121 100644 --- a/frontend/packages/console-shared/src/components/editor/CodeEditorSidebar.tsx +++ b/frontend/packages/console-shared/src/components/editor/CodeEditorSidebar.tsx @@ -1,11 +1,11 @@ import type { MutableRefObject, FC } from 'react'; import { useCallback } from 'react'; -import { JSONSchema7 } from 'json-schema'; +import type { JSONSchema7 } from 'json-schema'; import { Range, Selection } from 'monaco-editor'; import { CodeEditorRef } from '@console/dynamic-plugin-sdk'; import { ResourceSidebar } from '@console/internal/components/sidebars/resource-sidebar'; -import { K8sKind } from '@console/internal/module/k8s'; -import { Sample } from '../../utils'; +import type { K8sKind } from '@console/internal/module/k8s'; +import type { Sample } from '@console/shared/src/hooks/useResourceSidebarSamples'; import { downloadYaml } from './yaml-download-utils'; type CodeEditorSidebarProps = { diff --git a/frontend/packages/console-shared/src/components/formik-fields/CodeEditorField.tsx b/frontend/packages/console-shared/src/components/formik-fields/CodeEditorField.tsx index e7ef9e62c24..ec10aea0cef 100644 --- a/frontend/packages/console-shared/src/components/formik-fields/CodeEditorField.tsx +++ b/frontend/packages/console-shared/src/components/formik-fields/CodeEditorField.tsx @@ -3,7 +3,6 @@ import { useRef, useState, useCallback } from 'react'; import { css } from '@patternfly/react-styles'; import { FormikValues, useField, useFormikContext } from 'formik'; import { isEmpty } from 'lodash'; -import { useTranslation } from 'react-i18next'; import { useResolvedExtensions, isYAMLTemplate, @@ -16,7 +15,7 @@ import { ConsoleYAMLSampleModel } from '@console/internal/models'; import { getYAMLTemplates } from '@console/internal/models/yaml-templates'; import { definitionFor, K8sResourceCommon, referenceForModel } from '@console/internal/module/k8s'; import { ToggleSidebarButton } from '@console/shared/src/components/editor/ToggleSidebarButton'; -import { getResourceSidebarSamples } from '../../utils'; +import { useResourceSidebarSamples } from '@console/shared/src/hooks/useResourceSidebarSamples'; import { CodeEditorFieldProps } from './field-types'; import './CodeEditorField.scss'; @@ -40,7 +39,6 @@ const CodeEditorField: FC = ({ }) => { const [field] = useField(name); const { setFieldValue } = useFormikContext(); - const { t } = useTranslation(); const editorRef = useRef(); const [sidebarOpen, setSidebarOpen] = useState(true); @@ -49,17 +47,11 @@ const CodeEditorField: FC = ({ SampleResource, ); - const { samples, snippets } = model - ? getResourceSidebarSamples( - model, - { - data: sampleResources, - loaded, - loadError, - }, - t, - ) - : { samples: [], snippets: [] }; + const { samples, snippets } = useResourceSidebarSamples(model, { + data: sampleResources, + loaded, + loadError, + }); const definition = model ? definitionFor(model) : { properties: [] }; const hasSchema = !!schema || (!!definition && !isEmpty(definition.properties)); diff --git a/frontend/packages/console-shared/src/hooks/useResourceSidebarSamples.ts b/frontend/packages/console-shared/src/hooks/useResourceSidebarSamples.ts new file mode 100644 index 00000000000..0a35573cc83 --- /dev/null +++ b/frontend/packages/console-shared/src/hooks/useResourceSidebarSamples.ts @@ -0,0 +1,378 @@ +import { Map as ImmutableMap } from 'immutable'; +import YAML from 'js-yaml'; +import * as _ from 'lodash'; +import { useTranslation } from 'react-i18next'; +import { PodDisruptionBudgetModel } from '@console/app/src/models'; +import { + AddAction, + isAddAction, + CatalogItemType, + isCatalogItemType, + isPerspective, + Perspective, +} from '@console/dynamic-plugin-sdk'; +import { FirehoseResult } from '@console/internal/components/utils/types'; +import { + BuildConfigModel, + ClusterRoleModel, + ConsoleLinkModel, + ConsoleOperatorConfigModel, + ResourceQuotaModel, + RoleModel, +} from '@console/internal/models'; +import { + apiVersionForModel, + GroupVersionKind, + K8sKind, + K8sResourceKind, + referenceForModel, +} from '@console/internal/module/k8s'; +import { useExtensions } from '@console/plugin-sdk/src/api/useExtensions'; +import { defaultCatalogCategories } from '../utils/default-categories'; + +export type Sample = { + highlightText?: string; + title: string; + img?: string; + description: string; + id: string; + yaml?: string; + lazyYaml?: () => string; + snippet?: boolean; + targetResource: { + apiVersion: string; + kind: string; + }; +}; + +type ProjectAccessRoles = { + availableClusterRoles: string[]; +}; + +const getTargetResource = (model: K8sKind) => ({ + apiVersion: apiVersionForModel(model), + kind: model.kind, +}); + +const defaultProjectAccessRoles: ProjectAccessRoles = { + availableClusterRoles: ['admin', 'edit', 'view'], +}; + +const samplePinnedResources = [ + { group: 'apps', version: 'v1', resource: 'deployments' }, + { group: '', version: 'v1', resource: 'secrets' }, + { group: '', version: 'v1', resource: 'configmaps' }, + { group: '', version: 'v1', resource: 'pods' }, +]; + +const useClusterRoleBindingSamples = (): Sample[] => { + const { t } = useTranslation('console-shared'); + + return [ + { + title: t('Allow reading Nodes in the core API groups (for ClusterRoleBinding)'), + description: t( + 'This "ClusterRole" is allowed to read the resource "Nodes" in the core group (because a Node is cluster-scoped, this must be bound with a "ClusterRoleBinding" to be effective).', + ), + id: 'read-nodes', + targetResource: getTargetResource(ClusterRoleModel), + }, + { + title: t( + '"GET/POST" requests to non-resource endpoint and all subpaths (for ClusterRoleBinding)', + ), + description: t( + 'This "ClusterRole" is allowed to "GET" and "POST" requests to the non-resource endpoint "/healthz" and all subpaths (must be in the "ClusterRole" bound with a "ClusterRoleBinding" to be effective).', + ), + id: 'get-and-post-to-non-resource-endpoints', + targetResource: getTargetResource(ClusterRoleModel), + }, + ]; +}; + +const useDefaultSamples = () => { + const { t } = useTranslation('console-shared'); + const addActions = useExtensions(isAddAction); + const catalogItemTypes = useExtensions(isCatalogItemType); + const perspectives = useExtensions(isPerspective); + const clusterRoleBindingSamples = useClusterRoleBindingSamples(); + + return ImmutableMap() + .setIn( + [referenceForModel(BuildConfigModel)], + [ + { + title: t('Build from Dockerfile'), + description: t( + 'A Dockerfile build performs an image build using a Dockerfile in the source repository or specified in build configuration.', + ), + id: 'docker-build', + targetResource: getTargetResource(BuildConfigModel), + }, + { + title: t('Source-to-Image (S2I) build'), + description: t( + 'S2I is a tool for building reproducible container images. It produces ready-to-run images by injecting the application source into a container image and assembling a new image.', + ), + id: 's2i-build', + targetResource: getTargetResource(BuildConfigModel), + }, + ], + ) + .setIn( + [referenceForModel(ResourceQuotaModel)], + [ + { + title: t('Set compute resource quota'), + description: t( + 'Limit the total amount of memory and CPU that can be used in a namespace.', + ), + id: 'rq-compute', + targetResource: getTargetResource(ResourceQuotaModel), + }, + { + title: t('Set maximum count for any resource'), + description: t( + 'Restrict maximum count of each resource so users cannot create more than the allotted amount.', + ), + id: 'rq-counts', + targetResource: getTargetResource(ResourceQuotaModel), + }, + { + title: t('Specify resource quotas for a given storage class'), + description: t( + 'Limit the size and number of persistent volume claims that can be created with a storage class.', + ), + id: 'rq-storageclass', + targetResource: getTargetResource(ResourceQuotaModel), + }, + ], + ) + .setIn( + [referenceForModel(RoleModel)], + [ + { + title: t('Allow reading the resource in API group'), + description: t( + 'This "Role" is allowed to read the resource "Pods" in the core API group.', + ), + id: 'read-pods-within-ns', + targetResource: getTargetResource(RoleModel), + }, + { + title: t('Allow reading/writing the resource in API group'), + description: t( + 'This "Role" is allowed to read and write the "Deployments" in both the "extensions" and "apps" API groups.', + ), + id: 'read-write-deployment-in-ext-and-apps-apis', + targetResource: getTargetResource(RoleModel), + }, + { + title: t('Allow different access rights to different types of resource and API groups'), + description: t( + 'This "Role" is allowed to read "Pods" and read/write "Jobs" resources in API groups.', + ), + id: 'read-pods-and-read-write-jobs', + targetResource: getTargetResource(RoleModel), + }, + { + title: t('Allow reading a ConfigMap in a specific namespace (for RoleBinding)'), + description: t( + 'This "Role" is allowed to read a "ConfigMap" named "my-config" (must be bound with a "RoleBinding" to limit to a single "ConfigMap" in a single namespace).', + ), + id: 'read-configmap-within-ns', + targetResource: getTargetResource(RoleModel), + }, + ...clusterRoleBindingSamples, + ], + ) + .setIn([referenceForModel(ClusterRoleModel)], clusterRoleBindingSamples) + .setIn( + [referenceForModel(ConsoleLinkModel)], + [ + { + title: t('Add a link to the user menu'), + description: t( + 'The user menu appears in the right side of the masthead below the username.', + ), + id: 'cl-user-menu', + targetResource: getTargetResource(ConsoleLinkModel), + }, + { + title: t('Add a link to the application menu'), + description: t( + 'The application menu appears in the masthead below the 9x9 grid icon. Application menu links can include an optional image and section heading.', + ), + id: 'cl-application-menu', + targetResource: getTargetResource(ConsoleLinkModel), + }, + { + title: t('Add a link to the namespace dashboard'), + description: t( + 'Namespace dashboard links appear on the project dashboard and namespace details pages in a section called "Launcher". Namespace dashboard links can optionally be restricted to a specific namespace or namespaces.', + ), + id: 'cl-namespace-dashboard', + targetResource: getTargetResource(ConsoleLinkModel), + }, + { + title: t('Add a link to the contact mail'), + description: t( + 'The contact mail link appears in the user menu below the username. The link will open the default email client with the email address filled in.', + ), + id: 'cl-contact-mail', + targetResource: getTargetResource(ConsoleLinkModel), + }, + ], + ) + .setIn( + [referenceForModel(ConsoleOperatorConfigModel)], + [ + { + title: t('Add catalog categories'), + description: t( + 'Provides a list of default categories which are shown in the Software Catalog. The categories must be added below customization developerCatalog.', + ), + id: 'devcatalog-categories', + snippet: true, + lazyYaml: () => YAML.dump(defaultCatalogCategories), + targetResource: getTargetResource(ConsoleOperatorConfigModel), + }, + { + title: t('Add project access roles'), + description: t( + 'Provides a list of default roles which are shown in the Project Access. The roles must be added below customization projectAccess.', + ), + id: 'projectaccess-roles', + snippet: true, + lazyYaml: () => YAML.dump(defaultProjectAccessRoles), + targetResource: getTargetResource(ConsoleOperatorConfigModel), + }, + { + title: t('Add page actions'), + description: t( + 'Provides a list of all available actions on the Add page in the Developer perspective. The IDs must be added below customization addPage disabledActions to hide these actions.', + ), + id: 'addpage-actions', + snippet: true, + lazyYaml: () => { + const sortedExtensions = addActions + .slice() + .sort((a, b) => a.properties.id.localeCompare(b.properties.id)); + const yaml = sortedExtensions + .map((extension) => { + const { id, label, description } = extension.properties; + const labelComment = label.split('\n').join('\n # '); + const descriptionComment = description.split('\n').join('\n # '); + return `- # ${labelComment}\n # ${descriptionComment}\n ${id}`; + }) + .join('\n'); + return yaml; + }, + targetResource: getTargetResource(ConsoleOperatorConfigModel), + }, + { + title: t('Add sub-catalog types'), + description: t( + 'Provides a list of all the available sub-catalog types which are shown in the Software Catalog. The types must be added below spec customization developerCatalog', + ), + id: 'devcatalog-types', + snippet: true, + lazyYaml: () => { + const enabledTypes = { + state: 'Enabled', + enabled: catalogItemTypes.map((extension) => extension.properties.type), + }; + return YAML.dump(enabledTypes); + }, + targetResource: getTargetResource(ConsoleOperatorConfigModel), + }, + { + title: t('Add user perspectives'), + description: t( + 'Provides a list of all the available user perspectives which are shown in the perspective dropdown. The perspectives must be added below spec customization.', + ), + id: 'user-perspectives', + snippet: true, + lazyYaml: () => { + const yaml = perspectives.map((extension) => { + const { id } = extension.properties; + return { + id, + visibility: { + state: 'Enabled', + }, + }; + }); + return YAML.dump(yaml); + }, + targetResource: getTargetResource(ConsoleOperatorConfigModel), + }, + { + title: t('Add pinned resources'), + description: t( + 'Provides a list of resources to be pinned on the Developer perspective navigation. The pinned resources must be added below spec customization perspectives.', + ), + id: 'dev-pinned-resources', + snippet: true, + lazyYaml: () => YAML.dump(samplePinnedResources), + targetResource: getTargetResource(ConsoleOperatorConfigModel), + }, + ], + ) + .setIn( + [referenceForModel(PodDisruptionBudgetModel)], + [ + { + title: t('Set maxUnavailable to 0'), + description: t( + 'An eviction is allowed if at most 0 pods selected by "selector" are unavailable after the eviction.', + ), + id: 'pdb-max-unavailable', + targetResource: getTargetResource(PodDisruptionBudgetModel), + }, + { + title: t('Set minAvailable to 25%'), + description: t( + 'An eviction is allowed if at least 25% of pods selected by "selector" will still be available after the eviction.', + ), + id: 'pdb-min-available', + targetResource: getTargetResource(PodDisruptionBudgetModel), + }, + ], + ); +}; + +export const useResourceSidebarSamples = (kindObj: K8sKind, yamlSamplesList: FirehoseResult) => { + const defaultSamples = useDefaultSamples(); + + if (!kindObj) { + return { snippets: [], samples: [] }; + } + + const yamlSamplesData = !_.isEmpty(yamlSamplesList) + ? _.filter( + yamlSamplesList.data, + (sample: K8sResourceKind) => + sample.spec.targetResource.apiVersion === apiVersionForModel(kindObj) && + sample.spec.targetResource.kind === kindObj.kind, + ) + : []; + + const existingSamples = defaultSamples.get(referenceForModel(kindObj)) || []; + const extensionSamples = !_.isEmpty(yamlSamplesData) + ? yamlSamplesData.map((sample: K8sResourceKind) => { + return { + id: sample.metadata.uid, + ...(sample.spec as Exclude), + }; + }) + : []; + + const allSamples = [...existingSamples, ...extensionSamples]; + + // For the time being, `snippets` are a superset of `samples` + const snippets = allSamples.filter((sample: Sample) => sample.snippet); + const samples = allSamples.filter((sample: Sample) => !sample.snippet); + + return { snippets, samples }; +}; diff --git a/frontend/packages/console-shared/src/utils/index.ts b/frontend/packages/console-shared/src/utils/index.ts index aac57da945d..b05617e4664 100644 --- a/frontend/packages/console-shared/src/utils/index.ts +++ b/frontend/packages/console-shared/src/utils/index.ts @@ -15,7 +15,6 @@ export * from './alert-utils'; export * from './operator-utils'; export * from './helm-utils'; export * from './hpa-utils'; -export * from './sample-utils'; export * from './multiselectdropdown'; export * from './annotations'; export * from './yup-validations'; diff --git a/frontend/packages/console-shared/src/utils/sample-utils.ts b/frontend/packages/console-shared/src/utils/sample-utils.ts deleted file mode 100644 index 907eb4fef8b..00000000000 --- a/frontend/packages/console-shared/src/utils/sample-utils.ts +++ /dev/null @@ -1,405 +0,0 @@ -import { TFunction } from 'i18next'; -import { Map as ImmutableMap } from 'immutable'; -import YAML from 'js-yaml'; -import * as _ from 'lodash'; -import { PodDisruptionBudgetModel } from '@console/app/src/models'; -import { - AddAction, - isAddAction, - CatalogItemType, - isCatalogItemType, - isPerspective, - Perspective, -} from '@console/dynamic-plugin-sdk'; -import type { LoadedExtension } from '@console/dynamic-plugin-sdk/src/types'; -import { FirehoseResult } from '@console/internal/components/utils/types'; -import { - BuildConfigModel, - ClusterRoleModel, - ConsoleLinkModel, - ConsoleOperatorConfigModel, - ResourceQuotaModel, - RoleModel, -} from '@console/internal/models'; -import { - apiVersionForModel, - GroupVersionKind, - K8sKind, - K8sResourceKind, - referenceForModel, -} from '@console/internal/module/k8s'; -import { subscribeToExtensions } from '@console/plugin-sdk/src/api/pluginSubscriptionService'; -import { defaultCatalogCategories } from './default-categories'; - -export type Sample = { - highlightText?: string; - title: string; - img?: string; - description: string; - id: string; - yaml?: string; - lazyYaml?: () => Promise; - snippet?: boolean; - targetResource: { - apiVersion: string; - kind: string; - }; -}; - -type ProjectAccessRoles = { - availableClusterRoles: string[]; -}; - -const getTargetResource = (model: K8sKind) => ({ - apiVersion: apiVersionForModel(model), - kind: model.kind, -}); - -const defaultProjectAccessRoles: ProjectAccessRoles = { - availableClusterRoles: ['admin', 'edit', 'view'], -}; - -const samplePinnedResources = [ - { group: 'apps', version: 'v1', resource: 'deployments' }, - { group: '', version: 'v1', resource: 'secrets' }, - { group: '', version: 'v1', resource: 'configmaps' }, - { group: '', version: 'v1', resource: 'pods' }, -]; - -const clusterRoleBindingSamples = (t: TFunction): Sample[] => [ - { - title: t('console-shared~Allow reading Nodes in the core API groups (for ClusterRoleBinding)'), - description: t( - 'console-shared~This "ClusterRole" is allowed to read the resource "Nodes" in the core group (because a Node is cluster-scoped, this must be bound with a "ClusterRoleBinding" to be effective).', - ), - id: 'read-nodes', - targetResource: getTargetResource(ClusterRoleModel), - }, - { - title: t( - 'console-shared~"GET/POST" requests to non-resource endpoint and all subpaths (for ClusterRoleBinding)', - ), - description: t( - 'console-shared~This "ClusterRole" is allowed to "GET" and "POST" requests to the non-resource endpoint "/healthz" and all subpaths (must be in the "ClusterRole" bound with a "ClusterRoleBinding" to be effective).', - ), - id: 'get-and-post-to-non-resource-endpoints', - targetResource: getTargetResource(ClusterRoleModel), - }, -]; - -const defaultSamples = (t: TFunction) => - ImmutableMap() - .setIn( - [referenceForModel(BuildConfigModel)], - [ - { - title: t('console-shared~Build from Dockerfile'), - description: t( - 'console-shared~A Dockerfile build performs an image build using a Dockerfile in the source repository or specified in build configuration.', - ), - id: 'docker-build', - targetResource: getTargetResource(BuildConfigModel), - }, - { - title: t('console-shared~Source-to-Image (S2I) build'), - description: t( - 'console-shared~S2I is a tool for building reproducible container images. It produces ready-to-run images by injecting the application source into a container image and assembling a new image.', - ), - id: 's2i-build', - targetResource: getTargetResource(BuildConfigModel), - }, - ], - ) - .setIn( - [referenceForModel(ResourceQuotaModel)], - [ - { - title: t('console-shared~Set compute resource quota'), - description: t( - 'console-shared~Limit the total amount of memory and CPU that can be used in a namespace.', - ), - id: 'rq-compute', - targetResource: getTargetResource(ResourceQuotaModel), - }, - { - title: t('console-shared~Set maximum count for any resource'), - description: t( - 'console-shared~Restrict maximum count of each resource so users cannot create more than the allotted amount.', - ), - id: 'rq-counts', - targetResource: getTargetResource(ResourceQuotaModel), - }, - { - title: t('console-shared~Specify resource quotas for a given storage class'), - description: t( - 'console-shared~Limit the size and number of persistent volume claims that can be created with a storage class.', - ), - id: 'rq-storageclass', - targetResource: getTargetResource(ResourceQuotaModel), - }, - ], - ) - .setIn( - [referenceForModel(RoleModel)], - [ - { - title: t('console-shared~Allow reading the resource in API group'), - description: t( - 'console-shared~This "Role" is allowed to read the resource "Pods" in the core API group.', - ), - id: 'read-pods-within-ns', - targetResource: getTargetResource(RoleModel), - }, - { - title: t('console-shared~Allow reading/writing the resource in API group'), - description: t( - 'console-shared~This "Role" is allowed to read and write the "Deployments" in both the "extensions" and "apps" API groups.', - ), - id: 'read-write-deployment-in-ext-and-apps-apis', - targetResource: getTargetResource(RoleModel), - }, - { - title: t( - 'console-shared~Allow different access rights to different types of resource and API groups', - ), - description: t( - 'console-shared~This "Role" is allowed to read "Pods" and read/write "Jobs" resources in API groups.', - ), - id: 'read-pods-and-read-write-jobs', - targetResource: getTargetResource(RoleModel), - }, - { - title: t( - 'console-shared~Allow reading a ConfigMap in a specific namespace (for RoleBinding)', - ), - description: t( - 'console-shared~This "Role" is allowed to read a "ConfigMap" named "my-config" (must be bound with a "RoleBinding" to limit to a single "ConfigMap" in a single namespace).', - ), - id: 'read-configmap-within-ns', - targetResource: getTargetResource(RoleModel), - }, - ...clusterRoleBindingSamples(t), - ], - ) - .setIn([referenceForModel(ClusterRoleModel)], clusterRoleBindingSamples(t)) - .setIn( - [referenceForModel(ConsoleLinkModel)], - [ - { - title: t('console-shared~Add a link to the user menu'), - description: t( - 'console-shared~The user menu appears in the right side of the masthead below the username.', - ), - id: 'cl-user-menu', - targetResource: getTargetResource(ConsoleLinkModel), - }, - { - title: t('console-shared~Add a link to the application menu'), - description: t( - 'console-shared~The application menu appears in the masthead below the 9x9 grid icon. Application menu links can include an optional image and section heading.', - ), - id: 'cl-application-menu', - targetResource: getTargetResource(ConsoleLinkModel), - }, - { - title: t('console-shared~Add a link to the namespace dashboard'), - description: t( - 'console-shared~Namespace dashboard links appear on the project dashboard and namespace details pages in a section called "Launcher". Namespace dashboard links can optionally be restricted to a specific namespace or namespaces.', - ), - id: 'cl-namespace-dashboard', - targetResource: getTargetResource(ConsoleLinkModel), - }, - { - title: t('console-shared~Add a link to the contact mail'), - description: t( - 'console-shared~The contact mail link appears in the user menu below the username. The link will open the default email client with the email address filled in.', - ), - id: 'cl-contact-mail', - targetResource: getTargetResource(ConsoleLinkModel), - }, - ], - ) - .setIn( - [referenceForModel(ConsoleOperatorConfigModel)], - [ - { - title: t('console-shared~Add catalog categories'), - description: t( - 'console-shared~Provides a list of default categories which are shown in the Software Catalog. The categories must be added below customization developerCatalog.', - ), - id: 'devcatalog-categories', - snippet: true, - lazyYaml: () => YAML.dump(defaultCatalogCategories), - targetResource: getTargetResource(ConsoleOperatorConfigModel), - }, - { - title: t('console-shared~Add project access roles'), - description: t( - 'console-shared~Provides a list of default roles which are shown in the Project Access. The roles must be added below customization projectAccess.', - ), - id: 'projectaccess-roles', - snippet: true, - lazyYaml: () => YAML.dump(defaultProjectAccessRoles), - targetResource: getTargetResource(ConsoleOperatorConfigModel), - }, - { - title: t('console-shared~Add page actions'), - description: t( - 'console-shared~Provides a list of all available actions on the Add page in the Developer perspective. The IDs must be added below customization addPage disabledActions to hide these actions.', - ), - id: 'addpage-actions', - snippet: true, - lazyYaml: () => { - // Similar to useTranslationExt - const translateExtension = (key: string) => { - if (key.length < 3 || key[0] !== '%' || key[key.length - 1] !== '%') { - return key; - } - return t(key.substr(1, key.length - 2)); - }; - return new Promise((resolve) => { - // Using subscribeToExtensions here instead of useExtensions hook because - // this lazyYaml method is not called in a render flow. - // We should probably have a yaml snippets extension later for this. - const unsubscribe = subscribeToExtensions( - (extensions: LoadedExtension[]) => { - const sortedExtensions = extensions - .slice() - .sort((a, b) => a.properties.id.localeCompare(b.properties.id)); - const yaml = sortedExtensions - .map((extension) => { - const { id, label, description } = extension.properties; - const labelComment = translateExtension(label).split('\n').join('\n # '); - const descriptionComment = translateExtension(description) - .split('\n') - .join('\n # '); - return `- # ${labelComment}\n # ${descriptionComment}\n ${id}`; - }) - .join('\n'); - resolve(yaml); - unsubscribe(); - }, - isAddAction, - ); - }); - }, - targetResource: getTargetResource(ConsoleOperatorConfigModel), - }, - { - title: t('console-shared~Add sub-catalog types'), - description: t( - 'console-shared~Provides a list of all the available sub-catalog types which are shown in the Software Catalog. The types must be added below spec customization developerCatalog', - ), - id: 'devcatalog-types', - snippet: true, - lazyYaml: () => { - return new Promise((resolve) => { - const unsubscribe = subscribeToExtensions( - (extensions: LoadedExtension[]) => { - const enabledTypes = { - state: 'Enabled', - enabled: extensions.map((extension) => extension.properties.type), - }; - resolve(YAML.dump(enabledTypes)); - unsubscribe(); - }, - isCatalogItemType, - ); - }); - }, - targetResource: getTargetResource(ConsoleOperatorConfigModel), - }, - { - title: t('console-shared~Add user perspectives'), - description: t( - 'console-shared~Provides a list of all the available user perspectives which are shown in the perspective dropdown. The perspectives must be added below spec customization.', - ), - id: 'user-perspectives', - snippet: true, - lazyYaml: () => { - return new Promise((resolve) => { - const unsubscribe = subscribeToExtensions( - (extensions: LoadedExtension[]) => { - const yaml = extensions.map((extension) => { - const { id } = extension.properties; - return { - id, - visibility: { - state: 'Enabled', - }, - }; - }); - resolve(YAML.dump(yaml)); - unsubscribe(); - }, - isPerspective, - ); - }); - }, - targetResource: getTargetResource(ConsoleOperatorConfigModel), - }, - { - title: t('console-shared~Add pinned resources'), - description: t( - 'console-shared~Provides a list of resources to be pinned on the Developer perspective navigation. The pinned resources must be added below spec customization perspectives.', - ), - id: 'dev-pinned-resources', - snippet: true, - lazyYaml: () => YAML.dump(samplePinnedResources), - targetResource: getTargetResource(ConsoleOperatorConfigModel), - }, - ], - ) - .setIn( - [referenceForModel(PodDisruptionBudgetModel)], - [ - { - title: t('console-shared~Set maxUnavaliable to 0'), - description: t( - 'console-shared~An eviction is allowed if at most 0 pods selected by "selector" are unavailable after the eviction.', - ), - id: 'pdb-max-unavailable', - targetResource: getTargetResource(PodDisruptionBudgetModel), - }, - { - title: t('console-shared~Set minAvailable to 25%'), - description: t( - 'console-shared~An eviction is allowed if at least 25% of pods selected by "selector" will still be available after the eviction.', - ), - id: 'pdb-min-available', - targetResource: getTargetResource(PodDisruptionBudgetModel), - }, - ], - ); - -export const getResourceSidebarSamples = ( - kindObj: K8sKind, - yamlSamplesList: FirehoseResult, - t: TFunction, -) => { - const yamlSamplesData = !_.isEmpty(yamlSamplesList) - ? _.filter( - yamlSamplesList.data, - (sample: K8sResourceKind) => - sample.spec.targetResource.apiVersion === apiVersionForModel(kindObj) && - sample.spec.targetResource.kind === kindObj.kind, - ) - : []; - const existingSamples = defaultSamples(t).get(referenceForModel(kindObj)) || []; - const extensionSamples = !_.isEmpty(yamlSamplesData) - ? yamlSamplesData.map((sample: K8sResourceKind) => { - return { - id: sample.metadata.uid, - ...(sample.spec as Exclude), - }; - }) - : []; - - const allSamples = [...existingSamples, ...extensionSamples]; - - // For the time being, `snippets` are a superset of `samples` - const snippets = allSamples.filter((sample: Sample) => sample.snippet); - const samples = allSamples.filter((sample: Sample) => !sample.snippet); - - return { snippets, samples }; -}; diff --git a/frontend/public/components/edit-yaml.tsx b/frontend/public/components/edit-yaml.tsx index efd5f86572d..e979649dd4f 100644 --- a/frontend/public/components/edit-yaml.tsx +++ b/frontend/public/components/edit-yaml.tsx @@ -12,7 +12,7 @@ import { Trans, useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom-v5-compat'; import { FLAGS, ALL_NAMESPACES_KEY } from '@console/shared/src/constants/common'; import { getBadgeFromType } from '@console/shared/src/components/badges/badge-factory'; -import { getResourceSidebarSamples } from '@console/shared/src/utils/sample-utils'; +import { useResourceSidebarSamples } from '@console/shared/src/hooks/useResourceSidebarSamples'; import { useTelemetry } from '@console/shared/src/hooks/useTelemetry'; import { useResourceConnectionHandler } from '@console/shared/src/hooks/useResourceConnectionHandler'; @@ -212,8 +212,10 @@ const EditYAMLInner: FC = (props) => { [models], ); + const model = getModel(props.obj) || props.model; + const { samples, snippets } = useResourceSidebarSamples(model, yamlSamplesList); + const navigateToResourceList = () => { - const model = getModel(props.obj) || props.model; if (model) { const namespace = model.namespaced && props.activeNamespace !== ALL_NAMESPACES_KEY @@ -260,15 +262,15 @@ const EditYAMLInner: FC = (props) => { return; } - const model = getModel(obj); - if (!model) { + const objModel = getModel(obj); + if (!objModel) { return; } const { name, namespace } = obj.metadata; const resourceAttributes: AccessReviewResourceAttributes = { - group: model.apiGroup, - resource: model.plural, + group: objModel.apiGroup, + resource: objModel.plural, verb: 'update', name, namespace, @@ -284,7 +286,7 @@ const EditYAMLInner: FC = (props) => { console.warn('Error while check edit access', e); }); }, - [props.readOnly, props.impersonate, create, getModel, editorMounted], + [props.readOnly, props.impersonate, create, editorMounted, getModel], ); const appendYAMLString = useCallback((yaml) => { @@ -462,12 +464,12 @@ const EditYAMLInner: FC = (props) => { const updateYAML = useCallback( (obj) => { - const model = getModel(obj); setSuccess(null); setErrors(null); + const objModel = getModel(obj); const response = create - ? k8sCreate(model, _.omit(obj, ['metadata.resourceVersion'])) - : k8sUpdate(model, obj, obj.metadata.namespace, obj.metadata.name); + ? k8sCreate(objModel, _.omit(obj, ['metadata.resourceVersion'])) + : k8sUpdate(objModel, obj, obj.metadata.namespace, obj.metadata.name); response .then((o) => postFormSubmissionCallback(o)) @@ -539,8 +541,8 @@ const EditYAMLInner: FC = (props) => { return t('public~No "kind" field found in YAML.'); } - const model = getModel(obj); - if (!model) { + const objModel = getModel(obj); + if (!objModel) { return t( 'public~The server doesn\'t have a resource type "kind: {{kind}}, apiVersion: {{apiVersion}}".', { kind: obj.kind, apiVersion: obj.apiVersion }, @@ -551,19 +553,19 @@ const EditYAMLInner: FC = (props) => { return t('public~No "metadata" field found in YAML.'); } - if (obj.metadata.namespace && !model.namespaced) { + if (obj.metadata.namespace && !objModel.namespaced) { delete obj.metadata.namespace; } // If this is a namespaced resource, default to the active namespace when none is specified in the YAML. - if (!obj.metadata.namespace && model.namespaced) { + if (!obj.metadata.namespace && objModel.namespaced) { if (props.activeNamespace === ALL_NAMESPACES_KEY) { return t('public~No "metadata.namespace" field found in YAML.'); } obj.metadata.namespace = props.activeNamespace; } }, - [getModel, props.activeNamespace, t], + [props.activeNamespace, t, getModel], ); const [saving, setSaving] = useState(false); @@ -786,10 +788,6 @@ const EditYAMLInner: FC = (props) => { const readOnly = props.readOnly || notAllowed; const options: CodeEditorProps['options'] = { fontSize, readOnly, scrollBeyondLastLine: false }; - const model = getModel(props.obj); - const { samples, snippets } = model - ? getResourceSidebarSamples(model, yamlSamplesList, t) - : { samples: [], snippets: [] }; const definition = model ? definitionFor(model) : { properties: [] }; const showSchema = definition && !_.isEmpty(definition.properties); const hasSidebarContent = showSchema || (create && !_.isEmpty(samples)) || !_.isEmpty(snippets); diff --git a/frontend/public/components/sidebars/resource-sidebar-samples.tsx b/frontend/public/components/sidebars/resource-sidebar-samples.tsx index 7e187ab1df8..2512f66ba44 100644 --- a/frontend/public/components/sidebars/resource-sidebar-samples.tsx +++ b/frontend/public/components/sidebars/resource-sidebar-samples.tsx @@ -8,7 +8,7 @@ import { ChevronDownIcon } from '@patternfly/react-icons/dist/esm/icons/chevron- import { ChevronRightIcon } from '@patternfly/react-icons/dist/esm/icons/chevron-right-icon'; import { DownloadIcon } from '@patternfly/react-icons/dist/esm/icons/download-icon'; import { PasteIcon } from '@patternfly/react-icons/dist/esm/icons/paste-icon'; -import { Sample } from '@console/shared/src/utils/sample-utils'; +import { Sample } from '@console/shared/src/hooks/useResourceSidebarSamples'; import { useTranslation } from 'react-i18next'; import { K8sKind, referenceFor } from '../../module/k8s'; @@ -93,12 +93,12 @@ const ResourceSidebarSnippet: FC = ({ const [yamlPreview, setYamlPreview] = useState(yaml); const [yamlPreviewOpen, setYamlPreviewOpen] = useState(false); - const resolveYaml = async (callback: (resolvedYaml: string) => void) => { + const resolveYaml = (callback: (resolvedYaml: string) => void) => { if (yaml) { callback(yaml); } else if (lazyYaml) { try { - callback(await lazyYaml()); + callback(lazyYaml()); } catch (error) { // eslint-disable-next-line no-console console.warn(`Error while running lazy yaml snippet ${id} (${title})`, error); diff --git a/frontend/public/components/sidebars/resource-sidebar.tsx b/frontend/public/components/sidebars/resource-sidebar.tsx index ebb4d0915b1..27a7cfc6ac3 100644 --- a/frontend/public/components/sidebars/resource-sidebar.tsx +++ b/frontend/public/components/sidebars/resource-sidebar.tsx @@ -12,7 +12,7 @@ import { } from './resource-sidebar-samples'; import { ExploreType } from './explore-type-sidebar'; import { SimpleTabNav, Tab } from '../utils/simple-tab-nav'; -import { Sample } from '@console/shared/src/utils/sample-utils'; +import { Sample } from '@console/shared/src/hooks/useResourceSidebarSamples'; import { Flex, FlexItem, Title } from '@patternfly/react-core'; import PaneBody from '@console/shared/src/components/layout/PaneBody';