From bfe6e4ccc68d402162c860dbde34dc9b2e404ebe Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 28 May 2024 07:49:52 -0400 Subject: [PATCH 1/2] frontend: Accept given YAML/JSON in EditorDialog Signed-off-by: Evangelos Skopelitis --- .../common/Resource/CreateButton.tsx | 3 +- .../common/Resource/EditorDialog.tsx | 64 +++++++++++-------- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/frontend/src/components/common/Resource/CreateButton.tsx b/frontend/src/components/common/Resource/CreateButton.tsx index 4bd579fdb7..5b9c4245f9 100644 --- a/frontend/src/components/common/Resource/CreateButton.tsx +++ b/frontend/src/components/common/Resource/CreateButton.tsx @@ -36,6 +36,7 @@ export default function CreateButton(props: CreateButtonProps) { const dispatchCreateEvent = useEventCallback(HeadlampEventType.CREATE_RESOURCE); const clusters = useClusterGroup(); const [targetCluster, setTargetCluster] = React.useState(clusters[0] || ''); + const [currentItem, setCurrentItem] = React.useState({}); // eslint-disable-line no-unused-vars // When the clusters in the group change, we want to reset the target cluster // if it's not in the new list of clusters. @@ -148,7 +149,7 @@ export default function CreateButton(props: CreateButtonProps) { )} setOpenDialog(false)} onSave={handleSave} diff --git a/frontend/src/components/common/Resource/EditorDialog.tsx b/frontend/src/components/common/Resource/EditorDialog.tsx index 58bd07b2e6..983c30cd50 100644 --- a/frontend/src/components/common/Resource/EditorDialog.tsx +++ b/frontend/src/components/common/Resource/EditorDialog.tsx @@ -49,8 +49,8 @@ import SimpleEditor from './SimpleEditor'; type KubeObjectIsh = Partial; export interface EditorDialogProps extends DialogProps { - /** The object to edit, or null to make the dialog be in "loading mode". Pass it an empty object if no contents are to be shown when the dialog is first open. */ - item: KubeObjectIsh | null; + /** The object(s) to edit, or null to make the dialog be in "loading mode". Pass it an empty object if no contents are to be shown when the dialog is first open. */ + item: KubeObjectIsh | object | object[] | string | null; /** Called when the dialog is closed. */ onClose: () => void; /** Called when the user clicks the save button. */ @@ -88,11 +88,14 @@ export default function EditorDialog(props: EditorDialogProps) { const [lang, setLang] = React.useState(i18n.language); const themeName = getThemeName(); - const originalCodeRef = React.useRef({ code: '', format: item ? 'yaml' : '' }); + const initialCode = typeof item === 'string' ? item : yaml.dump(item || {}); + const originalCodeRef = React.useRef({ code: initialCode, format: item ? 'yaml' : '' }); const [code, setCode] = React.useState(originalCodeRef.current); const codeRef = React.useRef(code); const lastCodeCheckHandler = React.useRef(0); - const previousVersionRef = React.useRef(item?.metadata?.resourceVersion || ''); + const previousVersionRef = React.useRef( + isKubeObjectIsh(item) ? item?.metadata?.resourceVersion || '' : '' + ); const [error, setError] = React.useState(''); const [docSpecs, setDocSpecs] = React.useState< KubeObjectInterface | KubeObjectInterface[] | null @@ -109,36 +112,45 @@ export default function EditorDialog(props: EditorDialogProps) { setUseSimpleEditorState(data); } + function isKubeObjectIsh(item: any): item is KubeObjectIsh { + return item && typeof item === 'object' && !Array.isArray(item) && 'metadata' in item; + } + // Update the code when the item changes, but only if the code hasn't been touched. React.useEffect(() => { if (!item || Object.keys(item || {}).length === 0) { + const defaultCode = '# Enter your YAML or JSON here'; + originalCodeRef.current = { code: defaultCode, format: 'yaml' }; + setCode({ code: defaultCode, format: 'yaml' }); return; } - const originalCode = originalCodeRef.current.code; - const itemCode = - originalCodeRef.current.format === 'json' ? JSON.stringify(item) : yaml.dump(item); - if (itemCode !== originalCodeRef.current.code) { - originalCodeRef.current = { code: itemCode, format: originalCodeRef.current.format }; - } + // Determine the format (YAML or JSON) and serialize to string + const format = looksLikeJson(originalCodeRef.current.code) ? 'json' : 'yaml'; + const itemCode = format === 'json' ? JSON.stringify(item) : yaml.dump(item); - if (!item.metadata) { - return; + // Update the code if the item representation has changed + if (itemCode !== originalCodeRef.current.code) { + originalCodeRef.current = { code: itemCode, format }; + setCode({ code: itemCode, format }); } - const resourceVersionsDiffer = - (previousVersionRef.current || '') !== (item.metadata!.resourceVersion || ''); - // Only change if the code hasn't been touched. - // We use the codeRef in this effect instead of the code, because we need to access the current - // state of the code but we don't want to trigger a re-render when we set the code here. - if (resourceVersionsDiffer || codeRef.current.code === originalCode) { - // Prevent updating to the same code, which would lead to an infinite loop. - if (codeRef.current.code !== itemCode) { - setCode({ code: itemCode, format: originalCodeRef.current.format }); - } + // Additional handling for Kubernetes objects + if (isKubeObjectIsh(item) && item.metadata) { + const resourceVersionsDiffer = + (previousVersionRef.current || '') !== (item.metadata!.resourceVersion || ''); + // Only change if the code hasn't been touched. + // We use the codeRef in this effect instead of the code, because we need to access the current + // state of the code but we don't want to trigger a re-render when we set the code here. + if (resourceVersionsDiffer || codeRef.current.code === originalCodeRef.current.code) { + // Prevent updating to the same code, which would lead to an infinite loop. + if (codeRef.current.code !== itemCode) { + setCode({ code: itemCode, format: originalCodeRef.current.format }); + } - if (resourceVersionsDiffer && !!item.metadata!.resourceVersion) { - previousVersionRef.current = item.metadata!.resourceVersion; + if (resourceVersionsDiffer && !!item.metadata!.resourceVersion) { + previousVersionRef.current = item.metadata!.resourceVersion; + } } } }, [item]); @@ -309,7 +321,9 @@ export default function EditorDialog(props: EditorDialogProps) { const errorLabel = error || errorMessage; let dialogTitle = title; if (!dialogTitle && item) { - const itemName = item.metadata?.name || t('New Object'); + const itemName = isKubeObjectIsh(item) + ? item.metadata?.name || t('New Object') + : t('New Object'); dialogTitle = isReadOnly() ? t('translation|View: {{ itemName }}', { itemName }) : t('translation|Edit: {{ itemName }}', { itemName }); From 46f4e679060e5b00b16817c52680980eefc05bae Mon Sep 17 00:00:00 2001 From: Evangelos Skopelitis Date: Tue, 28 May 2024 07:51:13 -0400 Subject: [PATCH 2/2] frontend: Add create resource UI These changes introduce a new UI feature that allows users to create resources from the associated list view. Clicking the 'Create' button opens up the EditorDialog used in the generic 'Create / Apply' button, now accepting generic YAML/JSON text rather than explicitly expecting an item that looks like a Kubernetes resource. The dialog box also includes a generic template for each resource. The apply logic for this new feature (as well as the original 'Create / Apply' button) has been consolidated in EditorDialog, with a flag allowing external components to utilize their own dispatch functionality. Fixes: #1820 Signed-off-by: Evangelos Skopelitis --- .../common/CreateResourceButton.stories.tsx | 98 +++++++ .../common/CreateResourceButton.tsx | 41 +++ .../common/Resource/EditorDialog.stories.tsx | 11 + .../common/Resource/EditorDialog.tsx | 82 +++++- .../common/Resource/ResourceListView.tsx | 10 +- .../common/Resource/ViewButton.stories.tsx | 11 + ...rceButton.ConfigMapStory.stories.storyshot | 1 + ...ceButton.InvalidResource.stories.storyshot | 13 + ...urceButton.ValidResource.stories.storyshot | 13 + frontend/src/components/common/index.test.ts | 1 + frontend/src/components/common/index.ts | 1 + .../List.Items.stories.storyshot | 265 +++++++++++++++++- .../src/components/crd/CustomResourceList.tsx | 8 +- .../CustomResourceList.List.stories.storyshot | 265 +++++++++++++++++- .../List.DaemonSets.stories.storyshot | 265 +++++++++++++++++- .../EndpointList.Items.stories.storyshot | 265 +++++++++++++++++- .../HPAList.Items.stories.storyshot | 265 +++++++++++++++++- .../ClassList.Items.stories.storyshot | 265 +++++++++++++++++- .../List.Items.stories.storyshot | 265 +++++++++++++++++- .../List.Items.stories.storyshot | 265 +++++++++++++++++- .../List.Nodes.stories.storyshot | 265 +++++++++++++++++- .../pdbList.Items.stories.storyshot | 265 +++++++++++++++++- .../priorityClassList.Items.stories.storyshot | 265 +++++++++++++++++- .../List.ReplicaSets.stories.storyshot | 265 +++++++++++++++++- .../List.Items.stories.storyshot | 265 +++++++++++++++++- .../List.Items.stories.storyshot | 265 +++++++++++++++++- .../ClaimList.Items.stories.storyshot | 265 +++++++++++++++++- .../ClassList.Items.stories.storyshot | 265 +++++++++++++++++- .../VolumeList.Items.stories.storyshot | 265 +++++++++++++++++- .../VPAList.List.stories.storyshot | 265 +++++++++++++++++- ...gWebhookConfigList.Items.stories.storyshot | 265 +++++++++++++++++- ...gWebhookConfigList.Items.stories.storyshot | 265 +++++++++++++++++- frontend/src/i18n/locales/de/translation.json | 1 + frontend/src/i18n/locales/en/translation.json | 1 + frontend/src/i18n/locales/es/translation.json | 1 + frontend/src/i18n/locales/fr/translation.json | 1 + frontend/src/i18n/locales/pt/translation.json | 1 + frontend/src/lib/k8s/KubeObject.ts | 12 + frontend/src/lib/k8s/configMap.ts | 6 + frontend/src/lib/k8s/cronJob.ts | 31 ++ frontend/src/lib/k8s/daemonSet.ts | 34 ++- frontend/src/lib/k8s/deployment.ts | 29 ++ frontend/src/lib/k8s/endpoints.ts | 23 ++ frontend/src/lib/k8s/hpa.ts | 11 + frontend/src/lib/k8s/ingress.ts | 33 +++ frontend/src/lib/k8s/ingressClass.ts | 6 + frontend/src/lib/k8s/lease.ts | 11 + frontend/src/lib/k8s/limitRange.tsx | 28 ++ .../lib/k8s/mutatingWebhookConfiguration.ts | 26 ++ frontend/src/lib/k8s/networkpolicy.tsx | 43 +++ frontend/src/lib/k8s/persistentVolume.ts | 19 ++ frontend/src/lib/k8s/persistentVolumeClaim.ts | 13 + frontend/src/lib/k8s/podDisruptionBudget.ts | 6 + frontend/src/lib/k8s/priorityClass.ts | 9 + frontend/src/lib/k8s/replicaSet.ts | 28 ++ frontend/src/lib/k8s/resourceQuota.ts | 6 + frontend/src/lib/k8s/runtime.ts | 6 + frontend/src/lib/k8s/secret.ts | 6 + frontend/src/lib/k8s/service.ts | 20 ++ frontend/src/lib/k8s/serviceAccount.ts | 10 + frontend/src/lib/k8s/statefulSet.ts | 32 ++- frontend/src/lib/k8s/storageClass.ts | 9 + .../lib/k8s/validatingWebhookConfiguration.ts | 26 ++ frontend/src/lib/k8s/vpa.ts | 12 + .../plugin/__snapshots__/pluginLib.snapshot | 1 + 65 files changed, 6059 insertions(+), 32 deletions(-) create mode 100644 frontend/src/components/common/CreateResourceButton.stories.tsx create mode 100644 frontend/src/components/common/CreateResourceButton.tsx create mode 100644 frontend/src/components/common/__snapshots__/CreateResourceButton.ConfigMapStory.stories.storyshot create mode 100644 frontend/src/components/common/__snapshots__/CreateResourceButton.InvalidResource.stories.storyshot create mode 100644 frontend/src/components/common/__snapshots__/CreateResourceButton.ValidResource.stories.storyshot diff --git a/frontend/src/components/common/CreateResourceButton.stories.tsx b/frontend/src/components/common/CreateResourceButton.stories.tsx new file mode 100644 index 0000000000..77f3e5b808 --- /dev/null +++ b/frontend/src/components/common/CreateResourceButton.stories.tsx @@ -0,0 +1,98 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { expect, userEvent, waitFor } from '@storybook/test'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { KubeObjectClass } from '../../lib/k8s/cluster'; +import ConfigMap from '../../lib/k8s/configMap'; +import store from '../../redux/stores/store'; +import { TestContext } from '../../test'; +import { CreateResourceButton, CreateResourceButtonProps } from './CreateResourceButton'; + +export default { + title: 'CreateResourceButton', + component: CreateResourceButton, + parameters: { + storyshots: { + disable: true, + }, + }, + decorators: [ + Story => { + return ( + + + + + + ); + }, + ], +} as Meta; + +type Story = StoryObj; + +export const ValidResource: Story = { + args: { resourceClass: ConfigMap as unknown as KubeObjectClass }, + + play: async ({ args }) => { + await userEvent.click( + screen.getByRole('button', { + name: `Create ${args.resourceClass.getBaseObject().kind}`, + }) + ); + + await waitFor(() => expect(screen.getByRole('textbox')).toBeVisible()); + + await userEvent.click(screen.getByRole('textbox')); + + await userEvent.keyboard('{Control>}a{/Control} {Backspace}'); + await userEvent.keyboard(`apiVersion: v1{Enter}`); + await userEvent.keyboard(`kind: ConfigMap{Enter}`); + await userEvent.keyboard(`metadata:{Enter}`); + await userEvent.keyboard(` name: base-configmap`); + + const button = await screen.findByRole('button', { name: 'Apply' }); + expect(button).toBeVisible(); + }, +}; + +export const InvalidResource: Story = { + args: { resourceClass: ConfigMap as unknown as KubeObjectClass }, + + play: async ({ args }) => { + await userEvent.click( + screen.getByRole('button', { + name: `Create ${args.resourceClass.getBaseObject().kind}`, + }) + ); + + await waitFor(() => expect(screen.getByRole('textbox')).toBeVisible()); + + await userEvent.click(screen.getByRole('textbox')); + + await userEvent.keyboard('{Control>}a{/Control}'); + await userEvent.keyboard(`apiVersion: v1{Enter}`); + await userEvent.keyboard(`kind: ConfigMap{Enter}`); + await userEvent.keyboard(`metadata:{Enter}`); + await userEvent.keyboard(` name: base-configmap{Enter}`); + await userEvent.keyboard(`creationTimestamp: ''`); + + const button = await screen.findByRole('button', { name: 'Apply' }); + expect(button).toBeVisible(); + + await userEvent.click(button); + + await waitFor(() => + userEvent.click( + screen.getByRole('button', { + name: `Create ${args.resourceClass.getBaseObject().kind}`, + }) + ) + ); + + await waitFor(() => expect(screen.getByText(/Failed/)).toBeVisible(), { + timeout: 15000, + }); + }, +}; diff --git a/frontend/src/components/common/CreateResourceButton.tsx b/frontend/src/components/common/CreateResourceButton.tsx new file mode 100644 index 0000000000..8e3340e9c2 --- /dev/null +++ b/frontend/src/components/common/CreateResourceButton.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { KubeObjectClass } from '../../lib/k8s/cluster'; +import { ActionButton, AuthVisible, EditorDialog } from '../common'; + +export interface CreateResourceButtonProps { + resourceClass: KubeObjectClass; + resourceName?: string; +} + +export function CreateResourceButton(props: CreateResourceButtonProps) { + const { resourceClass, resourceName } = props; + const { t } = useTranslation(['glossary', 'translation']); + const [openDialog, setOpenDialog] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState(''); + + const baseObject = resourceClass.getBaseObject(); + const name = resourceName ?? baseObject.kind; + + return ( + + { + setOpenDialog(true); + }} + /> + setOpenDialog(false)} + saveLabel={t('translation|Apply')} + errorMessage={errorMessage} + onEditorChanged={() => setErrorMessage('')} + title={t('translation|Create {{ name }}', { name })} + /> + + ); +} diff --git a/frontend/src/components/common/Resource/EditorDialog.stories.tsx b/frontend/src/components/common/Resource/EditorDialog.stories.tsx index 0e974b6481..3ad4b5c66c 100644 --- a/frontend/src/components/common/Resource/EditorDialog.stories.tsx +++ b/frontend/src/components/common/Resource/EditorDialog.stories.tsx @@ -2,12 +2,23 @@ import FormControlLabel from '@mui/material/FormControlLabel'; import FormGroup from '@mui/material/FormGroup'; import Switch from '@mui/material/Switch'; import { Meta, StoryFn } from '@storybook/react'; +import { Provider } from 'react-redux'; +import store from '../../../redux/stores/store'; import { EditorDialog, EditorDialogProps } from '..'; export default { title: 'Resource/EditorDialog', component: EditorDialog, argTypes: {}, + decorators: [ + Story => { + return ( + + + + ); + }, + ], } as Meta; const Template: StoryFn = args => { diff --git a/frontend/src/components/common/Resource/EditorDialog.tsx b/frontend/src/components/common/Resource/EditorDialog.tsx index 983c30cd50..a39e319c4c 100644 --- a/frontend/src/components/common/Resource/EditorDialog.tsx +++ b/frontend/src/components/common/Resource/EditorDialog.tsx @@ -18,9 +18,19 @@ import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; +import { getCluster } from '../../../lib/cluster'; +import { apply } from '../../../lib/k8s/apiProxy'; import { KubeObjectInterface } from '../../../lib/k8s/KubeObject'; import { getThemeName } from '../../../lib/themes'; import { useId } from '../../../lib/util'; +import { clusterAction } from '../../../redux/clusterActionSlice'; +import { + EventStatus, + HeadlampEventType, + useEventCallback, +} from '../../../redux/headlampEventSlice'; +import { AppDispatch } from '../../../redux/stores/store'; import ConfirmButton from '../ConfirmButton'; import { Dialog, DialogProps } from '../Dialog'; import Loader from '../Loader'; @@ -53,8 +63,8 @@ export interface EditorDialogProps extends DialogProps { item: KubeObjectIsh | object | object[] | string | null; /** Called when the dialog is closed. */ onClose: () => void; - /** Called when the user clicks the save button. */ - onSave: ((...args: any[]) => void) | null; + /** Called by a component for when the user clicks the save button. When set to "default", internal save logic is applied. */ + onSave?: ((...args: any[]) => void) | 'default' | null; /** Called when the editor's contents change. */ onEditorChanged?: ((newValue: string) => void) | null; /** The label to use for the save button. */ @@ -71,7 +81,7 @@ export default function EditorDialog(props: EditorDialogProps) { const { item, onClose, - onSave, + onSave = 'default', onEditorChanged, saveLabel, errorMessage, @@ -106,6 +116,8 @@ export default function EditorDialog(props: EditorDialogProps) { const localData = localStorage.getItem('useSimpleEditor'); return localData ? JSON.parse(localData) : false; }); + const dispatchCreateEvent = useEventCallback(HeadlampEventType.CREATE_RESOURCE); + const dispatch: AppDispatch = useDispatch(); function setUseSimpleEditor(data: boolean) { localStorage.setItem('useSimpleEditor', JSON.stringify(data)); @@ -269,6 +281,32 @@ export default function EditorDialog(props: EditorDialogProps) { setCode(originalCodeRef.current); } + const applyFunc = async (newItems: KubeObjectInterface[], clusterName: string) => { + await Promise.allSettled(newItems.map(newItem => apply(newItem, clusterName))).then( + (values: any) => { + values.forEach((value: any, index: number) => { + if (value.status === 'rejected') { + let msg; + const kind = newItems[index].kind; + const name = newItems[index].metadata.name; + const apiVersion = newItems[index].apiVersion; + if (newItems.length === 1) { + msg = t('translation|Failed to create {{ kind }} {{ name }}.', { kind, name }); + } else { + msg = t('translation|Failed to create {{ kind }} {{ name }} in {{ apiVersion }}.', { + kind, + name, + apiVersion, + }); + } + setError(msg); + throw msg; + } + }); + } + ); + }; + function handleSave() { // Verify the YAML even means anything before trying to use it. const { obj, format, error } = getObjectsFromCode(code); @@ -285,7 +323,39 @@ export default function EditorDialog(props: EditorDialogProps) { setError(t("Error parsing the code. Please verify it's valid YAML or JSON!")); return; } - onSave!(obj); + + const newItemDefs = obj!; + + if (typeof onSave === 'string' && onSave === 'default') { + const resourceNames = newItemDefs.map(newItemDef => newItemDef.metadata.name); + const clusterName = getCluster() || ''; + + dispatch( + clusterAction(() => applyFunc(newItemDefs, clusterName), { + startMessage: t('translation|Applying {{ newItemName }}…', { + newItemName: resourceNames.join(','), + }), + cancelledMessage: t('translation|Cancelled applying {{ newItemName }}.', { + newItemName: resourceNames.join(','), + }), + successMessage: t('translation|Applied {{ newItemName }}.', { + newItemName: resourceNames.join(','), + }), + errorMessage: t('translation|Failed to apply {{ newItemName }}.', { + newItemName: resourceNames.join(','), + }), + cancelUrl: location.pathname, + }) + ); + + dispatchCreateEvent({ + status: EventStatus.CONFIRMED, + }); + + onClose(); + } else if (typeof onSave === 'function') { + onSave!(obj); + } } function makeEditor() { @@ -321,9 +391,7 @@ export default function EditorDialog(props: EditorDialogProps) { const errorLabel = error || errorMessage; let dialogTitle = title; if (!dialogTitle && item) { - const itemName = isKubeObjectIsh(item) - ? item.metadata?.name || t('New Object') - : t('New Object'); + const itemName = (isKubeObjectIsh(item) && item.metadata?.name) || t('New Object'); dialogTitle = isReadOnly() ? t('translation|View: {{ itemName }}', { itemName }) : t('translation|Edit: {{ itemName }}', { itemName }); diff --git a/frontend/src/components/common/Resource/ResourceListView.tsx b/frontend/src/components/common/Resource/ResourceListView.tsx index 19e6334b23..1157e0c674 100644 --- a/frontend/src/components/common/Resource/ResourceListView.tsx +++ b/frontend/src/components/common/Resource/ResourceListView.tsx @@ -1,6 +1,6 @@ import React, { PropsWithChildren, ReactElement, ReactNode } from 'react'; -import { KubeObject } from '../../../lib/k8s/KubeObject'; -import { KubeObjectClass } from '../../../lib/k8s/KubeObject'; +import { KubeObject, KubeObjectClass } from '../../../lib/k8s/KubeObject'; +import { CreateResourceButton } from '../CreateResourceButton'; import SectionBox from '../SectionBox'; import SectionFilterHeader, { SectionFilterHeaderProps } from '../SectionFilterHeader'; import ResourceTable, { ResourceTableProps } from './ResourceTable'; @@ -30,6 +30,8 @@ export default function ResourceListView( ) { const { title, children, headerProps, ...tableProps } = props; const withNamespaceFilter = 'resourceClass' in props && props.resourceClass?.isNamespaced; + const resourceClass = (props as ResourceListViewWithResourceClassProps) + .resourceClass as KubeObjectClass; return ( ] : undefined) + } {...headerProps} /> ) : ( diff --git a/frontend/src/components/common/Resource/ViewButton.stories.tsx b/frontend/src/components/common/Resource/ViewButton.stories.tsx index 9f4867efcc..b61ab181be 100644 --- a/frontend/src/components/common/Resource/ViewButton.stories.tsx +++ b/frontend/src/components/common/Resource/ViewButton.stories.tsx @@ -1,7 +1,9 @@ import '../../../i18n/config'; import { Meta, StoryFn } from '@storybook/react'; import React from 'react'; +import { Provider } from 'react-redux'; import { KubeObject } from '../../../lib/k8s/KubeObject'; +import store from '../../../redux/stores/store'; import ViewButton from './ViewButton'; import { ViewButtonProps } from './ViewButton'; @@ -9,6 +11,15 @@ export default { title: 'Resource/ViewButton', component: ViewButton, argTypes: {}, + decorators: [ + Story => { + return ( + + + + ); + }, + ], } as Meta; const Template: StoryFn = args => ; diff --git a/frontend/src/components/common/__snapshots__/CreateResourceButton.ConfigMapStory.stories.storyshot b/frontend/src/components/common/__snapshots__/CreateResourceButton.ConfigMapStory.stories.storyshot new file mode 100644 index 0000000000..df46f87231 --- /dev/null +++ b/frontend/src/components/common/__snapshots__/CreateResourceButton.ConfigMapStory.stories.storyshot @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/common/__snapshots__/CreateResourceButton.InvalidResource.stories.storyshot b/frontend/src/components/common/__snapshots__/CreateResourceButton.InvalidResource.stories.storyshot new file mode 100644 index 0000000000..895858ca2d --- /dev/null +++ b/frontend/src/components/common/__snapshots__/CreateResourceButton.InvalidResource.stories.storyshot @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/common/__snapshots__/CreateResourceButton.ValidResource.stories.storyshot b/frontend/src/components/common/__snapshots__/CreateResourceButton.ValidResource.stories.storyshot new file mode 100644 index 0000000000..895858ca2d --- /dev/null +++ b/frontend/src/components/common/__snapshots__/CreateResourceButton.ValidResource.stories.storyshot @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/common/index.test.ts b/frontend/src/components/common/index.test.ts index 0af5c688a8..a1d343491c 100644 --- a/frontend/src/components/common/index.test.ts +++ b/frontend/src/components/common/index.test.ts @@ -19,6 +19,7 @@ const checkExports = [ 'Chart', 'ConfirmDialog', 'ConfirmButton', + 'CreateResourceButton', 'Dialog', 'EmptyContent', 'ErrorPage', diff --git a/frontend/src/components/common/index.ts b/frontend/src/components/common/index.ts index 4e35bcc8c7..54e664535d 100644 --- a/frontend/src/components/common/index.ts +++ b/frontend/src/components/common/index.ts @@ -50,3 +50,4 @@ export { default as ConfirmButton } from './ConfirmButton'; export * from './NamespacesAutocomplete'; export * from './Table/Table'; export { default as Table } from './Table'; +export * from './CreateResourceButton'; diff --git a/frontend/src/components/configmap/__snapshots__/List.Items.stories.storyshot b/frontend/src/components/configmap/__snapshots__/List.Items.stories.storyshot index a43869cb31..9cdf1481c0 100644 --- a/frontend/src/components/configmap/__snapshots__/List.Items.stories.storyshot +++ b/frontend/src/components/configmap/__snapshots__/List.Items.stories.storyshot @@ -19,7 +19,19 @@
+ > + +
+