From 0201673cebc3a7c92a839fc8ceec2e6d689a751d Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 19 Apr 2023 11:56:29 +0200 Subject: [PATCH] Refactor webhook overview layout to master details page Co-authored-by: Konstantin Schaper --- build.gradle | 2 +- gradle/changelog/masterdetailsview.yaml | 2 + package.json | 18 +- src/main/js/CreatePage.tsx | 68 +++ src/main/js/EditPage.tsx | 69 +++ src/main/js/GlobalWebhookConfiguration.tsx | 7 +- src/main/js/MasterDetailsView.tsx | 136 +++++ src/main/js/Overview.tsx | 131 +++++ src/main/js/PrimaryInformation.tsx | 31 + .../js/RepositoryWebhookConfiguration.tsx | 14 +- src/main/js/SecondaryInformation.tsx | 31 + .../js/SimpleWebHookConfigurationForm.tsx | 4 +- .../js/SimpleWebhookOverviewCardBottom.tsx | 40 ++ src/main/js/SimpleWebhookOverviewCardTop.tsx | 45 ++ src/main/js/WebHookConfiguration.tsx | 105 ---- src/main/js/extensionPoints.ts | 4 +- src/main/js/index.ts | 10 +- src/main/js/types.ts | 9 +- src/main/resources/locales/de/plugins.json | 25 +- src/main/resources/locales/en/plugins.json | 25 +- yarn.lock | 528 ++++++++++++++++-- 21 files changed, 1116 insertions(+), 188 deletions(-) create mode 100644 gradle/changelog/masterdetailsview.yaml create mode 100644 src/main/js/CreatePage.tsx create mode 100644 src/main/js/EditPage.tsx create mode 100644 src/main/js/MasterDetailsView.tsx create mode 100644 src/main/js/Overview.tsx create mode 100644 src/main/js/PrimaryInformation.tsx create mode 100644 src/main/js/SecondaryInformation.tsx create mode 100644 src/main/js/SimpleWebhookOverviewCardBottom.tsx create mode 100644 src/main/js/SimpleWebhookOverviewCardTop.tsx delete mode 100644 src/main/js/WebHookConfiguration.tsx diff --git a/build.gradle b/build.gradle index bfde086..0a6c49f 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ dependencies { } scmPlugin { - scmVersion = "2.42.3-SNAPSHOT" + scmVersion = "2.43.1-SNAPSHOT" displayName = "Webhook" description = "Notifies a remote webserver whenever a repository is pushed to" author = "Cloudogu GmbH" diff --git a/gradle/changelog/masterdetailsview.yaml b/gradle/changelog/masterdetailsview.yaml new file mode 100644 index 0000000..7d61a4e --- /dev/null +++ b/gradle/changelog/masterdetailsview.yaml @@ -0,0 +1,2 @@ +- type: changed + description: BREAKING - Change webhooks layout to master details multi-page design diff --git a/package.json b/package.json index 378ead6..0e1074d 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,9 @@ "postinstall": "plugin-scripts postinstall" }, "dependencies": { - "@scm-manager/ui-plugins": "2.41.1-20230101-071950", - "@scm-manager/ui-components": "2.41.1-20230101-071950", - "@scm-manager/ui-extensions": "2.41.1-20230101-071950", + "@scm-manager/ui-plugins": "^2.43.1-20230318-085802", + "@scm-manager/ui-components": "2.43.1-20230318-085802", + "@scm-manager/ui-extensions": "2.43.1-20230318-085802", "classnames": "^2.2.6", "query-string": "6.14.1", "react": "^17.0.1", @@ -22,8 +22,8 @@ "redux": "^4.0.0", "styled-components": "^5.3.5", "react-hook-form": "^7.5.1", - "@scm-manager/ui-forms": "2.41.1-20230101-071950", - "@scm-manager/ui-buttons": "2.41.1-20230101-071950", + "@scm-manager/ui-forms": "2.43.1-20230318-085802", + "@scm-manager/ui-buttons": "2.43.1-20230318-085802", "react-router": "^5.3.1" }, "babel": { @@ -44,9 +44,9 @@ "@scm-manager/jest-preset": "^2.13.0", "@scm-manager/prettier-config": "^2.10.1", "@scm-manager/tsconfig": "^2.13.0", - "@scm-manager/ui-scripts": "2.41.1-20230101-071950", - "@scm-manager/ui-tests": "2.41.1-20230101-071950", - "@scm-manager/ui-types": "2.41.1-20230101-071950", + "@scm-manager/ui-scripts": "2.43.1-20230318-085802", + "@scm-manager/ui-tests": "2.43.1-20230318-085802", + "@scm-manager/ui-types": "2.43.1-20230318-085802", "@types/classnames": "^2.2.9", "@types/enzyme": "^3.10.3", "@types/fetch-mock": "^7.3.1", @@ -60,4 +60,4 @@ "jest": "^24.9.0", "@scm-manager/plugin-scripts": "^1.3.0" } -} +} \ No newline at end of file diff --git a/src/main/js/CreatePage.tsx b/src/main/js/CreatePage.tsx new file mode 100644 index 0000000..b997dca --- /dev/null +++ b/src/main/js/CreatePage.tsx @@ -0,0 +1,68 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { Form } from "@scm-manager/ui-forms"; +import React, { FC, useMemo, useState } from "react"; +import { WebhookConfiguration } from "./extensionPoints"; +import { SelectField } from "@scm-manager/ui-forms"; +import { useHistory } from "react-router-dom"; +import { Subtitle } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; + +type Props = { + webhookMap: Record; + typeOptions: any; + onCreate: (name: string, configuration: unknown) => Promise; + baseRoute: string; +}; + +const CreatePage: FC = ({ webhookMap, onCreate, typeOptions, baseRoute }) => { + const [t] = useTranslation("plugins"); + const [type, setType] = useState(""); + const extension = useMemo(() => webhookMap[type], [type, webhookMap]); + const history = useHistory(); + + const options = useMemo(() => [{ label: "", value: "" }, ...typeOptions], [typeOptions]); + + return ( + <> + {t("scm-webhook-plugin.config.createSubtitle")} + setType(event.target.value)} options={options} /> + {type ? ( + <> +
+
onCreate(type, formValue).then(() => history.push(baseRoute))} + defaultValues={extension.defaultConfiguration} + translationPath={["plugins", "scm-webhook-plugin.config.form.webhooks.configuration"]} + > + {({ watch }) => React.createElement(extension.FormComponent, { webhook: watch() })} +
+ + ) : null} + + ); +}; + +export default CreatePage; diff --git a/src/main/js/EditPage.tsx b/src/main/js/EditPage.tsx new file mode 100644 index 0000000..62e1bbf --- /dev/null +++ b/src/main/js/EditPage.tsx @@ -0,0 +1,69 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { Form } from "@scm-manager/ui-forms"; +import React, { FC, useMemo } from "react"; +import { WebhookConfiguration } from "./extensionPoints"; +import { WebHookConfiguration } from "./types"; +import { useHistory, useParams } from "react-router-dom"; +import { Subtitle } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; +import { Button } from "@scm-manager/ui-buttons"; + +type Props = { + webhookMap: Record; + webhooks: WebHookConfiguration[]; + onUpdate: (webhook: WebHookConfiguration) => Promise; + onDelete: (webhook: WebHookConfiguration) => Promise; + baseRoute: string; +}; + +const EditPage: FC = ({ webhookMap, webhooks, onUpdate, onDelete, baseRoute }) => { + const [t] = useTranslation("plugins"); + const { id } = useParams<{ id: string }>(); + const webhook = useMemo(() => webhooks.find(wh => wh.id === id), [id, webhooks]); + const extension = useMemo(() => webhookMap[webhook.name], [webhook.name, webhookMap]); + const history = useHistory(); + + return ( + <> + {t("scm-webhook-plugin.config.editSubtitle", { name: webhook.name})} +
onUpdate({ ...webhook, configuration: formValue }).then(() => history.push(baseRoute))} + defaultValues={webhook.configuration} + translationPath={["plugins", "scm-webhook-plugin.config.form.webhooks.configuration"]} + > + {({ watch }) => React.createElement(extension.FormComponent, { webhook: watch() })} +
+
+
+ +
+ + ); +}; + +export default EditPage; diff --git a/src/main/js/GlobalWebhookConfiguration.tsx b/src/main/js/GlobalWebhookConfiguration.tsx index 3c940c8..1458b2c 100644 --- a/src/main/js/GlobalWebhookConfiguration.tsx +++ b/src/main/js/GlobalWebhookConfiguration.tsx @@ -22,7 +22,7 @@ * SOFTWARE. */ import React, { FC } from "react"; -import WebHookConfigurationForm from "./WebHookConfiguration"; +import MasterDetailsView from "./MasterDetailsView"; import { useTranslation } from "react-i18next"; type Props = { @@ -31,8 +31,9 @@ type Props = { const GlobalWebhookConfiguration: FC = props => { const [t] = useTranslation("plugins"); - - return ; + return ( + + ); }; export default GlobalWebhookConfiguration; diff --git a/src/main/js/MasterDetailsView.tsx b/src/main/js/MasterDetailsView.tsx new file mode 100644 index 0000000..eeea07a --- /dev/null +++ b/src/main/js/MasterDetailsView.tsx @@ -0,0 +1,136 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React, { FC, useCallback, useMemo } from "react"; +import { Route, Switch } from "react-router-dom"; +import Overview from "./Overview"; +import CreatePage from "./CreatePage"; +import { useBinder } from "@scm-manager/ui-extensions"; +import { WebhookConfiguration } from "./extensionPoints"; +import { Repository } from "@scm-manager/ui-types"; +import { useTranslation } from "react-i18next"; +import { useConfigLink } from "@scm-manager/ui-api"; +import { WebHookConfiguration, WebHookConfigurations } from "./types"; +import { Loading, Subtitle, Title } from "@scm-manager/ui-components"; +import EditPage from "./EditPage"; + +type Props = { + baseRoute: string; + link: string; + title?: string; + repository?: Repository; +}; + +const MasterDetailsView: FC = ({ repository, link, title, baseRoute }) => { + const [t] = useTranslation("plugins"); + const binder = useBinder(); + const { initialConfiguration, isReadOnly, update, isLoading, isUpdating } = useConfigLink( + link + ); + const allWebHooks = binder.getExtensions("webhook.configuration"); + const create = useCallback( + (name: string, configuration: unknown) => { + const newWebhook = { + name, + configuration + } as WebHookConfiguration; + initialConfiguration.webhooks.push(newWebhook); + return update(initialConfiguration); + }, + [initialConfiguration, update] + ); + + const updateWebhook = useCallback( + (webhook: WebHookConfiguration) => { + initialConfiguration.webhooks[initialConfiguration.webhooks.findIndex(wh => wh.id === webhook.id)] = webhook; + return update(initialConfiguration); + }, + [initialConfiguration, update] + ); + + const deleteWebhook = useCallback( + (webhook: WebHookConfiguration) => { + initialConfiguration.webhooks.splice( + initialConfiguration.webhooks.findIndex(wh => wh.id === webhook.id), + 1 + ); + return update(initialConfiguration); + }, + [initialConfiguration, update] + ); + + const webhookMap = useMemo>( + () => + allWebHooks.reduce((prev, cur) => { + prev[cur.name] = cur; + return prev; + }, {}), + [allWebHooks] + ); + + const typeOptions = useMemo( + () => + (repository?._embedded?.supportedWebHookTypes.types ?? allWebHooks.map(({ name }) => name)).map(name => ({ + value: name, + label: t(`webhooks.${name}.name`) + })), + [allWebHooks, repository, t] + ); + + if (isLoading || isUpdating) { + return ; + } + + return ( + <> + {title ? {title} : null} + + + + + + + + + + + + + ); +}; + +export default MasterDetailsView; diff --git a/src/main/js/Overview.tsx b/src/main/js/Overview.tsx new file mode 100644 index 0000000..68bd19d --- /dev/null +++ b/src/main/js/Overview.tsx @@ -0,0 +1,131 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import React, { FC, useMemo, useState } from "react"; +import { Notification, Subtitle } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; +import { WebhookConfiguration } from "./extensionPoints"; +import PrimaryInformation from "./PrimaryInformation"; +import { WebHookConfiguration } from "./types"; +import { Select } from "@scm-manager/ui-forms"; +import { Icon, LinkButton } from "@scm-manager/ui-buttons"; +import { Menu } from "@scm-manager/ui-overlays"; +import { Repository } from "@scm-manager/ui-types"; + +type Props = { + repository?: Repository; + webhooks: WebHookConfiguration[]; + webhookMap: Record; + typeOptions: any; + isReadOnly?: boolean; + onDelete: (webhook: WebHookConfiguration) => Promise; +}; + +const Overview: FC = ({ repository, webhooks, webhookMap, typeOptions, isReadOnly, onDelete }) => { + const [t] = useTranslation("plugins"); + const [type, setType] = useState("ALL"); + const filteredWebhooks = useMemo( + () => (type === "ALL" ? webhooks : webhooks.filter(webhook => webhook.name === type)), + [type, webhooks] + ); + + const options = useMemo( + () => [{ label: t("scm-webhook-plugin.config.filter.all"), value: "ALL" }, ...typeOptions], + [typeOptions] + ); + + return ( + <> + {repository ? {t("scm-webhook-plugin.config.title")} : null} +
+
+ {t("scm-webhook-plugin.config.filter.label")} +