add tag overview for repository

This commit is contained in:
Eduard Heimbuch
2020-09-16 12:45:22 +02:00
parent 6454167b0d
commit f9b3d14541
12 changed files with 541 additions and 0 deletions

View File

@@ -27,5 +27,6 @@ import { Links } from "./hal";
export type Tag = {
name: string;
revision: string;
date: Date;
_links: Links;
};

View File

@@ -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",

View File

@@ -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",

View File

@@ -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<Props> {
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<Props> {
render={() => <BranchesOverview repository={repository} baseUrl={`${url}/branch`} />}
/>
<Route path={`${url}/branches/create`} render={() => <CreateBranch repository={repository} />} />
<Route
path={`${url}/tag/:tag`}
render={() => <TagRoot repository={repository} baseUrl={`${url}/tag`} />}
/>
<Route
path={`${url}/tags`}
exact={true}
render={() => <TagsOverview repository={repository} baseUrl={`${url}/tag`} />}
/>
<ExtensionPoint name="repository.route" props={extensionProps} renderAll={true} />
</Switch>
</PrimaryContentColumn>
@@ -267,6 +284,16 @@ class RepositoryRoot extends React.Component<Props> {
activeOnlyWhenExact={false}
title={t("repositoryRoot.menu.branchesNavLink")}
/>
<RepositoryNavLink
repository={repository}
linkName="tags"
to={`${url}/tags/`}
icon="fas fa-tags"
label={t("repositoryRoot.menu.tagsNavLink")}
activeWhenMatch={this.matchesTags}
activeOnlyWhenExact={false}
title={t("repositoryRoot.menu.tagsNavLink")}
/>
<RepositoryNavLink
repository={repository}
linkName={this.getCodeLinkname()}

View File

@@ -0,0 +1,49 @@
/*
* 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 { Tag } from "@scm-manager/ui-types";
type Props = {
tag?: Tag;
};
const TagDetail: FC<Props> = ({ tag }) => {
const [t] = useTranslation("repos");
if (!tag) {
return null;
}
return (
<div className="media">
<div className="media-content subtitle">
<strong>{t("tag.name")}</strong> {tag?.name}
</div>
</div>
);
};
export default TagDetail;

View File

@@ -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<Props> = ({ tag, baseUrl }) => {
const [t] = useTranslation("repos");
const to = `${baseUrl}/${encodeURIComponent(tag.name)}/info`;
return (
<tr>
<td>
<Link to={to} title={tag.name}>
{tag.name}
<Created className="has-text-grey is-ellipsis-overflow">
{t("tags.overview.created")} <DateFromNow date={tag.date} />
</Created>
</Link>
</td>
</tr>
);
};
export default TagRow;

View File

@@ -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<Props> = ({ baseUrl, tags }) => {
const [t] = useTranslation("repos");
const renderRow = () => {
let rowContent = null;
if (tags) {
rowContent = tags.map((tag, index) => {
return <TagRow key={index} baseUrl={baseUrl} tag={tag} />;
});
}
return rowContent;
};
return (
<table className="card-table table is-hoverable is-fullwidth is-word-break">
<thead>
<tr>
<th>{t("tags.table.tags")}</th>
</tr>
</thead>
<tbody>{renderRow()}</tbody>
</table>
);
};
export default TagTable;

View File

@@ -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<Props> = ({ repository, tag }) => {
return (
<div>
<TagDetail tag={tag} />
<hr />
<div className="content">
<ExtensionPoint
name="repos.tag-details.information"
renderAll={true}
props={{
repository,
tag
}}
/>
</div>
</div>
);
};
export default TagView;

View File

@@ -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<Props> = ({ repository, baseUrl }) => {
const match = useRouteMatch();
const [tags, setTags] = useState<Tag[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | undefined>(undefined);
const [tag, setTag] = useState<Tag>();
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 <ErrorNotification error={error} />;
}
if (loading || !tags) {
return <Loading />;
}
const url = matchedUrl();
return (
<Switch>
<Redirect exact from={url} to={`${url}/info`} />
<Route path={`${url}/info`} component={() => <TagView repository={repository} tag={tag} />} />
</Switch>
);
};
export default TagRoot;

View File

@@ -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<Props> = ({ repository, baseUrl }) => {
const [t] = useTranslation("repos");
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | undefined>(undefined);
const [tags, setTags] = useState<Tag[]>([]);
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 <TagTable baseUrl={baseUrl} tags={tags} />;
}
return <Notification type="info">{t("tags.overview.noTags")}</Notification>;
};
if (error) {
return <ErrorNotification error={error} />;
}
if (loading) {
return <Loading />;
}
return (
<>
<Subtitle subtitle={t("tags.overview.title")} />
{renderTagsTable()}
</>
);
};
export default TagsOverview;

View File

@@ -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]);
});
});

View File

@@ -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);
});
}