Skip to content

Commit

Permalink
Refactor webhook overview layout to master details page
Browse files Browse the repository at this point in the history
Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
  • Loading branch information
Eduard Heimbuch and Pilopa committed Apr 20, 2023
1 parent eb31592 commit 0201673
Show file tree
Hide file tree
Showing 21 changed files with 1,116 additions and 188 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions gradle/changelog/masterdetailsview.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- type: changed
description: BREAKING - Change webhooks layout to master details multi-page design
18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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": {
Expand All @@ -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",
Expand All @@ -60,4 +60,4 @@
"jest": "^24.9.0",
"@scm-manager/plugin-scripts": "^1.3.0"
}
}
}
68 changes: 68 additions & 0 deletions src/main/js/CreatePage.tsx
Original file line number Diff line number Diff line change
@@ -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<string, WebhookConfiguration["type"]>;
typeOptions: any;
onCreate: (name: string, configuration: unknown) => Promise<unknown>;
baseRoute: string;
};

const CreatePage: FC<Props> = ({ 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 (
<>
<Subtitle>{t("scm-webhook-plugin.config.createSubtitle")}</Subtitle>
<SelectField label={t("scm-webhook-plugin.config.type.label")} value={type} onChange={event => setType(event.target.value)} options={options} />
{type ? (
<>
<hr />
<Form
onSubmit={formValue => 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() })}
</Form>
</>
) : null}
</>
);
};

export default CreatePage;
69 changes: 69 additions & 0 deletions src/main/js/EditPage.tsx
Original file line number Diff line number Diff line change
@@ -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<string, WebhookConfiguration["type"]>;
webhooks: WebHookConfiguration[];
onUpdate: (webhook: WebHookConfiguration) => Promise<unknown>;
onDelete: (webhook: WebHookConfiguration) => Promise<unknown>;
baseRoute: string;
};

const EditPage: FC<Props> = ({ 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 (
<>
<Subtitle>{t("scm-webhook-plugin.config.editSubtitle", { name: webhook.name})}</Subtitle>
<Form
onSubmit={formValue => 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() })}
</Form>
<hr/>
<div className="level-right">
<Button variant="signal" onClick={() => onDelete(webhook).then(() => history.push(baseRoute))}>
{t("Delete webhook")}
</Button>
</div>
</>
);
};

export default EditPage;
7 changes: 4 additions & 3 deletions src/main/js/GlobalWebhookConfiguration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -31,8 +31,9 @@ type Props = {

const GlobalWebhookConfiguration: FC<Props> = props => {
const [t] = useTranslation("plugins");

return <WebHookConfigurationForm title={t("scm-webhook-plugin.config.header")} {...props} />;
return (
<MasterDetailsView title={t("scm-webhook-plugin.config.title")} baseRoute="/admin/settings/webhook" {...props} />
);
};

export default GlobalWebhookConfiguration;
136 changes: 136 additions & 0 deletions src/main/js/MasterDetailsView.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ repository, link, title, baseRoute }) => {
const [t] = useTranslation("plugins");
const binder = useBinder();
const { initialConfiguration, isReadOnly, update, isLoading, isUpdating } = useConfigLink<WebHookConfigurations>(
link
);
const allWebHooks = binder.getExtensions<WebhookConfiguration>("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<Record<string, WebhookConfiguration["type"]>>(
() =>
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 <Loading />;
}

return (
<>
{title ? <Title>{title}</Title> : null}
<Switch>
<Route path={`${baseRoute}/add`}>
<CreatePage webhookMap={webhookMap} typeOptions={typeOptions} onCreate={create} baseRoute={baseRoute} />
</Route>
<Route path={`${baseRoute}/:id`}>
<EditPage
webhooks={initialConfiguration.webhooks}
webhookMap={webhookMap}
onUpdate={updateWebhook}
baseRoute={baseRoute}
onDelete={deleteWebhook}
/>
</Route>
<Route path={baseRoute} exact>
<Overview
repository={repository}
webhooks={initialConfiguration.webhooks}
webhookMap={webhookMap}
typeOptions={typeOptions}
isReadOnly={isReadOnly}
onDelete={deleteWebhook}
/>
</Route>
</Switch>
</>
);
};

export default MasterDetailsView;
Loading

0 comments on commit 0201673

Please sign in to comment.