You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by mi...@apache.org on 2023/09/12 03:39:26 UTC
[incubator-devlake] 01/03: feat(config-ui): improve the api key in webhook
This is an automated email from the ASF dual-hosted git repository.
mintsweet pushed a commit to branch feat-5791
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git
commit 68d2f977b025286ffe04564e50125ea304fd73d5
Author: mintsweet <0x...@gmail.com>
AuthorDate: Mon Sep 11 22:00:24 2023 +1200
feat(config-ui): improve the api key in webhook
---
config-ui/src/plugins/register/webook/api.ts | 2 +
.../index.tsx => components/create-dialog.tsx} | 105 +++++++++--------
.../use-delete.ts => components/delete-dialog.tsx} | 43 ++++---
.../register/webook/components/edit-dialog.tsx | 73 ++++++++++++
.../plugins/register/webook/components/index.ts | 6 +-
.../webook/components/miller-columns/index.tsx | 66 -----------
.../miller-columns/use-miller-columns.ts | 58 ---------
.../register/webook/components/selector-dialog.tsx | 98 +++++++++++++++
.../register/webook/components/view-dialog.tsx | 131 +++++++++++++++++++++
.../{connection/index.tsx => connection.tsx} | 53 +++------
.../plugins/register/webook/connection/styled.ts | 37 ------
.../register/webook/delete-dialog/index.tsx | 55 ---------
.../register/webook/delete-dialog/styled.ts | 21 ----
config-ui/src/plugins/register/webook/index.ts | 6 +-
.../register/webook/selector-dialog/index.tsx | 61 ----------
.../register/webook/selector-dialog/styled.ts | 33 ------
.../register/webook/{create-dialog => }/styled.ts | 30 ++++-
.../register/webook/view-or-edit-dialog/index.tsx | 81 -------------
.../register/webook/view-or-edit-dialog/styled.ts | 34 ------
.../webook/view-or-edit-dialog/use-view-or-edit.ts | 77 ------------
20 files changed, 435 insertions(+), 635 deletions(-)
diff --git a/config-ui/src/plugins/register/webook/api.ts b/config-ui/src/plugins/register/webook/api.ts
index 15c4de061..fa35f40e6 100644
--- a/config-ui/src/plugins/register/webook/api.ts
+++ b/config-ui/src/plugins/register/webook/api.ts
@@ -42,3 +42,5 @@ export const deleteConnection = (id: ID) =>
request(`/plugins/webhook/connections/${id}`, {
method: 'delete',
});
+
+export const renewApiKey = (id: ID) => request(`/api-keys/${id}`, { method: 'put' });
diff --git a/config-ui/src/plugins/register/webook/create-dialog/index.tsx b/config-ui/src/plugins/register/webook/components/create-dialog.tsx
similarity index 60%
rename from config-ui/src/plugins/register/webook/create-dialog/index.tsx
rename to config-ui/src/plugins/register/webook/components/create-dialog.tsx
index 180bb16ac..5d4424eb1 100644
--- a/config-ui/src/plugins/register/webook/create-dialog/index.tsx
+++ b/config-ui/src/plugins/register/webook/components/create-dialog.tsx
@@ -19,12 +19,12 @@
import { useState, useMemo } from 'react';
import { InputGroup, Icon } from '@blueprintjs/core';
-import { Dialog, FormItem, CopyText, Message } from '@/components';
+import { Dialog, FormItem, CopyText } from '@/components';
+import { useConnections } from '@/hooks';
import { operator } from '@/utils';
import * as API from '../api';
-
-import * as S from './styled';
+import * as S from '../styled';
interface Props {
isOpen: boolean;
@@ -32,7 +32,7 @@ interface Props {
onSubmitAfter?: (id: ID) => void;
}
-export const WebhookCreateDialog = ({ isOpen, onCancel, onSubmitAfter }: Props) => {
+export const CreateDialog = ({ isOpen, onCancel, onSubmitAfter }: Props) => {
const [operating, setOperating] = useState(false);
const [step, setStep] = useState(1);
const [name, setName] = useState('');
@@ -44,49 +44,46 @@ export const WebhookCreateDialog = ({ isOpen, onCancel, onSubmitAfter }: Props)
apiKey: '',
});
+ const { onRefresh } = useConnections();
+
const prefix = useMemo(() => `${window.location.origin}/api`, []);
const handleSubmit = async () => {
- if (step === 1) {
- const [success, res] = await operator(
- async () => {
- const { id, apiKey } = await API.createConnection({ name });
- const { postIssuesEndpoint, closeIssuesEndpoint, postPipelineDeployTaskEndpoint } = await API.getConnection(
- id,
- );
- return {
- id,
- apiKey: apiKey.apiKey,
- postIssuesEndpoint,
- closeIssuesEndpoint,
- postPipelineDeployTaskEndpoint,
- };
- },
- {
- setOperating,
- hideToast: true,
- },
- );
+ const [success, res] = await operator(
+ async () => {
+ const { id, apiKey } = await API.createConnection({ name });
+ const { postIssuesEndpoint, closeIssuesEndpoint, postPipelineDeployTaskEndpoint } = await API.getConnection(id);
+ return {
+ id,
+ apiKey: apiKey.apiKey,
+ postIssuesEndpoint,
+ closeIssuesEndpoint,
+ postPipelineDeployTaskEndpoint,
+ };
+ },
+ {
+ setOperating,
+ hideToast: true,
+ },
+ );
- if (success) {
- setStep(2);
- setRecord({
- id: res.id,
- postIssuesEndpoint: `${prefix}${res.postIssuesEndpoint}?key={KEY}`,
- closeIssuesEndpoint: `${prefix}${res.closeIssuesEndpoint}?key={KEY}`,
- postDeploymentsCurl: `curl ${prefix}${res.postPipelineDeployTaskEndpoint} -X 'POST'
- \\ -H 'Authorization: Bearer {KEY}'
+ if (success) {
+ setStep(2);
+ setRecord({
+ id: res.id,
+ postIssuesEndpoint: `${prefix}${res.postIssuesEndpoint}?api_key=${res.apiKey}`,
+ closeIssuesEndpoint: `${prefix}${res.closeIssuesEndpoint}?api_key=${res.apiKey}`,
+ postDeploymentsCurl: `curl ${prefix}${res.postPipelineDeployTaskEndpoint} -X 'POST'
+ \\ -H 'Authorization: Bearer ${res.apiKey}'
\\ -d '{
\\"commit_sha\\":\\"the sha of deployment commit\\",
\\"repo_url\\":\\"the repo URL of the deployment commit\\",
\\"start_time\\":\\"eg. 2020-01-01T12:00:00+00:00\\"
}'`,
- apiKey: res.apiKey,
- });
- }
- } else {
- onCancel();
- onSubmitAfter?.(record.id);
+ apiKey: res.apiKey,
+ });
+ onRefresh('webhook');
+ onSubmitAfter?.(res.id);
}
};
@@ -95,6 +92,7 @@ export const WebhookCreateDialog = ({ isOpen, onCancel, onSubmitAfter }: Props)
isOpen={isOpen}
title="Add a New Webhook"
style={{ width: 820 }}
+ footer={step === 2 ? null : undefined}
okText={step === 1 ? 'Generate POST URL' : 'Done'}
okDisabled={step === 1 && !name}
okLoading={operating}
@@ -102,14 +100,18 @@ export const WebhookCreateDialog = ({ isOpen, onCancel, onSubmitAfter }: Props)
onOk={handleSubmit}
>
{step === 1 && (
- <S.Detail>
- <h3>Webhook Name *</h3>
- <p>Give your Webhook a unique name to help you identify it in the future.</p>
- <InputGroup value={name} onChange={(e) => setName(e.target.value)} />
- </S.Detail>
+ <S.Wrapper>
+ <FormItem
+ label="Webhook Name"
+ subLabel="Give your Webhook a unique name to help you identify it in the future."
+ required
+ >
+ <InputGroup placeholder="Webhook Name" value={name} onChange={(e) => setName(e.target.value)} />
+ </FormItem>
+ </S.Wrapper>
)}
{step === 2 && (
- <S.Detail>
+ <S.Wrapper>
<h2>
<Icon icon="endorsed" size={30} />
<span>POST URL Generated!</span>
@@ -118,6 +120,10 @@ export const WebhookCreateDialog = ({ isOpen, onCancel, onSubmitAfter }: Props)
Copy the following POST URLs to your issue tracking or CI tools to push `Incidents` and `Deployments` by
making a POST to DevLake.
</p>
+ <p>
+ An API key is automatically generated for the authentication of this webhook. This key does not expire. You
+ can revoke it in the webhook page at any time.
+ </p>
<FormItem label="Incident">
<h5>Post to register an incident</h5>
<CopyText content={record.postIssuesEndpoint} />
@@ -128,15 +134,10 @@ export const WebhookCreateDialog = ({ isOpen, onCancel, onSubmitAfter }: Props)
<h5>Post to register a deployment</h5>
<CopyText content={record.postDeploymentsCurl} />
</FormItem>
- <FormItem label="API Key">
- <Message
- style={{ marginBottom: 8 }}
- content="Please make sure to copy your API key now. You will not be able to see it again."
- />
- <CopyText content={record.apiKey} />
- </FormItem>
- </S.Detail>
+ </S.Wrapper>
)}
</Dialog>
);
};
+
+export default CreateDialog;
diff --git a/config-ui/src/plugins/register/webook/delete-dialog/use-delete.ts b/config-ui/src/plugins/register/webook/components/delete-dialog.tsx
similarity index 55%
rename from config-ui/src/plugins/register/webook/delete-dialog/use-delete.ts
rename to config-ui/src/plugins/register/webook/components/delete-dialog.tsx
index 71d959f06..404ba9a97 100644
--- a/config-ui/src/plugins/register/webook/delete-dialog/use-delete.ts
+++ b/config-ui/src/plugins/register/webook/components/delete-dialog.tsx
@@ -16,35 +16,48 @@
*
*/
-import { useState, useMemo } from 'react';
+import { useState } from 'react';
+import { Dialog, Message } from '@/components';
+import { useConnections } from '@/hooks';
import { operator } from '@/utils';
import * as API from '../api';
-export interface UseDeleteProps {
- initialID: ID;
+interface Props {
+ initialId: ID;
+ onCancel: () => void;
onSubmitAfter?: (id: ID) => void;
}
-export const useDelete = ({ initialID, onSubmitAfter }: UseDeleteProps) => {
- const [saving, setSaving] = useState(false);
+export const DeleteDialog = ({ initialId, onCancel, onSubmitAfter }: Props) => {
+ const [operating, setOperating] = useState(false);
- const handleDelete = async () => {
- const [success] = await operator(() => API.deleteConnection(initialID), {
- setOperating: setSaving,
+ const { onRefresh } = useConnections();
+
+ const handleSubmit = async () => {
+ const [success] = await operator(() => API.deleteConnection(initialId), {
+ setOperating,
});
if (success) {
- onSubmitAfter?.(initialID);
+ onRefresh('webhook');
+ onSubmitAfter?.(initialId);
+ onCancel();
}
};
- return useMemo(
- () => ({
- saving,
- onSubmit: handleDelete,
- }),
- [saving],
+ return (
+ <Dialog
+ isOpen
+ title="Delete this Webhook?"
+ // style={{ width: 600 }}
+ okText="Confirm"
+ okLoading={operating}
+ onCancel={onCancel}
+ onOk={handleSubmit}
+ >
+ <Message content="This Webhook cannot be recovered once it’s deleted." />
+ </Dialog>
);
};
diff --git a/config-ui/src/plugins/register/webook/components/edit-dialog.tsx b/config-ui/src/plugins/register/webook/components/edit-dialog.tsx
new file mode 100644
index 000000000..e66b554d1
--- /dev/null
+++ b/config-ui/src/plugins/register/webook/components/edit-dialog.tsx
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { useState, useEffect } from 'react';
+import { InputGroup } from '@blueprintjs/core';
+
+import { Dialog, FormItem } from '@/components';
+import { useConnections } from '@/hooks';
+import { operator } from '@/utils';
+
+import * as API from '../api';
+
+interface Props {
+ initialId: ID;
+ onCancel: () => void;
+}
+
+export const EditDialog = ({ initialId, onCancel }: Props) => {
+ const [name, setName] = useState('');
+ const [operating, setOperating] = useState(false);
+
+ const { onRefresh } = useConnections({ plugin: 'webhook' });
+
+ useEffect(() => {
+ (async () => {
+ const res = await API.getConnection(initialId);
+ setName(res.name);
+ })();
+ }, [initialId]);
+
+ const handleSubmit = async () => {
+ const [success] = await operator(() => API.updateConnection(initialId, { name }), {
+ setOperating,
+ });
+
+ if (success) {
+ onRefresh('webhook');
+ onCancel();
+ }
+ };
+
+ return (
+ <Dialog
+ style={{ width: 820 }}
+ isOpen
+ title="Edit Webhook Name"
+ okLoading={operating}
+ okDisabled={!name}
+ okText="Save"
+ onCancel={onCancel}
+ onOk={handleSubmit}
+ >
+ <FormItem label="Name" required>
+ <InputGroup value={name} onChange={(e) => setName(e.target.value)} />
+ </FormItem>
+ </Dialog>
+ );
+};
diff --git a/config-ui/src/plugins/register/webook/components/index.ts b/config-ui/src/plugins/register/webook/components/index.ts
index 30c0d1fa0..8703633a7 100644
--- a/config-ui/src/plugins/register/webook/components/index.ts
+++ b/config-ui/src/plugins/register/webook/components/index.ts
@@ -16,4 +16,8 @@
*
*/
-export * from './miller-columns';
+export * from './create-dialog';
+export * from './delete-dialog';
+export * from './edit-dialog';
+export * from './selector-dialog';
+export * from './view-dialog';
diff --git a/config-ui/src/plugins/register/webook/components/miller-columns/index.tsx b/config-ui/src/plugins/register/webook/components/miller-columns/index.tsx
deleted file mode 100644
index 4812f8d98..000000000
--- a/config-ui/src/plugins/register/webook/components/miller-columns/index.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-import { useState, useEffect } from 'react';
-import MillerColumnsSelect from 'miller-columns-select';
-
-import { Loading } from '@/components';
-
-import { useMillerColumns } from './use-miller-columns';
-
-interface Props {
- selectedItems: any[];
- onChangeItems: (selectedItems: any[]) => void;
-}
-
-export const MillerColumns = ({ selectedItems, onChangeItems }: Props) => {
- const [seletedIds, setSelectedIds] = useState<ID[]>([]);
-
- const { items, getHasMore } = useMillerColumns();
-
- useEffect(() => {
- setSelectedIds(selectedItems.map((it) => it.boardId));
- }, []);
-
- useEffect(() => {
- onChangeItems(
- items
- .filter((it) => seletedIds.includes(it.id))
- .map((it) => ({
- id: it.id,
- name: it.name,
- })),
- );
- }, [seletedIds]);
-
- const renderLoading = () => {
- return <Loading size={20} style={{ padding: '4px 12px' }} />;
- };
-
- return (
- <MillerColumnsSelect
- columnCount={1}
- columnHeight={160}
- getHasMore={getHasMore}
- renderLoading={renderLoading}
- items={items}
- selectedIds={seletedIds}
- onSelectItemIds={setSelectedIds}
- />
- );
-};
diff --git a/config-ui/src/plugins/register/webook/components/miller-columns/use-miller-columns.ts b/config-ui/src/plugins/register/webook/components/miller-columns/use-miller-columns.ts
deleted file mode 100644
index 95b79124e..000000000
--- a/config-ui/src/plugins/register/webook/components/miller-columns/use-miller-columns.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-import { useState, useEffect, useMemo } from 'react';
-import type { McsItem } from 'miller-columns-select';
-
-import * as API from '../../api';
-
-type WebhookItemType = McsItem<{
- id: ID;
- name: string;
-}>;
-
-export const useMillerColumns = () => {
- const [items, setItems] = useState<WebhookItemType[]>([]);
- const [isLast, setIsLast] = useState(false);
-
- const updateItems = (arr: any) =>
- arr.map((it: any) => ({
- parentId: null,
- id: it.id,
- title: it.name,
- name: it.name,
- }));
-
- useEffect(() => {
- (async () => {
- const res = await API.getConnections();
- setItems([...updateItems(res)]);
- setIsLast(true);
- })();
- }, []);
-
- return useMemo(
- () => ({
- items,
- getHasMore() {
- return !isLast;
- },
- }),
- [items, isLast],
- );
-};
diff --git a/config-ui/src/plugins/register/webook/components/selector-dialog.tsx b/config-ui/src/plugins/register/webook/components/selector-dialog.tsx
new file mode 100644
index 000000000..09fe03945
--- /dev/null
+++ b/config-ui/src/plugins/register/webook/components/selector-dialog.tsx
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { useState, useEffect } from 'react';
+import type { McsItem } from 'miller-columns-select';
+import MillerColumnsSelect from 'miller-columns-select';
+
+import { Dialog, FormItem, Loading } from '@/components';
+
+import * as T from '../types';
+import * as API from '../api';
+import * as S from '../styled';
+
+interface Props {
+ isOpen: boolean;
+ saving: boolean;
+ onCancel: () => void;
+ onSubmit: (items: T.WebhookItemType[]) => void;
+}
+
+export const SelectorDialog = ({ isOpen, saving, onCancel, onSubmit }: Props) => {
+ const [items, setItems] = useState<McsItem<T.WebhookItemType>[]>([]);
+ const [selectedItems, setSelectedItems] = useState<T.WebhookItemType[]>([]);
+ const [isLast, setIsLast] = useState(false);
+
+ const updateItems = (arr: any) =>
+ arr.map((it: any) => ({
+ parentId: null,
+ id: it.id,
+ title: it.name,
+ name: it.name,
+ }));
+
+ useEffect(() => {
+ (async () => {
+ const res = await API.getConnections();
+ setItems([...updateItems(res)]);
+ setIsLast(true);
+ })();
+ }, []);
+
+ const handleSubmit = () => onSubmit(selectedItems);
+
+ return (
+ <Dialog
+ isOpen={isOpen}
+ title="Select Existing Webhooks"
+ style={{
+ width: 820,
+ }}
+ okText="Confrim"
+ okLoading={saving}
+ okDisabled={!selectedItems.length}
+ onCancel={onCancel}
+ onOk={handleSubmit}
+ >
+ <S.Wrapper>
+ <FormItem label="Webhooks" subLabel="Select an existing Webhook to import to the current project.">
+ <MillerColumnsSelect
+ columnCount={1}
+ columnHeight={160}
+ getHasMore={() => !isLast}
+ renderLoading={() => <Loading size={20} style={{ padding: '4px 12px' }} />}
+ items={items}
+ selectedIds={selectedItems.map((it) => it.id)}
+ onSelectItemIds={(seletedIds: ID[]) =>
+ setSelectedItems(
+ items
+ .filter((it) => seletedIds.includes(it.id))
+ .map((it) => ({
+ id: it.id,
+ name: it.name,
+ })),
+ )
+ }
+ />
+ </FormItem>
+ </S.Wrapper>
+ </Dialog>
+ );
+};
+
+export default SelectorDialog;
diff --git a/config-ui/src/plugins/register/webook/components/view-dialog.tsx b/config-ui/src/plugins/register/webook/components/view-dialog.tsx
new file mode 100644
index 000000000..0b7ec81e5
--- /dev/null
+++ b/config-ui/src/plugins/register/webook/components/view-dialog.tsx
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { useState, useEffect, useMemo } from 'react';
+import { Button, Icon, Intent } from '@blueprintjs/core';
+
+import { Dialog, FormItem, CopyText } from '@/components';
+import { operator } from '@/utils';
+
+import * as API from '../api';
+import * as S from '../styled';
+
+interface Props {
+ initialId: ID;
+ onCancel: () => void;
+}
+
+export const ViewDialog = ({ initialId, onCancel }: Props) => {
+ const [record, setRecord] = useState({
+ apiKeyId: '',
+ postIssuesEndpoint: '',
+ closeIssuesEndpoint: '',
+ postDeploymentsCurl: '',
+ });
+ const [operating, setOperating] = useState(false);
+ const [apiKey, setApiKey] = useState('');
+
+ const prefix = useMemo(() => `${window.location.origin}/api`, []);
+
+ useEffect(() => {
+ (async () => {
+ const res = await API.getConnection(initialId);
+ setRecord({
+ apiKeyId: res.apiKey.id,
+ postIssuesEndpoint: `${prefix}${res.postIssuesEndpoint}`,
+ closeIssuesEndpoint: `${prefix}${res.closeIssuesEndpoint}`,
+ postDeploymentsCurl: `curl ${prefix}${res.postPipelineDeployTaskEndpoint} -X 'POST' -d "{
+ \\"commit_sha\\":\\"the sha of deployment commit\\",
+ \\"repo_url\\":\\"the repo URL of the deployment commit\\",
+ \\"start_time\\":\\"Optional, eg. 2020-01-01T12:00:00+00:00\\"
+ }"`,
+ });
+ })();
+ }, [initialId]);
+
+ const handleGenerateNewKey = async () => {
+ const [success, res] = await operator(() => API.renewApiKey(record.apiKeyId), {
+ setOperating,
+ });
+
+ if (success) {
+ setApiKey(res.apiKey);
+ setRecord({
+ ...record,
+ postIssuesEndpoint: `${prefix}${res.postIssuesEndpoint}?api_key=${res.apiKey}`,
+ closeIssuesEndpoint: `${prefix}${res.closeIssuesEndpoint}?api_key=${res.apiKey}`,
+ postDeploymentsCurl: `curl ${prefix}${res.postPipelineDeployTaskEndpoint} -X 'POST' -d "{
+ \\ -H 'Authorization: Bearer ${res.apiKey}'
+ \\ "commit_sha\\":\\"the sha of deployment commit\\",
+ \\ "repo_url\\":\\"the repo URL of the deployment commit\\",
+ \\ "start_time\\":\\"Optional, eg. 2020-01-01T12:00:00+00:00\\"
+ }"`,
+ });
+ }
+ };
+
+ return (
+ <Dialog style={{ width: 820 }} isOpen title="View Webhook" footer={null} onCancel={onCancel}>
+ <S.Wrapper>
+ {apiKey && (
+ <h2>
+ <Icon icon="endorsed" size={30} />
+ <span>POST URL Generated!</span>
+ </h2>
+ )}
+ <p>
+ Copy the following POST URLs to your issue tracking or CI tools to push `Incidents` and `Deployments` by
+ making a POST to DevLake. Please replace the API_key in the following URLs.
+ </p>
+ <FormItem label="Incidents">
+ <h5>Post to register an incident</h5>
+ <CopyText content={record.postIssuesEndpoint} />
+ <h5>Post to close a registered incident</h5>
+ <CopyText content={record.closeIssuesEndpoint} />
+ </FormItem>
+ <FormItem label="Deployment">
+ <h5>Post to register a deployment</h5>
+ <CopyText content={record.postDeploymentsCurl} />
+ </FormItem>
+ <FormItem
+ label="API Key"
+ subLabel="If you have forgotten your API key, you can revoke the previous key and generate a new one as a replacement."
+ >
+ {!apiKey ? (
+ <Button
+ intent={Intent.PRIMARY}
+ loading={operating}
+ text="Revoke and generate a new key"
+ onClick={handleGenerateNewKey}
+ />
+ ) : (
+ <>
+ <S.ApiKey>
+ <CopyText content={apiKey} />
+ <span>No Expiration</span>
+ </S.ApiKey>
+ <S.Tips>
+ <strong>Please copy your key now. You will not be able to see it again.</strong>
+ </S.Tips>
+ </>
+ )}
+ </FormItem>
+ </S.Wrapper>
+ </Dialog>
+ );
+};
diff --git a/config-ui/src/plugins/register/webook/connection/index.tsx b/config-ui/src/plugins/register/webook/connection.tsx
similarity index 66%
rename from config-ui/src/plugins/register/webook/connection/index.tsx
rename to config-ui/src/plugins/register/webook/connection.tsx
index 3f0802e1b..b21707fa6 100644
--- a/config-ui/src/plugins/register/webook/connection/index.tsx
+++ b/config-ui/src/plugins/register/webook/connection.tsx
@@ -17,16 +17,14 @@
*/
import { useState } from 'react';
-import { ButtonGroup, Button, Intent } from '@blueprintjs/core';
+import { Button, Intent } from '@blueprintjs/core';
-import { Table, ColumnType, ExternalLink, IconButton } from '@/components';
+import { Buttons, Table, ColumnType, ExternalLink, IconButton } from '@/components';
import { useConnections } from '@/hooks';
import { DOC_URL } from '@/release';
-import type { WebhookItemType } from '../types';
-import { WebhookCreateDialog } from '../create-dialog';
-import { WebhookDeleteDialog } from '../delete-dialog';
-import { WebhookViewOrEditDialog } from '../view-or-edit-dialog';
+import { CreateDialog, ViewDialog, EditDialog, DeleteDialog } from './components';
+import * as T from './types';
import * as S from './styled';
@@ -42,19 +40,19 @@ export const WebHookConnection = ({ filterIds, onCreateAfter, onDeleteAfter }: P
const [type, setType] = useState<Type>();
const [currentID, setCurrentID] = useState<ID>();
- const { connections, onRefresh } = useConnections({ plugin: 'webhook' });
+ const { connections } = useConnections({ plugin: 'webhook' });
const handleHideDialog = () => {
setType(undefined);
setCurrentID(undefined);
};
- const handleShowDialog = (t: Type, r?: WebhookItemType) => {
+ const handleShowDialog = (t: Type, r?: T.WebhookItemType) => {
setType(t);
setCurrentID(r?.id);
};
- const columns: ColumnType<WebhookItemType> = [
+ const columns: ColumnType<T.WebhookItemType> = [
{
title: 'ID',
dataIndex: 'id',
@@ -73,7 +71,8 @@ export const WebHookConnection = ({ filterIds, onCreateAfter, onDeleteAfter }: P
align: 'center',
render: (_, row) => (
<S.Action>
- <IconButton icon="edit" tooltip="Edit" onClick={() => handleShowDialog('edit', row)} />
+ <IconButton icon="array" tooltip="View" onClick={() => handleShowDialog('show', row)} />
+ <IconButton icon="annotation" tooltip="Edit" onClick={() => handleShowDialog('edit', row)} />
<IconButton icon="trash" tooltip="Delete" onClick={() => handleShowDialog('delete', row)} />
</S.Action>
),
@@ -82,9 +81,9 @@ export const WebHookConnection = ({ filterIds, onCreateAfter, onDeleteAfter }: P
return (
<S.Wrapper>
- <ButtonGroup>
+ <Buttons>
<Button icon="plus" text="Add a Webhook" intent={Intent.PRIMARY} onClick={() => handleShowDialog('add')} />
- </ButtonGroup>
+ </Buttons>
<Table
columns={columns}
dataSource={connections.filter((cs) => (filterIds ? filterIds.includes(cs.id) : true))}
@@ -100,34 +99,12 @@ export const WebHookConnection = ({ filterIds, onCreateAfter, onDeleteAfter }: P
}}
/>
{type === 'add' && (
- <WebhookCreateDialog
- isOpen
- onCancel={handleHideDialog}
- onSubmitAfter={(id) => {
- onRefresh();
- onCreateAfter?.(id);
- }}
- />
+ <CreateDialog isOpen onCancel={handleHideDialog} onSubmitAfter={(id) => onCreateAfter?.(id)} />
)}
+ {type === 'show' && currentID && <ViewDialog initialId={currentID} onCancel={handleHideDialog} />}
+ {type === 'edit' && currentID && <EditDialog initialId={currentID} onCancel={handleHideDialog} />}
{type === 'delete' && currentID && (
- <WebhookDeleteDialog
- isOpen
- initialID={currentID}
- onCancel={handleHideDialog}
- onSubmitAfter={(id) => {
- onRefresh();
- onDeleteAfter?.(id);
- }}
- />
- )}
- {(type === 'edit' || type === 'show') && currentID && (
- <WebhookViewOrEditDialog
- type={type}
- isOpen
- initialID={currentID}
- onCancel={handleHideDialog}
- onSubmitAfter={() => onRefresh()}
- />
+ <DeleteDialog initialId={currentID} onCancel={handleHideDialog} onSubmitAfter={(id) => onDeleteAfter?.(id)} />
)}
</S.Wrapper>
);
diff --git a/config-ui/src/plugins/register/webook/connection/styled.ts b/config-ui/src/plugins/register/webook/connection/styled.ts
deleted file mode 100644
index 83d21157a..000000000
--- a/config-ui/src/plugins/register/webook/connection/styled.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-import styled from 'styled-components';
-
-export const Wrapper = styled.div`
- .bp4-button-group {
- margin-bottom: 16px;
- }
-`;
-
-export const Action = styled.div`
- color: #7497f7;
-
- span {
- cursor: pointer;
- }
-
- span + span {
- margin-left: 8px;
- }
-`;
diff --git a/config-ui/src/plugins/register/webook/delete-dialog/index.tsx b/config-ui/src/plugins/register/webook/delete-dialog/index.tsx
deleted file mode 100644
index 4fbe0433b..000000000
--- a/config-ui/src/plugins/register/webook/delete-dialog/index.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-import { Dialog } from '@/components';
-
-import type { UseDeleteProps } from './use-delete';
-import { useDelete } from './use-delete';
-import * as S from './styled';
-
-interface Props extends UseDeleteProps {
- isOpen: boolean;
- onCancel: () => void;
-}
-
-export const WebhookDeleteDialog = ({ isOpen, onCancel, ...props }: Props) => {
- const { saving, onSubmit } = useDelete({ ...props });
-
- const handleSubmit = () => {
- onSubmit();
- onCancel();
- };
-
- return (
- <Dialog
- isOpen={isOpen}
- title="Delete this Webhook?"
- style={{ width: 600 }}
- okText="Confirm"
- okLoading={saving}
- onCancel={onCancel}
- onOk={handleSubmit}
- >
- <S.Wrapper>
- <div className="message">
- <p>This Webhook cannot be recovered once it’s deleted.</p>
- </div>
- </S.Wrapper>
- </Dialog>
- );
-};
diff --git a/config-ui/src/plugins/register/webook/delete-dialog/styled.ts b/config-ui/src/plugins/register/webook/delete-dialog/styled.ts
deleted file mode 100644
index 4bf74a53c..000000000
--- a/config-ui/src/plugins/register/webook/delete-dialog/styled.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-import styled from 'styled-components';
-
-export const Wrapper = styled.div``;
diff --git a/config-ui/src/plugins/register/webook/index.ts b/config-ui/src/plugins/register/webook/index.ts
index fa62a00e3..3d084a606 100644
--- a/config-ui/src/plugins/register/webook/index.ts
+++ b/config-ui/src/plugins/register/webook/index.ts
@@ -19,7 +19,5 @@
export * from './types';
export * from './config';
export * from './connection';
-export * from './create-dialog';
-export * from './delete-dialog';
-export * from './view-or-edit-dialog';
-export * from './selector-dialog';
+export { default as WebhookCreateDialog } from './components/create-dialog';
+export { default as WebhookSelectorDialog } from './components/selector-dialog';
diff --git a/config-ui/src/plugins/register/webook/selector-dialog/index.tsx b/config-ui/src/plugins/register/webook/selector-dialog/index.tsx
deleted file mode 100644
index 2de2d0ae8..000000000
--- a/config-ui/src/plugins/register/webook/selector-dialog/index.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-import { useState } from 'react';
-
-import { Dialog } from '@/components';
-
-import type { WebhookItemType } from '../types';
-
-import { MillerColumns } from '../components';
-
-import * as S from './styled';
-
-interface Props {
- isOpen: boolean;
- saving: boolean;
- onCancel: () => void;
- onSubmit: (items: WebhookItemType[]) => void;
-}
-
-export const WebhookSelectorDialog = ({ isOpen, saving, onCancel, onSubmit }: Props) => {
- const [selectedItems, setSelectedItems] = useState<WebhookItemType[]>([]);
-
- const handleSubmit = () => onSubmit(selectedItems);
-
- return (
- <Dialog
- isOpen={isOpen}
- title="Select Existing Webhooks"
- style={{
- width: 820,
- }}
- okText="Confrim"
- okLoading={saving}
- okDisabled={!selectedItems.length}
- onCancel={onCancel}
- onOk={handleSubmit}
- >
- <S.Wrapper>
- <h3>Webhooks</h3>
- <p>Select an existing Webhook to import to the current project.</p>
- <MillerColumns selectedItems={selectedItems} onChangeItems={setSelectedItems} />
- </S.Wrapper>
- </Dialog>
- );
-};
diff --git a/config-ui/src/plugins/register/webook/selector-dialog/styled.ts b/config-ui/src/plugins/register/webook/selector-dialog/styled.ts
deleted file mode 100644
index c51774c44..000000000
--- a/config-ui/src/plugins/register/webook/selector-dialog/styled.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-import styled from 'styled-components';
-
-export const Wrapper = styled.div`
- padding: 0 8px;
-
- h3 {
- margin: 8px 0;
- font-size: 14px;
- }
-
- p {
- margin: 0 0 8px;
- font-size: 12px;
- }
-`;
diff --git a/config-ui/src/plugins/register/webook/create-dialog/styled.ts b/config-ui/src/plugins/register/webook/styled.ts
similarity index 76%
rename from config-ui/src/plugins/register/webook/create-dialog/styled.ts
rename to config-ui/src/plugins/register/webook/styled.ts
index 8d4f16106..4c5ad12d1 100644
--- a/config-ui/src/plugins/register/webook/create-dialog/styled.ts
+++ b/config-ui/src/plugins/register/webook/styled.ts
@@ -16,10 +16,10 @@
*
*/
-import styled from 'styled-components';
import { Colors } from '@blueprintjs/core';
+import styled from 'styled-components';
-export const Detail = styled.div`
+export const Wrapper = styled.div`
h2 {
display: flex;
align-items: center;
@@ -39,3 +39,29 @@ export const Detail = styled.div`
margin: 8px 0;
}
`;
+
+export const Action = styled.div`
+ color: #7497f7;
+
+ span {
+ cursor: pointer;
+ }
+
+ span + span {
+ margin-left: 8px;
+ }
+`;
+
+export const ApiKey = styled.div`
+ display: flex;
+ align-items: center;
+
+ & > div {
+ max-width: 50%;
+ margin-right: 8px;
+ }
+`;
+
+export const Tips = styled.div`
+ margin-top: 8px;
+`;
diff --git a/config-ui/src/plugins/register/webook/view-or-edit-dialog/index.tsx b/config-ui/src/plugins/register/webook/view-or-edit-dialog/index.tsx
deleted file mode 100644
index 1b9437b4f..000000000
--- a/config-ui/src/plugins/register/webook/view-or-edit-dialog/index.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-import { InputGroup } from '@blueprintjs/core';
-
-import { Dialog, FormItem, CopyText } from '@/components';
-
-import type { UseViewOrEditProps } from './use-view-or-edit';
-import { useViewOrEdit } from './use-view-or-edit';
-import * as S from './styled';
-
-interface Props extends UseViewOrEditProps {
- type: 'edit' | 'show';
- isOpen: boolean;
- onCancel: () => void;
-}
-
-export const WebhookViewOrEditDialog = ({ type, isOpen, onCancel, ...props }: Props) => {
- const { saving, name, record, onChangeName, onSubmit } = useViewOrEdit({
- ...props,
- });
-
- const handleSubmit = () => {
- if (type === 'edit') {
- onSubmit();
- }
-
- onCancel();
- };
-
- return (
- <Dialog
- isOpen={isOpen}
- title="View/Edit Webhook"
- style={{ width: 820 }}
- okText={type === 'edit' ? 'Save' : 'Done'}
- okDisabled={!name}
- okLoading={saving}
- onCancel={onCancel}
- onOk={handleSubmit}
- >
- <S.Wrapper>
- <h3>Webhook Name *</h3>
- <p>
- Copy the following POST URLs to your issue tracking or CI tools to push `Incidents` and `Deployments` by
- making a POST to DevLake.
- </p>
- <InputGroup disabled={type !== 'edit'} value={name} onChange={(e) => onChangeName(e.target.value)} />
- <p>
- Copy the following URLs to your issue tracking tool for Incidents and CI tool for Deployments by making a POST
- to DevLake.
- </p>
- <FormItem label="Incidents">
- <h5>Post to register an incident</h5>
- <CopyText content={record.postIssuesEndpoint} />
- <h5>Post to close a registered incident</h5>
- <CopyText content={record.closeIssuesEndpoint} />
- </FormItem>
- <FormItem label="Deployment">
- <h5>Post to register a deployment</h5>
- <CopyText content={record.postDeploymentsCurl} />
- </FormItem>
- </S.Wrapper>
- </Dialog>
- );
-};
diff --git a/config-ui/src/plugins/register/webook/view-or-edit-dialog/styled.ts b/config-ui/src/plugins/register/webook/view-or-edit-dialog/styled.ts
deleted file mode 100644
index 969380e28..000000000
--- a/config-ui/src/plugins/register/webook/view-or-edit-dialog/styled.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-import styled from 'styled-components';
-
-export const Wrapper = styled.div`
- h5 {
- margin: 8px 0;
- }
-
- .block {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 8px;
- padding: 10px 16px;
- background: #f0f4fe;
- }
-`;
diff --git a/config-ui/src/plugins/register/webook/view-or-edit-dialog/use-view-or-edit.ts b/config-ui/src/plugins/register/webook/view-or-edit-dialog/use-view-or-edit.ts
deleted file mode 100644
index e7d14b18c..000000000
--- a/config-ui/src/plugins/register/webook/view-or-edit-dialog/use-view-or-edit.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-import { useState, useMemo, useEffect } from 'react';
-
-import { operator } from '@/utils';
-
-import * as API from '../api';
-
-export interface UseViewOrEditProps {
- initialID: ID;
- onSubmitAfter?: (id: ID) => void;
-}
-
-export const useViewOrEdit = ({ initialID, onSubmitAfter }: UseViewOrEditProps) => {
- const [saving, setSaving] = useState(false);
- const [name, setName] = useState('');
- const [record, setRecord] = useState({
- postIssuesEndpoint: '',
- closeIssuesEndpoint: '',
- postDeploymentsCurl: '',
- });
-
- const prefix = useMemo(() => `${window.location.origin}/api`, []);
-
- useEffect(() => {
- (async () => {
- const res = await API.getConnection(initialID);
- setName(res.name);
- setRecord({
- postIssuesEndpoint: `${prefix}${res.postIssuesEndpoint}`,
- closeIssuesEndpoint: `${prefix}${res.closeIssuesEndpoint}`,
- postDeploymentsCurl: `curl ${prefix}${res.postPipelineDeployTaskEndpoint} -X 'POST' -d "{
- \\"commit_sha\\":\\"the sha of deployment commit\\",
- \\"repo_url\\":\\"the repo URL of the deployment commit\\",
- \\"start_time\\":\\"Optional, eg. 2020-01-01T12:00:00+00:00\\"
- }"`,
- });
- })();
- }, [initialID]);
-
- const handleUpdate = async () => {
- const [success] = await operator(() => API.updateConnection(initialID, { name }), {
- setOperating: setSaving,
- });
-
- if (success) {
- onSubmitAfter?.(initialID);
- }
- };
-
- return useMemo(
- () => ({
- saving,
- name,
- record,
- onChangeName: setName,
- onSubmit: handleUpdate,
- }),
- [saving, name, record],
- );
-};