diff --git a/frontend/src/components/common/CreateResourceButton.stories.tsx b/frontend/src/components/common/CreateResourceButton.stories.tsx new file mode 100644 index 0000000000..216da9a146 --- /dev/null +++ b/frontend/src/components/common/CreateResourceButton.stories.tsx @@ -0,0 +1,39 @@ +import { Meta, StoryFn } from '@storybook/react'; +import React from 'react'; +import { Provider } from 'react-redux'; +import store from '../../redux/stores/store'; +import { CreateResourceButton, CreateResourceButtonProps } from './CreateResourceButton'; + +export default { + title: 'CreateResourceButton', + component: CreateResourceButton, + decorators: [ + Story => ( + + + + ), + ], +} as Meta; + +const Template: StoryFn = args => ; + +export const ConfigMap = Template.bind({}); +ConfigMap.args = { + resource: 'Config Map', +}; + +export const Secret = Template.bind({}); +Secret.args = { + resource: 'Secret', +}; + +export const Lease = Template.bind({}); +Lease.args = { + resource: 'Lease', +}; + +export const RuntimeClass = Template.bind({}); +RuntimeClass.args = { + resource: 'RuntimeClass', +}; diff --git a/frontend/src/components/common/CreateResourceButton.tsx b/frontend/src/components/common/CreateResourceButton.tsx new file mode 100644 index 0000000000..4fb7fc31f9 --- /dev/null +++ b/frontend/src/components/common/CreateResourceButton.tsx @@ -0,0 +1,210 @@ +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/cluster'; +import { KubeConfigMap } from '../../lib/k8s/configMap'; +import { KubeRuntimeClass } from '../../lib/k8s/runtime'; +import { KubeSecret } from '../../lib/k8s/secret'; +import { clusterAction } from '../../redux/clusterActionSlice'; +import { EventStatus, HeadlampEventType, useEventCallback } from '../../redux/headlampEventSlice'; +import { ActionButton, EditorDialog } from '../common'; + +const creationTimestamp = new Date('2022-01-01').toISOString(); +const BASE_EMPTY_CONFIG_MAP: KubeConfigMap = { + apiVersion: 'v1', + kind: 'ConfigMap', + metadata: { + creationTimestamp: '2023-04-27T20:31:27Z', + name: 'my-pvc', + namespace: 'default', + resourceVersion: '1234', + uid: 'abc-1234', + }, + data: {}, +}; +const LEASE_DUMMY_DATA = [ + { + apiVersion: 'coordination.k8s.io/v1', + kind: 'Lease', + metadata: { + name: 'lease', + namespace: 'default', + creationTimestamp, + uid: '123', + }, + spec: { + holderIdentity: 'holder', + leaseDurationSeconds: 10, + leaseTransitions: 1, + renewTime: '2021-03-01T00:00:00Z', + }, + }, +]; +const BASE_RC: KubeRuntimeClass = { + apiVersion: 'node.k8s.io/v1', + kind: 'RuntimeClass', + metadata: { + name: 'runtime-class', + namespace: 'default', + creationTimestamp, + uid: '123', + }, + handler: 'handler', + overhead: { + cpu: '100m', + memory: '128Mi', + }, + scheduling: { + nodeSelector: { + key: 'value', + }, + tolerations: [ + { + key: 'key', + operator: 'Equal', + value: 'value', + effect: 'NoSchedule', + tolerationSeconds: 10, + }, + ], + }, +}; +const BASE_EMPTY_SECRET: KubeSecret = { + apiVersion: 'v1', + kind: 'Secret', + metadata: { + creationTimestamp: '2023-04-27T20:31:27Z', + name: 'my-pvc', + namespace: 'default', + resourceVersion: '1234', + uid: 'abc-1234', + }, + data: {}, + type: 'bla', +}; + +export interface CreateResourceButtonProps { + resource: string; +} + +export function CreateResourceButton(props: CreateResourceButtonProps) { + const { resource } = props; + const { t } = useTranslation(['glossary', 'translation']); + const [openDialog, setOpenDialog] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState(''); + const dispatchCreateEvent = useEventCallback(HeadlampEventType.CREATE_RESOURCE); + const dispatch = useDispatch(); + + 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, + }); + } + setErrorMessage(msg); + setOpenDialog(true); + throw msg; + } + }); + } + ); + }; + + function handleSave(newItemDefs: KubeObjectInterface[]) { + let massagedNewItemDefs = newItemDefs; + const cancelUrl = location.pathname; + + // check if all yaml objects are valid + for (let i = 0; i < massagedNewItemDefs.length; i++) { + if (massagedNewItemDefs[i].kind === 'List') { + // flatten this List kind with the items that it has which is a list of valid k8s resources + const deletedItem = massagedNewItemDefs.splice(i, 1); + massagedNewItemDefs = massagedNewItemDefs.concat(deletedItem[0].items); + } + if (!massagedNewItemDefs[i].metadata?.name) { + setErrorMessage( + t(`translation|Invalid: One or more of resources doesn't have a name property`), + ); + return; + } + if (!massagedNewItemDefs[i].kind) { + setErrorMessage(t('translation|Invalid: Please set a kind to the resource')); + return; + } + } + // all resources name + const resourceNames = massagedNewItemDefs.map(newItemDef => newItemDef.metadata.name); + setOpenDialog(false); + + const clusterName = getCluster() || ''; + + dispatch( + clusterAction(() => applyFunc(massagedNewItemDefs, 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, + }) + ); + + dispatchCreateEvent({ + status: EventStatus.CONFIRMED, + }); + } + + const defaultContentMap: { [key: string]: any } = { + 'Config Map': BASE_EMPTY_CONFIG_MAP, + Secret: BASE_EMPTY_SECRET, + Lease: LEASE_DUMMY_DATA, + RuntimeClass: BASE_RC, + }; + + const getDefaultContent = () => defaultContentMap[resource] || ''; + + return ( + + { + setOpenDialog(true); + }} + /> + + setOpenDialog(false)} + onSave={handleSave} + saveLabel={t('translation|Apply')} + errorMessage={errorMessage} + onEditorChanged={() => setErrorMessage('')} + title={t('translation|Create {{ resource }}', { resource })} + /> + + ); +} diff --git a/frontend/src/components/common/__snapshots__/CreateResourceButton.ConfigMap.stories.storyshot b/frontend/src/components/common/__snapshots__/CreateResourceButton.ConfigMap.stories.storyshot new file mode 100644 index 0000000000..b506319d0d --- /dev/null +++ b/frontend/src/components/common/__snapshots__/CreateResourceButton.ConfigMap.stories.storyshot @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/common/__snapshots__/CreateResourceButton.Lease.stories.storyshot b/frontend/src/components/common/__snapshots__/CreateResourceButton.Lease.stories.storyshot new file mode 100644 index 0000000000..620729476f --- /dev/null +++ b/frontend/src/components/common/__snapshots__/CreateResourceButton.Lease.stories.storyshot @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/common/__snapshots__/CreateResourceButton.RuntimeClass.stories.storyshot b/frontend/src/components/common/__snapshots__/CreateResourceButton.RuntimeClass.stories.storyshot new file mode 100644 index 0000000000..5773b367fe --- /dev/null +++ b/frontend/src/components/common/__snapshots__/CreateResourceButton.RuntimeClass.stories.storyshot @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/common/__snapshots__/CreateResourceButton.Secret.stories.storyshot b/frontend/src/components/common/__snapshots__/CreateResourceButton.Secret.stories.storyshot new file mode 100644 index 0000000000..067f4f9804 --- /dev/null +++ b/frontend/src/components/common/__snapshots__/CreateResourceButton.Secret.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/List.tsx b/frontend/src/components/configmap/List.tsx index 4c099de7fd..173c96161d 100644 --- a/frontend/src/components/configmap/List.tsx +++ b/frontend/src/components/configmap/List.tsx @@ -1,5 +1,6 @@ import { useTranslation } from 'react-i18next'; import ConfigMap from '../../lib/k8s/configMap'; +import { CreateResourceButton } from '../common/CreateResourceButton'; import ResourceListView from '../common/Resource/ResourceListView'; export default function ConfigMapList() { @@ -8,6 +9,9 @@ export default function ConfigMapList() { return ( ], + }} resourceClass={ConfigMap} columns={[ 'name', diff --git a/frontend/src/components/configmap/__snapshots__/Details.WithBase.stories.storyshot b/frontend/src/components/configmap/__snapshots__/Details.WithBase.stories.storyshot index 37a7919d30..6c0872dbe4 100644 --- a/frontend/src/components/configmap/__snapshots__/Details.WithBase.stories.storyshot +++ b/frontend/src/components/configmap/__snapshots__/Details.WithBase.stories.storyshot @@ -195,13 +195,13 @@ style="display: flex; position: relative; text-align: initial; width: 100%; height: 100%;" >
@@ -442,13 +442,13 @@ style="display: flex; position: relative; text-align: initial; width: 100%; height: 100%;" >
@@ -689,13 +689,13 @@ style="display: flex; position: relative; text-align: initial; width: 100%; height: 100%;" >
diff --git a/frontend/src/components/configmap/__snapshots__/List.Items.stories.storyshot b/frontend/src/components/configmap/__snapshots__/List.Items.stories.storyshot index fa43b1ab7f..ade4ab7b1f 100644 --- a/frontend/src/components/configmap/__snapshots__/List.Items.stories.storyshot +++ b/frontend/src/components/configmap/__snapshots__/List.Items.stories.storyshot @@ -18,7 +18,19 @@
+ > + +
], + }} resourceClass={Lease} columns={[ 'name', diff --git a/frontend/src/components/lease/__snapshots__/List.Items.stories.storyshot b/frontend/src/components/lease/__snapshots__/List.Items.stories.storyshot index bbf461e82b..ad3bfbe58c 100644 --- a/frontend/src/components/lease/__snapshots__/List.Items.stories.storyshot +++ b/frontend/src/components/lease/__snapshots__/List.Items.stories.storyshot @@ -18,7 +18,19 @@
+ > + +
], + }} resourceClass={RuntimeClass} columns={[ 'name', diff --git a/frontend/src/components/runtimeClass/__snapshots__/List.Items.stories.storyshot b/frontend/src/components/runtimeClass/__snapshots__/List.Items.stories.storyshot index a96ef23ff0..cd3342f529 100644 --- a/frontend/src/components/runtimeClass/__snapshots__/List.Items.stories.storyshot +++ b/frontend/src/components/runtimeClass/__snapshots__/List.Items.stories.storyshot @@ -18,7 +18,19 @@
+ > + +
diff --git a/frontend/src/components/secret/List.tsx b/frontend/src/components/secret/List.tsx index e4729c372a..2faed67891 100644 --- a/frontend/src/components/secret/List.tsx +++ b/frontend/src/components/secret/List.tsx @@ -1,5 +1,6 @@ import { useTranslation } from 'react-i18next'; import Secret from '../../lib/k8s/secret'; +import { CreateResourceButton } from '../common/CreateResourceButton'; import ResourceListView from '../common/Resource/ResourceListView'; export default function SecretList() { @@ -8,6 +9,9 @@ export default function SecretList() { return ( ], + }} resourceClass={Secret} columns={[ 'name', diff --git a/frontend/src/components/secret/__snapshots__/List.Items.stories.storyshot b/frontend/src/components/secret/__snapshots__/List.Items.stories.storyshot index 19abf7837e..9b67d4e567 100644 --- a/frontend/src/components/secret/__snapshots__/List.Items.stories.storyshot +++ b/frontend/src/components/secret/__snapshots__/List.Items.stories.storyshot @@ -18,7 +18,19 @@
+ > + +