Feature/mirror (#1683)

Add mirror command and extension points.

Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
Co-authored-by: Sebastian Sdorra <sebastian.sdorra@cloudogu.com>
Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
Eduard Heimbuch
2021-06-04 14:05:47 +02:00
committed by GitHub
parent e55ba52ace
commit dd0975b49a
111 changed files with 6018 additions and 796 deletions

View File

@@ -0,0 +1,53 @@
/*
* 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 { storiesOf } from "@storybook/react";
import Duration from "./Duration";
import React from "react";
storiesOf("Duration", module).add("Duration", () => (
<div className="m-5 p-5">
<p>
<Duration duration={500} />
</p>
<p>
<Duration duration={2000} />
</p>
<p>
<Duration duration={42 * 1000 * 60} />
</p>
<p>
<Duration duration={21 * 1000 * 60 * 60} />
</p>
<p>
<Duration duration={5 * 1000 * 60 * 60 * 24} />
</p>
<p>
<Duration duration={3 * 1000 * 60 * 60 * 24 * 7} />
</p>
<p>
<Duration duration={12 * 1000 * 60 * 60 * 24} />
</p>
</div>
));

View File

@@ -0,0 +1,73 @@
/*
* 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";
type Unit = "ms" | "s" | "m" | "h" | "d" | "w";
type Props = {
duration: number;
};
export const parse = (duration: number) => {
let value = duration;
let unit: Unit = "ms";
if (value > 1000) {
unit = "s";
value /= 1000;
if (value > 60) {
unit = "m";
value /= 60;
if (value > 60) {
unit = "h";
value /= 60;
if (value > 24) {
unit = "d";
value /= 24;
if (value > 7) {
unit = "w";
value /= 7;
}
}
}
}
}
return {
duration: Math.round(value),
unit,
};
};
const Duration: FC<Props> = ({ duration }) => {
const [t] = useTranslation("commons");
const parsed = parse(duration);
return (
<time dateTime={`${parsed.duration}${parsed.unit}`}>
{t(`duration.${parsed.unit}`, { count: parsed.duration })}
</time>
);
};
export default Duration;

View File

@@ -74,7 +74,7 @@ const OverviewPageActions: FC<Props> = ({
if (showCreateButton) {
return (
<div className={classNames("input-button", "control", "column")}>
<Button label={label} link={createLink || `${link}create`} color="primary" />
<Button label={label} link={createLink || `${link}create/`} color="primary" />
</div>
);
}

View File

@@ -24,10 +24,10 @@
import styled from "styled-components";
import { storiesOf } from "@storybook/react";
import * as React from "react";
import React, { ReactNode } from "react";
import Tag from "./Tag";
import { ReactNode } from "react";
import { MemoryRouter } from "react-router-dom";
import { Color, colors, sizes } from "./styleConstants";
const Wrapper = styled.div`
margin: 2rem;
@@ -35,26 +35,43 @@ const Wrapper = styled.div`
`;
const Spacing = styled.div`
padding: 1em;
padding: 0.5rem;
`;
const colors = ["primary", "link", "info", "success", "warning", "danger"];
const RoutingDecorator = (story: () => ReactNode) => <MemoryRouter initialEntries={["/"]}>{story()}</MemoryRouter>;
storiesOf("Tag", module)
.addDecorator(RoutingDecorator)
.addDecorator(storyFn => <Wrapper>{storyFn()}</Wrapper>)
.addDecorator((storyFn) => <Wrapper>{storyFn()}</Wrapper>)
.add("Default", () => <Tag label="Default tag" />)
.add("Rounded", () => <Tag label="Rounded tag" color="dark" rounded={true} />)
.add("With Icon", () => <Tag label="System" icon="bolt" />)
.add("Colors", () => (
<div>
{colors.map(color => (
<Spacing>
{colors.map((color) => (
<Spacing key={color}>
<Tag color={color} label={color} />
</Spacing>
))}
</div>
))
.add("With title", () => <Tag label="hover me" title="good job"/>)
.add("Clickable", () => <Tag label="Click here" onClick={() => alert("Not so fast")}/>);
.add("Outlined", () => (
<div>
{(["success", "black", "danger"] as Color[]).map((color) => (
<Spacing key={color}>
<Tag color={color} label={color} outlined={true} />
</Spacing>
))}
</div>
))
.add("With title", () => <Tag label="hover me" title="good job" />)
.add("Clickable", () => <Tag label="Click here" onClick={() => alert("Not so fast")} />)
.add("Sizes", () => (
<div>
{sizes.map((size) => (
<Spacing key={size}>
<Tag size={size} label={size} />
</Spacing>
))}
</div>
));

View File

@@ -21,50 +21,84 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import * as React from "react";
import React, { FC, HTMLAttributes } from "react";
import classNames from "classnames";
import { Color, Size } from "./styleConstants";
import styled, { css } from "styled-components";
type Props = {
className?: string;
color: string;
color?: Color;
outlined?: boolean;
rounded?: boolean;
icon?: string;
label: string;
label?: string;
title?: string;
size?: Size;
onClick?: () => void;
onRemove?: () => void;
};
class Tag extends React.Component<Props> {
static defaultProps = {
color: "light"
};
type InnerTagProps = HTMLAttributes<HTMLSpanElement> & {
small: boolean;
};
render() {
const { className, color, icon, label, title, onClick, onRemove } = this.props;
let showIcon = null;
if (icon) {
showIcon = (
<>
<i className={classNames("fas", `fa-${icon}`)} />
&nbsp;
</>
);
}
let showDelete = null;
if (onRemove) {
showDelete = <a className="tag is-delete" onClick={onRemove} />;
}
const smallMixin = css`
font-size: 0.7rem !important;
padding: 0.25rem !important;
font-weight: bold;
`;
return (
const InnerTag = styled.span<InnerTagProps>`
${(props) => props.small && smallMixin};
`;
const Tag: FC<Props> = ({
className,
color = "light",
outlined,
size = "normal",
rounded,
icon,
label,
title,
onClick,
onRemove,
children,
}) => {
let showIcon = null;
if (icon) {
showIcon = (
<>
<span className={classNames("tag", `is-${color}`, className)} title={title} onClick={onClick}>
{showIcon}
{label}
</span>
{showDelete}
<i className={classNames("fas", `fa-${icon}`)} />
&nbsp;
</>
);
}
}
let showDelete = null;
if (onRemove) {
showDelete = <a className="tag is-delete" onClick={onRemove} />;
}
return (
<>
<InnerTag
className={classNames("tag", `is-${color}`, `is-${size}`, className, {
"is-outlined": outlined,
"is-rounded": rounded,
"has-cursor-pointer": onClick,
})}
title={title}
onClick={onClick}
small={size === "small"}
>
{showIcon}
{label}
{children}
</InnerTag>
{showDelete}
</>
);
};
export default Tag;

View File

@@ -0,0 +1,86 @@
/*
* 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, { ChangeEvent, FC, FocusEvent } from "react";
import classNames from "classnames";
import LabelWithHelpIcon from "./LabelWithHelpIcon";
import { createAttributesForTesting } from "../devBuild";
type Props = {
name?: string;
className?: string;
label?: string;
placeholder?: string;
helpText?: string;
disabled?: boolean;
testId?: string;
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
onBlur?: (event: FocusEvent<HTMLInputElement>) => void;
ref?: React.Ref<HTMLInputElement>;
};
const FileInput: FC<Props> = ({
name,
testId,
helpText,
placeholder,
disabled,
label,
className,
ref,
onBlur,
onChange
}) => {
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
if (onChange && event.target.files) {
onChange(event);
}
};
const handleBlur = (event: FocusEvent<HTMLInputElement>) => {
if (onBlur && event.target.files) {
onBlur(event);
}
};
return (
<div className={classNames("field", className)}>
<LabelWithHelpIcon label={label} helpText={helpText} />
<div className="control">
<input
ref={ref}
name={name}
className={classNames("input", "p-1", className)}
type="file"
placeholder={placeholder}
disabled={disabled}
onChange={handleChange}
onBlur={handleBlur}
{...createAttributesForTesting(testId)}
/>
</div>
</div>
);
};
export default FileInput;

View File

@@ -56,7 +56,7 @@ export default class TagGroup extends React.Component<Props> {
return (
<div className="control" key={key}>
<div className="tags has-addons">
<Tag color="info is-outlined" label={item.displayName} onRemove={() => this.removeEntry(item)} />
<Tag color="info" outlined={true} label={item.displayName} onRemove={() => this.removeEntry(item)} />
</div>
</div>
);
@@ -66,7 +66,7 @@ export default class TagGroup extends React.Component<Props> {
}
removeEntry = (item: DisplayedUser) => {
const newItems = this.props.items.filter(name => name !== item);
const newItems = this.props.items.filter((name) => name !== item);
this.props.onRemove(newItems);
};
}

View File

@@ -39,3 +39,4 @@ export { default as PasswordConfirmation } from "./PasswordConfirmation";
export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon";
export { default as DropDown } from "./DropDown";
export { default as FileUpload } from "./FileUpload";
export { default as FileInput } from "./FileInput";

View File

@@ -44,6 +44,7 @@ export { validation, repositories };
export { default as DateFromNow } from "./DateFromNow";
export { default as DateShort } from "./DateShort";
export { default as Duration } from "./Duration";
export { default as ErrorNotification } from "./ErrorNotification";
export { default as ErrorPage } from "./ErrorPage";
export { default as Icon } from "./Icon";

View File

@@ -396,10 +396,17 @@ class DiffFile extends React.Component<Props, State> {
if (key === value) {
value = file.type;
}
const color =
value === "added" ? "success is-outlined" : value === "deleted" ? "danger is-outlined" : "info is-outlined";
return <ChangeTypeTag className={classNames("is-rounded", "has-text-weight-normal")} color={color} label={value} />;
const color = value === "added" ? "success" : value === "deleted" ? "danger" : "info";
return (
<ChangeTypeTag
className={classNames("has-text-weight-normal")}
rounded={true}
outlined={true}
color={color}
label={value}
/>
);
};
hasContent = (file: FileDiff) => file && !file.isBinary && file.hunks && file.hunks.length > 0;

View File

@@ -33,6 +33,8 @@ import { Repository } from "@scm-manager/ui-types";
import Image from "../Image";
import Icon from "../Icon";
import { MemoryRouter } from "react-router-dom";
import { Color } from "../styleConstants";
import RepositoryFlag from "./RepositoryFlag";
const baseDate = "2020-03-26T12:13:42+02:00";
@@ -48,6 +50,14 @@ const bindAvatar = (binder: Binder, avatar: string) => {
});
};
const bindFlag = (binder: Binder, color: Color, label: string) => {
binder.bind("repository.card.flags", () => (
<RepositoryFlag title={label} color={color}>
{label}
</RepositoryFlag>
));
};
const bindBeforeTitle = (binder: Binder, extension: ReactNode) => {
binder.bind("repository.card.beforeTitle", () => {
return extension;
@@ -76,6 +86,17 @@ const QuickLink = (
const archivedRepository = { ...repository, archived: true };
const exportingRepository = { ...repository, exporting: true };
const healthCheckFailedRepository = {
...repository,
healthCheckFailures: [
{
id: "4211",
summary: "Something failed",
description: "Something realy bad happend",
url: "https://something-realy-bad.happend"
}
]
};
const archivedExportingRepository = { ...repository, archived: true, exporting: true };
storiesOf("RepositoryEntry", module)
@@ -109,6 +130,18 @@ storiesOf("RepositoryEntry", module)
bindAvatar(binder, Git);
return withBinder(binder, exportingRepository);
})
.add("HealthCheck Failure", () => {
const binder = new Binder("title");
bindAvatar(binder, Git);
return withBinder(binder, healthCheckFailedRepository);
})
.add("RepositoryFlag EP", () => {
const binder = new Binder("title");
bindAvatar(binder, Git);
bindFlag(binder, "success", "awesome");
bindFlag(binder, "warning", "ouhhh...");
return withBinder(binder, healthCheckFailedRepository);
})
.add("MultiRepositoryTags", () => {
const binder = new Binder("title");
bindAvatar(binder, Git);

View File

@@ -30,6 +30,7 @@ import { ExtensionPoint } from "@scm-manager/ui-extensions";
import { withTranslation, WithTranslation } from "react-i18next";
import styled from "styled-components";
import HealthCheckFailureDetail from "./HealthCheckFailureDetail";
import RepositoryFlag from "./RepositoryFlag";
type DateProp = Date | string;
@@ -44,37 +45,23 @@ type State = {
showHealthCheck: boolean;
};
const RepositoryTag = styled.span`
margin-left: 0.2rem;
background-color: #9a9a9a;
padding: 0.25rem;
border-radius: 5px;
color: white;
overflow: visible;
pointer-events: all;
font-weight: bold;
font-size: 0.7rem;
`;
const RepositoryWarnTag = styled.span`
margin-left: 0.2rem;
background-color: #f14668;
padding: 0.25rem;
border-radius: 5px;
color: white;
overflow: visible;
pointer-events: all;
font-weight: bold;
font-size: 0.7rem;
cursor: help;
const Title = styled.span`
display: flex;
align-items: center;
& > * {
margin-right: 0.25rem;
}
`;
class RepositoryEntry extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
showHealthCheck: false
showHealthCheck: false,
};
}
createLink = (repository: Repository) => {
return `/repo/${repository.namespace}/${repository.name}`;
};
@@ -170,31 +157,33 @@ class RepositoryEntry extends React.Component<Props, State> {
const { repository, t } = this.props;
const repositoryFlags = [];
if (repository.archived) {
repositoryFlags.push(<RepositoryTag title={t("archive.tooltip")}>{t("repository.archived")}</RepositoryTag>);
repositoryFlags.push(<RepositoryFlag title={t("archive.tooltip")}>{t("repository.archived")}</RepositoryFlag>);
}
if (repository.exporting) {
repositoryFlags.push(<RepositoryTag title={t("exporting.tooltip")}>{t("repository.exporting")}</RepositoryTag>);
repositoryFlags.push(<RepositoryFlag title={t("exporting.tooltip")}>{t("repository.exporting")}</RepositoryFlag>);
}
if (repository.healthCheckFailures && repository.healthCheckFailures.length > 0) {
repositoryFlags.push(
<RepositoryWarnTag
<RepositoryFlag
color="danger"
title={t("healthCheckFailure.tooltip")}
onClick={() => {
this.setState({ showHealthCheck: true });
}}
>
{t("repository.healthCheckFailure")}
</RepositoryWarnTag>
</RepositoryFlag>
);
}
return (
<>
<Title>
<ExtensionPoint name="repository.card.beforeTitle" props={{ repository }} />
<strong>{repository.name}</strong> {repositoryFlags.map(flag => flag)}
</>
<strong>{repository.name}</strong> {repositoryFlags}
<ExtensionPoint name="repository.flags" props={{ repository }} renderAll={true} />
</Title>
);
};

View File

@@ -0,0 +1,42 @@
/*
* 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 "../Tag";
import { Color, Size } from "../styleConstants";
type Props = {
color?: Color;
title?: string;
onClick?: () => void;
size?: Size;
};
const RepositoryFlag: FC<Props> = ({ children, size = "small", ...props }) => (
<Tag size={size} {...props}>
{children}
</Tag>
);
export default RepositoryFlag;

View File

@@ -29,10 +29,11 @@ import {
AnnotationFactory,
AnnotationFactoryContext,
DiffEventHandler,
DiffEventContext
DiffEventContext,
} from "./DiffTypes";
import { FileDiff as File, FileChangeType, Hunk, Change, ChangeType } from "@scm-manager/ui-types";
export { diffs };
export * from "./annotate";
@@ -46,6 +47,7 @@ export { default as LoadingDiff } from "./LoadingDiff";
export { DefaultCollapsed, DefaultCollapsedFunction } from "./defaultCollapsed";
export { default as RepositoryAvatar } from "./RepositoryAvatar";
export { default as RepositoryEntry } from "./RepositoryEntry";
export { default as RepositoryFlag } from "./RepositoryFlag";
export { default as RepositoryEntryLink } from "./RepositoryEntryLink";
export { default as JumpToFileButton } from "./JumpToFileButton";
export { default as CommitAuthor } from "./CommitAuthor";
@@ -61,5 +63,5 @@ export {
AnnotationFactory,
AnnotationFactoryContext,
DiffEventHandler,
DiffEventContext
DiffEventContext,
};

View File

@@ -0,0 +1,40 @@
/*
* 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 colors = [
"black",
"dark",
"light",
"white",
"primary",
"link",
"info",
"success",
"warning",
"danger",
] as const;
export type Color = typeof colors[number];
export const sizes = ["small", "normal", "medium", "large"] as const;
export type Size = typeof sizes[number];