-
Notifications
You must be signed in to change notification settings - Fork 155
Commit
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. Fixes: #1820 Signed-off-by: Evangelos Skopelitis <[email protected]>
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { Meta, StoryFn } from '@storybook/react'; | ||
import React from 'react'; | ||
import { Provider } from 'react-redux'; | ||
import ConfigMap from '../../lib/k8s/configMap'; | ||
import { Lease } from '../../lib/k8s/lease'; | ||
import { RuntimeClass } from '../../lib/k8s/runtime'; | ||
import Secret from '../../lib/k8s/secret'; | ||
import store from '../../redux/stores/store'; | ||
import { CreateResourceButton, CreateResourceButtonProps } from './CreateResourceButton'; | ||
|
||
export default { | ||
title: 'CreateResourceButton', | ||
component: CreateResourceButton, | ||
decorators: [ | ||
Story => ( | ||
<Provider store={store}> | ||
<Story /> | ||
</Provider> | ||
), | ||
], | ||
} as Meta; | ||
|
||
const Template: StoryFn<CreateResourceButtonProps> = args => <CreateResourceButton {...args} />; | ||
|
||
export const ConfigMapStory = Template.bind({}); | ||
ConfigMapStory.args = { | ||
resourceClass: ConfigMap, | ||
}; | ||
|
||
export const LeaseStory = Template.bind({}); | ||
LeaseStory.args = { | ||
resourceClass: Lease, | ||
}; | ||
|
||
export const RuntimeClassStory = Template.bind({}); | ||
RuntimeClassStory.args = { | ||
resourceClass: RuntimeClass, | ||
}; | ||
|
||
export const SecretStory = Template.bind({}); | ||
SecretStory.args = { | ||
resourceClass: Secret, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
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 { ClassWithBaseObject, KubeObject, KubeObjectInterface } from '../../lib/k8s/cluster'; | ||
import { clusterAction } from '../../redux/clusterActionSlice'; | ||
import { EventStatus, HeadlampEventType, useEventCallback } from '../../redux/headlampEventSlice'; | ||
import { ActionButton, AuthVisible, EditorDialog } from '../common'; | ||
|
||
export interface CreateResourceButtonProps { | ||
resourceClass: ClassWithBaseObject<KubeObject>; | ||
} | ||
|
||
export function CreateResourceButton(props: CreateResourceButtonProps) { | ||
const { resourceClass } = 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 baseObject = resourceClass.getBaseObject() || {}; | ||
const resourceName = resourceClass.kind; | ||
|
||
return ( | ||
<AuthVisible item={resourceClass} authVerb="create"> | ||
<ActionButton | ||
color="primary" | ||
description={t('translation|Create {{ resourceName }}', { resourceName })} | ||
Check failure on line 107 in frontend/src/components/common/CreateResourceButton.tsx GitHub Actions / test (20.x, ubuntu-22.04)Unhandled error
Check failure on line 107 in frontend/src/components/common/CreateResourceButton.tsx GitHub Actions / test (20.x, ubuntu-22.04)src/storybook.test.tsx > Storybook Tests > crd/CustomResourceDefinition > Details
Check failure on line 107 in frontend/src/components/common/CreateResourceButton.tsx GitHub Actions / test (20.x, ubuntu-22.04)src/storybook.test.tsx > Storybook Tests > crd/CustomResourceList > List
Check failure on line 107 in frontend/src/components/common/CreateResourceButton.tsx GitHub Actions / test (20.x, ubuntu-22.04)src/storybook.test.tsx > Storybook Tests > DaemonSet/List > DaemonSets
Check failure on line 107 in frontend/src/components/common/CreateResourceButton.tsx GitHub Actions / test (20.x, ubuntu-22.04)src/storybook.test.tsx > Storybook Tests > endpoints/EndpointsListView > Items
Check failure on line 107 in frontend/src/components/common/CreateResourceButton.tsx GitHub Actions / test (20.x, ubuntu-22.04)src/storybook.test.tsx > Storybook Tests > hpa/HpaListView > Items
Check failure on line 107 in frontend/src/components/common/CreateResourceButton.tsx GitHub Actions / test (20.x, ubuntu-22.04)src/storybook.test.tsx > Storybook Tests > IngressClass/ListView > Items
Check failure on line 107 in frontend/src/components/common/CreateResourceButton.tsx GitHub Actions / test (20.x, ubuntu-22.04)src/storybook.test.tsx > Storybook Tests > Ingress/ListView > Items
Check failure on line 107 in frontend/src/components/common/CreateResourceButton.tsx GitHub Actions / test (20.x, ubuntu-22.04)src/storybook.test.tsx > Storybook Tests > Namespace/ListView > Regular
Check failure on line 107 in frontend/src/components/common/CreateResourceButton.tsx GitHub Actions / test (20.x, ubuntu-22.04)src/storybook.test.tsx > Storybook Tests > node/List > Nodes
|
||
icon={'mdi:plus-circle'} | ||
onClick={() => { | ||
setOpenDialog(true); | ||
}} | ||
/> | ||
|
||
<EditorDialog | ||
item={baseObject} | ||
open={openDialog} | ||
onClose={() => setOpenDialog(false)} | ||
onSave={handleSave} | ||
saveLabel={t('translation|Apply')} | ||
errorMessage={errorMessage} | ||
onEditorChanged={() => setErrorMessage('')} | ||
title={t('translation|Create {{ resourceName }}', { resourceName })} | ||
/> | ||
</AuthVisible> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<DocumentFragment /> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<DocumentFragment /> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<DocumentFragment /> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<DocumentFragment /> |