mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-18 03:01:05 +01:00
Feature/branch details (#1876)
Enrich branch overview with more details like last committer and ahead/behind commits. Since calculating this information is pretty intense, we request it in chunks to prevent very long loading times. Also we cache the results in frontend and backend. Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
@@ -127,7 +127,8 @@
|
||||
"active": "Aktive Branches",
|
||||
"stale": "Stale Branches"
|
||||
},
|
||||
"lastCommit": "Letzter Commit"
|
||||
"lastCommit": "Letzter Commit",
|
||||
"lastCommitter": "von {{name}}"
|
||||
},
|
||||
"create": {
|
||||
"title": "Branch erstellen",
|
||||
@@ -142,6 +143,9 @@
|
||||
"sources": "Sources",
|
||||
"defaultTag": "Default",
|
||||
"dangerZone": "Branch löschen",
|
||||
"aheadBehind": {
|
||||
"tooltip": "{{ahead}} Commit(s) vor, {{behind}} Commit(s) hinter dem Default Branch"
|
||||
},
|
||||
"delete": {
|
||||
"button": "Branch löschen",
|
||||
"subtitle": "Branch löschen",
|
||||
|
||||
@@ -127,7 +127,8 @@
|
||||
"active": "Active Branches",
|
||||
"stale": "Stale Branches"
|
||||
},
|
||||
"lastCommit": "Last commit"
|
||||
"lastCommit": "Last commit",
|
||||
"lastCommitter": "by {{name}}"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Branch",
|
||||
@@ -142,6 +143,9 @@
|
||||
"sources": "Sources",
|
||||
"defaultTag": "Default",
|
||||
"dangerZone": "Delete Branch",
|
||||
"aheadBehind": {
|
||||
"tooltip": "{{ahead}} commit(s) ahead, {{behind}} commit(s) behind default branch"
|
||||
},
|
||||
"delete": {
|
||||
"button": "Delete Branch",
|
||||
"subtitle": "Delete Branch",
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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 { Branch, BranchDetails } from "@scm-manager/ui-types";
|
||||
import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import { Tooltip } from "@scm-manager/ui-components";
|
||||
import { calculateBarLength } from "./aheadBehind";
|
||||
|
||||
type Props = {
|
||||
branch: Branch;
|
||||
details: BranchDetails;
|
||||
};
|
||||
|
||||
type BarProps = {
|
||||
width: number;
|
||||
direction: "right" | "left";
|
||||
};
|
||||
|
||||
const Ahead = styled.div`
|
||||
border-left: 1px solid gray;
|
||||
`;
|
||||
|
||||
const Behind = styled.div``;
|
||||
|
||||
const Count = styled.div`
|
||||
word-break: keep-all;
|
||||
`;
|
||||
|
||||
const Bar = styled.span.attrs<BarProps>(props => ({
|
||||
style: {
|
||||
width: props.width + "%",
|
||||
borderRadius: props.direction === "left" ? "25px 0 0 25px" : "0 25px 25px 0"
|
||||
}
|
||||
}))<BarProps>`
|
||||
height: 3px;
|
||||
max-width: 100%;
|
||||
margin-top: -2px;
|
||||
margin-bottom: 2px;
|
||||
`;
|
||||
|
||||
const TooltipWithDefaultCursor = styled(Tooltip)`
|
||||
cursor: default !important;
|
||||
`;
|
||||
|
||||
const AheadBehindTag: FC<Props> = ({ branch, details }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
if (
|
||||
branch.defaultBranch ||
|
||||
typeof details.changesetsBehind !== "number" ||
|
||||
typeof details.changesetsAhead !== "number"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipWithDefaultCursor
|
||||
message={t("branch.aheadBehind.tooltip", { ahead: details.changesetsAhead, behind: details.changesetsBehind })}
|
||||
location="top"
|
||||
>
|
||||
<div className="columns is-flex is-unselectable is-hidden-mobile">
|
||||
<Behind className="column is-half is-flex is-flex-direction-column is-align-items-flex-end p-0">
|
||||
<Count className="is-size-7 pr-1">{details.changesetsBehind}</Count>
|
||||
<Bar
|
||||
className="has-rounded-border-left has-background-grey"
|
||||
width={calculateBarLength(details.changesetsBehind)}
|
||||
direction="left"
|
||||
/>
|
||||
</Behind>
|
||||
<Ahead className="column is-half is-flex is-flex-direction-column is-align-items-flex-start p-0">
|
||||
<Count className="is-size-7 pl-1">{details.changesetsAhead}</Count>
|
||||
<Bar
|
||||
className="has-rounded-border-right has-background-grey"
|
||||
width={calculateBarLength(details.changesetsAhead)}
|
||||
direction="right"
|
||||
/>
|
||||
</Ahead>
|
||||
</div>
|
||||
</TooltipWithDefaultCursor>
|
||||
);
|
||||
};
|
||||
|
||||
export default AheadBehindTag;
|
||||
@@ -25,42 +25,80 @@ import React, { FC } from "react";
|
||||
import { Link as ReactLink } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
import { Branch, Link } from "@scm-manager/ui-types";
|
||||
import { Branch, BranchDetails, Link } from "@scm-manager/ui-types";
|
||||
import { DateFromNow, Icon } from "@scm-manager/ui-components";
|
||||
import DefaultBranchTag from "./DefaultBranchTag";
|
||||
import AheadBehindTag from "./AheadBehindTag";
|
||||
|
||||
type Props = {
|
||||
baseUrl: string;
|
||||
branch: Branch;
|
||||
onDelete: (branch: Branch) => void;
|
||||
details?: BranchDetails;
|
||||
};
|
||||
|
||||
const BranchRow: FC<Props> = ({ baseUrl, branch, onDelete }) => {
|
||||
const BranchRow: FC<Props> = ({ baseUrl, branch, onDelete, details }) => {
|
||||
const to = `${baseUrl}/${encodeURIComponent(branch.name)}/info`;
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
let deleteButton;
|
||||
if ((branch?._links?.delete as Link)?.href) {
|
||||
deleteButton = (
|
||||
<span className="icon is-small is-hovered" onClick={() => onDelete(branch)} onKeyDown={(e) => e.key === "Enter" && onDelete(branch)} tabIndex={0}>
|
||||
<span
|
||||
className="icon is-small is-hovered is-clickable"
|
||||
onClick={() => onDelete(branch)}
|
||||
onKeyDown={e => e.key === "Enter" && onDelete(branch)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon name="trash" className="fas " title={t("branch.delete.button")} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const renderBranchTag = () => {
|
||||
if (branch.defaultBranch) {
|
||||
return <DefaultBranchTag defaultBranch={branch.defaultBranch} />;
|
||||
}
|
||||
if (details) {
|
||||
return <AheadBehindTag branch={branch} details={details} />;
|
||||
}
|
||||
return (
|
||||
<div className="loader-wrapper">
|
||||
<div className="loader is-loading" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const committedAt = (
|
||||
<>
|
||||
{t("branches.table.lastCommit")} <DateFromNow date={branch.lastCommitDate} />
|
||||
</>
|
||||
);
|
||||
|
||||
let committedAtBy;
|
||||
if (branch.lastCommitter?.name) {
|
||||
committedAtBy = (
|
||||
<>
|
||||
{committedAt} {t("branches.table.lastCommitter", { name: branch.lastCommitter?.name })}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
committedAtBy = committedAt;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<td className="is-flex">
|
||||
<ReactLink to={to} title={branch.name}>
|
||||
{branch.name}
|
||||
<DefaultBranchTag defaultBranch={branch.defaultBranch} />
|
||||
</ReactLink>
|
||||
{branch.lastCommitDate && (
|
||||
<span className={classNames("has-text-grey", "is-ellipsis-overflow", "is-size-7", "ml-4")}>
|
||||
{t("branches.table.lastCommit")} <DateFromNow date={branch.lastCommitDate} />
|
||||
{committedAtBy}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="has-text-centered">{renderBranchTag()}</td>
|
||||
<td className="is-darker has-text-centered">{deleteButton}</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import BranchRow from "./BranchRow";
|
||||
import { Branch, Repository } from "@scm-manager/ui-types";
|
||||
import { Branch, BranchDetails, Repository } from "@scm-manager/ui-types";
|
||||
import { ConfirmAlert, ErrorNotification } from "@scm-manager/ui-components";
|
||||
import { useDeleteBranch } from "@scm-manager/ui-api";
|
||||
|
||||
@@ -33,9 +33,10 @@ type Props = {
|
||||
repository: Repository;
|
||||
branches: Branch[];
|
||||
type: string;
|
||||
branchesDetails: BranchDetails[];
|
||||
};
|
||||
|
||||
const BranchTable: FC<Props> = ({ repository, baseUrl, branches, type }) => {
|
||||
const BranchTable: FC<Props> = ({ repository, baseUrl, branches, type, branchesDetails }) => {
|
||||
const { isLoading, error, remove, isDeleted } = useDeleteBranch(repository);
|
||||
const [t] = useTranslation("repos");
|
||||
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
|
||||
@@ -77,17 +78,17 @@ const BranchTable: FC<Props> = ({ repository, baseUrl, branches, type }) => {
|
||||
className: "is-outlined",
|
||||
label: t("branch.delete.confirmAlert.submit"),
|
||||
isLoading,
|
||||
onClick: () => deleteBranch(),
|
||||
onClick: () => deleteBranch()
|
||||
},
|
||||
{
|
||||
label: t("branch.delete.confirmAlert.cancel"),
|
||||
onClick: () => abortDelete(),
|
||||
},
|
||||
onClick: () => abortDelete()
|
||||
}
|
||||
]}
|
||||
close={() => abortDelete()}
|
||||
/>
|
||||
) : null}
|
||||
{error ? <ErrorNotification error={error} /> : null}
|
||||
<ErrorNotification error={error} />
|
||||
<table className="card-table table is-hoverable is-fullwidth is-word-break">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -95,8 +96,14 @@ const BranchTable: FC<Props> = ({ repository, baseUrl, branches, type }) => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(branches || []).map((branch) => (
|
||||
<BranchRow key={branch.name} baseUrl={baseUrl} branch={branch} onDelete={onDelete} />
|
||||
{(branches || []).map(branch => (
|
||||
<BranchRow
|
||||
key={branch.name}
|
||||
baseUrl={baseUrl}
|
||||
branch={branch}
|
||||
onDelete={onDelete}
|
||||
details={branchesDetails?.filter((b: BranchDetails) => b.branchName === branch.name)[0]}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -24,17 +24,22 @@
|
||||
import React from "react";
|
||||
import { WithTranslation, withTranslation } from "react-i18next";
|
||||
import { Tag } from "@scm-manager/ui-components";
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = WithTranslation & {
|
||||
defaultBranch?: boolean;
|
||||
};
|
||||
|
||||
const DefaultTag = styled(Tag)`
|
||||
max-height: 1.5em;
|
||||
`;
|
||||
|
||||
class DefaultBranchTag extends React.Component<Props> {
|
||||
render() {
|
||||
const { defaultBranch, t } = this.props;
|
||||
|
||||
if (defaultBranch) {
|
||||
return <Tag className="ml-3" color="dark" label={t("branch.defaultTag")} />;
|
||||
return <DefaultTag className="is-unselectable" color="dark" label={t("branch.defaultTag")} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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 { calculateBarLength } from "./aheadBehind";
|
||||
|
||||
describe("ahead/behind percentage", () => {
|
||||
it("0 should have percentage value of 5", () => {
|
||||
const percentage = calculateBarLength(0);
|
||||
expect(percentage).toEqual(5);
|
||||
});
|
||||
|
||||
let lastPercentage = 5;
|
||||
for (let changesets = 1; changesets < 4000; changesets++) {
|
||||
it(`${changesets} should have percentage value less or equal to last value`, () => {
|
||||
const percentage = calculateBarLength(changesets);
|
||||
expect(percentage).toBeGreaterThanOrEqual(lastPercentage);
|
||||
lastPercentage = percentage;
|
||||
});
|
||||
}
|
||||
|
||||
it("10000 should not have percentage value bigger than 100", () => {
|
||||
const percentage = calculateBarLength(10000);
|
||||
expect(percentage).toEqual(100);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const calculateBarLength = (changesets: number) => {
|
||||
if (changesets <= 10) {
|
||||
return changesets + 5;
|
||||
} else if (changesets <= 50) {
|
||||
return (changesets - 10) / 5 + 15;
|
||||
} else if (changesets <= 500) {
|
||||
return (changesets - 50) / 10 + 23;
|
||||
} else if (changesets <= 3700) {
|
||||
return (changesets - 500) / 100 + 68;
|
||||
} else {
|
||||
return 100;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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 { Branch, HalRepresentation, Repository } from "@scm-manager/ui-types";
|
||||
import { CreateButton, ErrorNotification, Notification, Subtitle } from "@scm-manager/ui-components";
|
||||
import { orderBranches } from "../util/orderBranches";
|
||||
import BranchTable from "../components/BranchTable";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useBranchDetailsCollection } from "@scm-manager/ui-api";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
baseUrl: string;
|
||||
data: HalRepresentation;
|
||||
};
|
||||
|
||||
const BranchTableWrapper: FC<Props> = ({ repository, baseUrl, data }) => {
|
||||
const [t] = useTranslation("repos");
|
||||
const branches: Branch[] = (data?._embedded?.branches as Branch[]) || [];
|
||||
orderBranches(branches);
|
||||
const staleBranches = branches.filter(b => b.stale);
|
||||
const activeBranches = branches.filter(b => !b.stale);
|
||||
const { error, data: branchesDetails } = useBranchDetailsCollection(repository, [
|
||||
...activeBranches,
|
||||
...staleBranches
|
||||
]);
|
||||
|
||||
if (branches.length === 0) {
|
||||
return <Notification type="info">{t("branches.overview.noBranches")}</Notification>;
|
||||
}
|
||||
|
||||
const showCreateButton = !!data._links.create;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle subtitle={t("branches.overview.title")} />
|
||||
<ErrorNotification error={error} />
|
||||
{activeBranches.length > 0 ? (
|
||||
<BranchTable
|
||||
repository={repository}
|
||||
baseUrl={baseUrl}
|
||||
type="active"
|
||||
branches={activeBranches}
|
||||
branchesDetails={branchesDetails}
|
||||
/>
|
||||
) : null}
|
||||
{staleBranches.length > 0 ? (
|
||||
<BranchTable
|
||||
repository={repository}
|
||||
baseUrl={baseUrl}
|
||||
type="stale"
|
||||
branches={staleBranches}
|
||||
branchesDetails={branchesDetails}
|
||||
/>
|
||||
) : null}
|
||||
{showCreateButton ? <CreateButton label={t("branches.overview.createButton")} link="./create" /> : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BranchTableWrapper;
|
||||
@@ -22,12 +22,10 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
import React, { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Repository } from "@scm-manager/ui-types";
|
||||
import { CreateButton, ErrorNotification, Loading, Notification, Subtitle } from "@scm-manager/ui-components";
|
||||
import { orderBranches } from "../util/orderBranches";
|
||||
import BranchTable from "../components/BranchTable";
|
||||
import { ErrorNotification, Loading } from "@scm-manager/ui-components";
|
||||
import { useBranches } from "@scm-manager/ui-api";
|
||||
import BranchTableWrapper from "./BranchTableWrapper";
|
||||
|
||||
type Props = {
|
||||
repository: Repository;
|
||||
@@ -36,7 +34,6 @@ type Props = {
|
||||
|
||||
const BranchesOverview: FC<Props> = ({ repository, baseUrl }) => {
|
||||
const { isLoading, error, data } = useBranches(repository);
|
||||
const [t] = useTranslation("repos");
|
||||
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
@@ -46,30 +43,7 @@ const BranchesOverview: FC<Props> = ({ repository, baseUrl }) => {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const branches = data?._embedded?.branches || [];
|
||||
|
||||
if (branches.length === 0) {
|
||||
return <Notification type="info">{t("branches.overview.noBranches")}</Notification>;
|
||||
}
|
||||
|
||||
orderBranches(branches);
|
||||
const staleBranches = branches.filter((b) => b.stale);
|
||||
const activeBranches = branches.filter((b) => !b.stale);
|
||||
|
||||
const showCreateButton = !!data._links.create;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Subtitle subtitle={t("branches.overview.title")} />
|
||||
{activeBranches.length > 0 ? (
|
||||
<BranchTable repository={repository} baseUrl={baseUrl} type="active" branches={activeBranches} />
|
||||
) : null}
|
||||
{staleBranches.length > 0 ? (
|
||||
<BranchTable repository={repository} baseUrl={baseUrl} type="stale" branches={staleBranches} />
|
||||
) : null}
|
||||
{showCreateButton ? <CreateButton label={t("branches.overview.createButton")} link="./create" /> : null}
|
||||
</>
|
||||
);
|
||||
return <BranchTableWrapper repository={repository} baseUrl={baseUrl} data={data} />;
|
||||
};
|
||||
|
||||
export default BranchesOverview;
|
||||
|
||||
@@ -41,7 +41,12 @@ const TagRow: FC<Props> = ({ tag, baseUrl, onDelete }) => {
|
||||
let deleteButton;
|
||||
if ((tag?._links?.delete as Link)?.href) {
|
||||
deleteButton = (
|
||||
<span className="icon is-small" onClick={() => onDelete(tag)} onKeyDown={(e) => e.key === "Enter" && onDelete(tag)} tabIndex={0}>
|
||||
<span
|
||||
className="icon is-small is-clickable"
|
||||
onClick={() => onDelete(tag)}
|
||||
onKeyDown={e => e.key === "Enter" && onDelete(tag)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon name="trash" className="fas" title={t("tag.delete.button")} />
|
||||
</span>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user