From f9b3d145416e2bbb8398ea95e5c34cd92a530ac1 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 16 Sep 2020 12:45:22 +0200 Subject: [PATCH] add tag overview for repository --- scm-ui/ui-types/src/Tags.ts | 1 + scm-ui/ui-webapp/public/locales/de/repos.json | 14 +++ scm-ui/ui-webapp/public/locales/en/repos.json | 14 +++ .../src/repos/containers/RepositoryRoot.tsx | 27 +++++ .../src/repos/tags/components/TagDetail.tsx | 49 ++++++++++ .../src/repos/tags/components/TagRow.tsx | 60 ++++++++++++ .../src/repos/tags/components/TagTable.tsx | 60 ++++++++++++ .../src/repos/tags/components/TagView.tsx | 54 ++++++++++ .../src/repos/tags/container/TagRoot.tsx | 98 +++++++++++++++++++ .../src/repos/tags/container/TagsOverview.tsx | 80 +++++++++++++++ .../src/repos/tags/orderTags.test.ts | 52 ++++++++++ scm-ui/ui-webapp/src/repos/tags/orderTags.ts | 32 ++++++ 12 files changed, 541 insertions(+) create mode 100644 scm-ui/ui-webapp/src/repos/tags/components/TagDetail.tsx create mode 100644 scm-ui/ui-webapp/src/repos/tags/components/TagRow.tsx create mode 100644 scm-ui/ui-webapp/src/repos/tags/components/TagTable.tsx create mode 100644 scm-ui/ui-webapp/src/repos/tags/components/TagView.tsx create mode 100644 scm-ui/ui-webapp/src/repos/tags/container/TagRoot.tsx create mode 100644 scm-ui/ui-webapp/src/repos/tags/container/TagsOverview.tsx create mode 100644 scm-ui/ui-webapp/src/repos/tags/orderTags.test.ts create mode 100644 scm-ui/ui-webapp/src/repos/tags/orderTags.ts diff --git a/scm-ui/ui-types/src/Tags.ts b/scm-ui/ui-types/src/Tags.ts index 6fc1973150..83ed4c3860 100644 --- a/scm-ui/ui-types/src/Tags.ts +++ b/scm-ui/ui-types/src/Tags.ts @@ -27,5 +27,6 @@ import { Links } from "./hal"; export type Tag = { name: string; revision: string; + date: Date; _links: Links; }; diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index 5b3e74f701..0b63dc7c9e 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -31,6 +31,7 @@ "navigationLabel": "Repository", "informationNavLink": "Informationen", "branchesNavLink": "Branches", + "tagsNavLink": "Tags", "sourcesNavLink": "Code", "settingsNavLink": "Einstellungen", "generalNavLink": "Generell", @@ -71,6 +72,19 @@ "sources": "Sources", "defaultTag": "Default" }, + "tags": { + "overview": { + "title": "Übersicht aller verfügbaren Tags", + "noTags": "Keine Tags gefunden.", + "created": "Erstellt" + }, + "table": { + "tags": "Tags" + } + }, + "tag": { + "name": "Name:" + }, "code": { "sources": "Sources", "commits": "Commits", diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index eeab9a9dda..033afe4ffd 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -31,6 +31,7 @@ "navigationLabel": "Repository", "informationNavLink": "Information", "branchesNavLink": "Branches", + "tagsNavLink": "Tags", "sourcesNavLink": "Code", "settingsNavLink": "Settings", "generalNavLink": "General", @@ -71,6 +72,19 @@ "sources": "Sources", "defaultTag": "Default" }, + "tags": { + "overview": { + "title": "Overview of all tags", + "noTags": "No tags found.", + "created": "Created" + }, + "table": { + "tags": "Tags" + } + }, + "tag": { + "name": "Name:" + }, "code": { "sources": "Sources", "commits": "Commits", diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index bcbb217fed..f3ef73bf36 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -54,6 +54,8 @@ import CodeOverview from "../codeSection/containers/CodeOverview"; import ChangesetView from "./ChangesetView"; import SourceExtensions from "../sources/containers/SourceExtensions"; import { FileControlFactory, JumpToFileButton } from "@scm-manager/ui-components"; +import TagsOverview from "../tags/container/TagsOverview"; +import TagRoot from "../tags/container/TagRoot"; type Props = RouteComponentProps & WithTranslation & { @@ -99,6 +101,12 @@ class RepositoryRoot extends React.Component { return route.location.pathname.match(regex); }; + matchesTags = (route: any) => { + const url = this.matchedUrl(); + const regex = new RegExp(`${url}/tag/.+/info`); + return route.location.pathname.match(regex); + }; + matchesCode = (route: any) => { const url = this.matchedUrl(); const regex = new RegExp(`${url}(/code)/.*`); @@ -245,6 +253,15 @@ class RepositoryRoot extends React.Component { render={() => } /> } /> + } + /> + } + /> @@ -267,6 +284,16 @@ class RepositoryRoot extends React.Component { activeOnlyWhenExact={false} title={t("repositoryRoot.menu.branchesNavLink")} /> + = ({ tag }) => { + const [t] = useTranslation("repos"); + + if (!tag) { + return null; + } + + return ( +
+
+ {t("tag.name")} {tag?.name} +
+
+ ); +}; + +export default TagDetail; diff --git a/scm-ui/ui-webapp/src/repos/tags/components/TagRow.tsx b/scm-ui/ui-webapp/src/repos/tags/components/TagRow.tsx new file mode 100644 index 0000000000..00b18da623 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/tags/components/TagRow.tsx @@ -0,0 +1,60 @@ +/* + * 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 } from "react"; +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; +import { Tag } from "@scm-manager/ui-types"; +import styled from "styled-components"; +import { DateFromNow } from "@scm-manager/ui-components"; + +type Props = { + tag: Tag; + baseUrl: string; +}; + +const Created = styled.span` + margin-left: 1rem; + font-size: 0.8rem; +`; + +const TagRow: FC = ({ tag, baseUrl }) => { + const [t] = useTranslation("repos"); + + const to = `${baseUrl}/${encodeURIComponent(tag.name)}/info`; + return ( + + + + {tag.name} + + {t("tags.overview.created")} + + + + + ); +}; + +export default TagRow; diff --git a/scm-ui/ui-webapp/src/repos/tags/components/TagTable.tsx b/scm-ui/ui-webapp/src/repos/tags/components/TagTable.tsx new file mode 100644 index 0000000000..4cd694766f --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/tags/components/TagTable.tsx @@ -0,0 +1,60 @@ +/* + * 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 } from "react"; +import { Tag } from "@scm-manager/ui-types"; +import { useTranslation } from "react-i18next"; +import TagRow from "./TagRow"; + +type Props = { + baseUrl: string; + tags: Tag[]; +}; + +const TagTable: FC = ({ baseUrl, tags }) => { + const [t] = useTranslation("repos"); + + const renderRow = () => { + let rowContent = null; + if (tags) { + rowContent = tags.map((tag, index) => { + return ; + }); + } + return rowContent; + }; + + return ( + + + + + + + {renderRow()} +
{t("tags.table.tags")}
+ ); +}; + +export default TagTable; diff --git a/scm-ui/ui-webapp/src/repos/tags/components/TagView.tsx b/scm-ui/ui-webapp/src/repos/tags/components/TagView.tsx new file mode 100644 index 0000000000..82ce697bc1 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/tags/components/TagView.tsx @@ -0,0 +1,54 @@ +/* + * 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 } from "react"; +import { Repository, Tag } from "@scm-manager/ui-types"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import TagDetail from "./TagDetail"; + +type Props = { + repository: Repository; + tag?: Tag; +}; + +const TagView: FC = ({ repository, tag }) => { + return ( +
+ +
+
+ +
+
+ ); +}; + +export default TagView; diff --git a/scm-ui/ui-webapp/src/repos/tags/container/TagRoot.tsx b/scm-ui/ui-webapp/src/repos/tags/container/TagRoot.tsx new file mode 100644 index 0000000000..55f45f01c4 --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/tags/container/TagRoot.tsx @@ -0,0 +1,98 @@ +/* + * 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, useEffect, useState } from "react"; +import { Link, Repository, Tag } from "@scm-manager/ui-types"; +import { Redirect, Switch, useLocation, useRouteMatch, Route } from "react-router-dom"; +import { apiClient, ErrorNotification, Loading } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; +import TagView from "../components/TagView"; + +type Props = { + repository: Repository; + baseUrl: string; +}; + +const TagRoot: FC = ({ repository, baseUrl }) => { + const match = useRouteMatch(); + const [tags, setTags] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(undefined); + const [tag, setTag] = useState(); + + useEffect(() => { + const link = (repository._links?.tags as Link)?.href; + if (link) { + setLoading(true); + apiClient + .get(link) + .then(r => r.json()) + .then(r => setTags(r._embedded.tags)) + .catch(setError); + } + }, [repository]); + + useEffect(() => { + const tagName = match?.params?.tag; + const link = tags && tags.length > 0 && (tags.find(tag => tag.name === tagName)?._links.self as Link).href; + if (link) { + apiClient + .get(link) + .then(r => r.json()) + .then(setTag) + .then(() => setLoading(false)) + .catch(setError); + } + }, [tags]); + + const stripEndingSlash = (url: string) => { + if (url.endsWith("/")) { + return url.substring(0, url.length - 1); + } + return url; + }; + + const matchedUrl = () => { + return stripEndingSlash(match.url); + }; + + if (error) { + return ; + } + + if (loading || !tags) { + return ; + } + + const url = matchedUrl(); + + return ( + + + } /> + + ); +}; + +export default TagRoot; diff --git a/scm-ui/ui-webapp/src/repos/tags/container/TagsOverview.tsx b/scm-ui/ui-webapp/src/repos/tags/container/TagsOverview.tsx new file mode 100644 index 0000000000..35b771ae1a --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/tags/container/TagsOverview.tsx @@ -0,0 +1,80 @@ +/* + * 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, useEffect, useState } from "react"; +import { Repository, Tag, Link } from "@scm-manager/ui-types"; +import { ErrorNotification, Loading, Notification, Subtitle, apiClient } from "@scm-manager/ui-components"; +import { useTranslation } from "react-i18next"; +import orderTags from "../orderTags"; +import TagTable from "../components/TagTable"; + +type Props = { + repository: Repository; + baseUrl: string; +}; + +const TagsOverview: FC = ({ repository, baseUrl }) => { + const [t] = useTranslation("repos"); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(undefined); + const [tags, setTags] = useState([]); + + useEffect(() => { + const link = (repository._links?.tags as Link)?.href; + if (link) { + setLoading(true); + apiClient + .get(link) + .then(r => r.json()) + .then(r => setTags(r._embedded.tags)) + .then(() => setLoading(false)) + .catch(setError); + } + }, [repository]); + + const renderTagsTable = () => { + if (!loading && tags && tags.length > 0) { + orderTags(tags); + return ; + } + return {t("tags.overview.noTags")}; + }; + + if (error) { + return ; + } + + if (loading) { + return ; + } + + return ( + <> + + {renderTagsTable()} + + ); +}; + +export default TagsOverview; diff --git a/scm-ui/ui-webapp/src/repos/tags/orderTags.test.ts b/scm-ui/ui-webapp/src/repos/tags/orderTags.test.ts new file mode 100644 index 0000000000..3b8667e21f --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/tags/orderTags.test.ts @@ -0,0 +1,52 @@ +/* + * 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 orderTags from "./orderTags"; + +const tag1 = { + name: "tag1", + revision: "revision1", + date: new Date(2020, 1, 1), + _links: {} +}; +const tag2 = { + name: "tag2", + revision: "revision2", + date: new Date(2020, 1, 3), + _links: {} +}; +const tag3 = { + name: "tag3", + revision: "revision3", + date: new Date(2020, 1, 2), + _links: {} +}; + +describe("order tags", () => { + it("should order tags descending by date", () => { + const tags = [tag1, tag2, tag3]; + orderTags(tags); + expect(tags).toEqual([tag2, tag3, tag1]); + }); +}); diff --git a/scm-ui/ui-webapp/src/repos/tags/orderTags.ts b/scm-ui/ui-webapp/src/repos/tags/orderTags.ts new file mode 100644 index 0000000000..87697c260a --- /dev/null +++ b/scm-ui/ui-webapp/src/repos/tags/orderTags.ts @@ -0,0 +1,32 @@ +/* + * 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. + */ + +// sort tags by date beginning with latest first +import { Tag } from "@scm-manager/ui-types"; + +export default function orderTags(tags: Tag[]) { + tags.sort((a, b) => { + return new Date(b.date) - new Date(a.date); + }); +}