Merge with upstream

This commit is contained in:
Florian Scholdei
2020-10-08 00:47:45 +02:00
69 changed files with 3031 additions and 1020 deletions

View File

@@ -21,80 +21,78 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import React, { FC, useState } from "react";
import { connect } from "react-redux";
import { compose } from "redux";
import { withRouter } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next";
import { History } from "history";
import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { RepositoryRole } from "@scm-manager/ui-types";
import { confirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { ConfirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { deleteRole, getDeleteRoleFailure, isDeleteRolePending } from "../modules/roles";
type Props = WithTranslation & {
type Props = {
loading: boolean;
error: Error;
role: RepositoryRole;
confirmDialog?: boolean;
deleteRole: (role: RepositoryRole, callback?: () => void) => void;
// context props
history: History;
};
class DeleteRepositoryRole extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
const DeleteRepositoryRole: FC<Props> = ({ confirmDialog = true, deleteRole, role, loading, error }: Props) => {
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
const [t] = useTranslation("admin");
const history = useHistory();
const roleDeleted = () => {
history.push("/admin/roles/");
};
roleDeleted = () => {
this.props.history.push("/admin/roles/");
const deleteRoleCallback = () => {
deleteRole(role, roleDeleted);
};
deleteRole = () => {
this.props.deleteRole(this.props.role, this.roleDeleted);
const confirmDelete = () => {
setShowConfirmAlert(true);
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("repositoryRole.delete.confirmAlert.title"),
message: t("repositoryRole.delete.confirmAlert.message"),
buttons: [
{
className: "is-outlined",
label: t("repositoryRole.delete.confirmAlert.submit"),
onClick: () => this.deleteRole()
},
{
label: t("repositoryRole.delete.confirmAlert.cancel"),
onClick: () => null
}
]
});
const isDeletable = () => {
return role._links.delete;
};
isDeletable = () => {
return this.props.role._links.delete;
};
const action = confirmDialog ? confirmDelete : deleteRoleCallback;
render() {
const { loading, error, confirmDialog, t } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deleteRole;
if (!this.isDeletable()) {
return null;
}
if (!isDeletable()) {
return null;
}
if (showConfirmAlert) {
return (
<>
<hr />
<ErrorNotification error={error} />
<Level right={<DeleteButton label={t("repositoryRole.delete.button")} action={action} loading={loading} />} />
</>
<ConfirmAlert
title={t("repositoryRole.delete.confirmAlert.title")}
message={t("repositoryRole.delete.confirmAlert.message")}
buttons={[
{
className: "is-outlined",
label: t("repositoryRole.delete.confirmAlert.submit"),
onClick: () => deleteRoleCallback()
},
{
label: t("repositoryRole.delete.confirmAlert.cancel"),
onClick: () => null
}
]}
close={() => setShowConfirmAlert(false)}
/>
);
}
}
return (
<>
<hr />
<ErrorNotification error={error} />
<Level right={<DeleteButton label={t("repositoryRole.delete.button")} action={action} loading={loading} />} />
</>
);
};
const mapStateToProps = (state: any, ownProps: Props) => {
const loading = isDeleteRolePending(state, ownProps.role.name);
@@ -113,8 +111,4 @@ const mapDispatchToProps = (dispatch: any) => {
};
};
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withRouter,
withTranslation("admin")
)(DeleteRepositoryRole);
export default connect(mapStateToProps, mapDispatchToProps)(DeleteRepositoryRole);

View File

@@ -44,6 +44,8 @@ import ProfileInfo from "./ProfileInfo";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import SetPublicKeys from "../users/components/publicKeys/SetPublicKeys";
import SetPublicKeyNavLink from "../users/components/navLinks/SetPublicKeysNavLink";
import SetApiKeys from "../users/components/apiKeys/SetApiKeys";
import SetApiKeyNavLink from "../users/components/navLinks/SetApiKeysNavLink";
import { urls } from "@scm-manager/ui-components";
type Props = RouteComponentProps &
@@ -65,6 +67,11 @@ class Profile extends React.Component<Props> {
return !!me?._links?.publicKeys;
};
canManageApiKeys = () => {
const { me } = this.props;
return !!me?._links?.apiKeys;
};
render() {
const url = urls.matchedUrl(this.props);
@@ -100,6 +107,9 @@ class Profile extends React.Component<Props> {
{this.canManagePublicKeys() && (
<Route path={`${url}/settings/publicKeys`} render={() => <SetPublicKeys user={me} />} />
)}
{this.canManageApiKeys() && (
<Route path={`${url}/settings/apiKeys`} render={() => <SetApiKeys user={me} />} />
)}
<ExtensionPoint name="profile.route" props={extensionProps} renderAll={true} />
</PrimaryContentColumn>
<SecondaryNavigationColumn>
@@ -118,6 +128,7 @@ class Profile extends React.Component<Props> {
>
<NavLink to={`${url}/settings/password`} label={t("profile.changePasswordNavLink")} />
<SetPublicKeyNavLink user={me} publicKeyUrl={`${url}/settings/publicKeys`} />
<SetApiKeyNavLink user={me} apiKeyUrl={`${url}/settings/apiKeys`} />
<ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} />
</SubNavigation>
)}

View File

@@ -21,80 +21,78 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import React, { FC, useState } from "react";
import { connect } from "react-redux";
import { compose } from "redux";
import { withRouter } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next";
import { History } from "history";
import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Group } from "@scm-manager/ui-types";
import { confirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { ConfirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { deleteGroup, getDeleteGroupFailure, isDeleteGroupPending } from "../modules/groups";
type Props = WithTranslation & {
type Props = {
loading: boolean;
error: Error;
group: Group;
confirmDialog?: boolean;
deleteGroup: (group: Group, callback?: () => void) => void;
// context props
history: History;
};
export class DeleteGroup extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
export const DeleteGroup: FC<Props> = ({ confirmDialog = true, group, deleteGroup, loading, error }) => {
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
const [t] = useTranslation("groups");
const history = useHistory();
const deleteGroupCallback = () => {
deleteGroup(group, groupDeleted);
};
deleteGroup = () => {
this.props.deleteGroup(this.props.group, this.groupDeleted);
const groupDeleted = () => {
history.push("/groups/");
};
groupDeleted = () => {
this.props.history.push("/groups/");
const confirmDelete = () => {
setShowConfirmAlert(true);
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("deleteGroup.confirmAlert.title"),
message: t("deleteGroup.confirmAlert.message"),
buttons: [
{
className: "is-outlined",
label: t("deleteGroup.confirmAlert.submit"),
onClick: () => this.deleteGroup()
},
{
label: t("deleteGroup.confirmAlert.cancel"),
onClick: () => null
}
]
});
const isDeletable = () => {
return group._links.delete;
};
isDeletable = () => {
return this.props.group._links.delete;
};
const action = confirmDialog ? confirmDelete : deleteGroupCallback;
render() {
const { loading, error, confirmDialog, t } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deleteGroup;
if (!this.isDeletable()) {
return null;
}
if (!isDeletable()) {
return null;
}
if (showConfirmAlert) {
return (
<>
<hr />
<ErrorNotification error={error} />
<Level right={<DeleteButton label={t("deleteGroup.button")} action={action} loading={loading} />} />
</>
<ConfirmAlert
title={t("deleteGroup.confirmAlert.title")}
message={t("deleteGroup.confirmAlert.message")}
buttons={[
{
className: "is-outlined",
label: t("deleteGroup.confirmAlert.submit"),
onClick: () => deleteGroupCallback()
},
{
label: t("deleteGroup.confirmAlert.cancel"),
onClick: () => null
}
]}
close={() => setShowConfirmAlert(false)}
/>
);
}
}
return (
<>
<hr />
<ErrorNotification error={error} />
<Level right={<DeleteButton label={t("deleteGroup.button")} action={action} loading={loading} />} />
</>
);
};
const mapStateToProps = (state: any, ownProps: Props) => {
const loading = isDeleteGroupPending(state, ownProps.group.name);
@@ -113,8 +111,4 @@ const mapDispatchToProps = (dispatch: any) => {
};
};
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withRouter,
withTranslation("groups")
)(DeleteGroup);
export default connect(mapStateToProps, mapDispatchToProps)(DeleteGroup);

View File

@@ -21,85 +21,86 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import React, { FC, useState } from "react";
import { connect } from "react-redux";
import { compose } from "redux";
import { RouteComponentProps, withRouter } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Repository } from "@scm-manager/ui-types";
import { confirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { ConfirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { deleteRepo, getDeleteRepoFailure, isDeleteRepoPending } from "../modules/repos";
type Props = RouteComponentProps &
WithTranslation & {
loading: boolean;
error: Error;
repository: Repository;
confirmDialog?: boolean;
deleteRepo: (p1: Repository, p2: () => void) => void;
type Props = {
loading: boolean;
error: Error;
repository: Repository;
confirmDialog?: boolean;
deleteRepo: (p1: Repository, p2: () => void) => void;
};
const DeleteRepo: FC<Props> = ({ confirmDialog = true, repository, deleteRepo, loading, error }: Props) => {
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
const [t] = useTranslation("repos");
const history = useHistory();
const deleted = () => {
history.push("/repos/");
};
class DeleteRepo extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
const deleteRepoCallback = () => {
deleteRepo(repository, deleted);
};
deleted = () => {
this.props.history.push("/repos/");
const confirmDelete = () => {
setShowConfirmAlert(true);
};
deleteRepo = () => {
this.props.deleteRepo(this.props.repository, this.deleted);
const isDeletable = () => {
return repository._links.delete;
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("deleteRepo.confirmAlert.title"),
message: t("deleteRepo.confirmAlert.message"),
buttons: [
{
className: "is-outlined",
label: t("deleteRepo.confirmAlert.submit"),
onClick: () => this.deleteRepo()
},
{
label: t("deleteRepo.confirmAlert.cancel"),
onClick: () => null
}
]
});
};
const action = confirmDialog ? confirmDelete : deleteRepoCallback;
isDeletable = () => {
return this.props.repository._links.delete;
};
render() {
const { loading, error, confirmDialog, t } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deleteRepo;
if (!this.isDeletable()) {
return null;
}
if (!isDeletable()) {
return null;
}
if (showConfirmAlert) {
return (
<>
<ErrorNotification error={error} />
<Level
left={
<p>
<strong>{t("deleteRepo.subtitle")}</strong>
<br />
{t("deleteRepo.description")}
</p>
<ConfirmAlert
title={t("deleteRepo.confirmAlert.title")}
message={t("deleteRepo.confirmAlert.message")}
buttons={[
{
className: "is-outlined",
label: t("deleteRepo.confirmAlert.submit"),
onClick: () => deleteRepoCallback()
},
{
label: t("deleteRepo.confirmAlert.cancel"),
onClick: () => null
}
right={<DeleteButton label={t("deleteRepo.button")} action={action} loading={loading} />}
/>
</>
]}
close={() => setShowConfirmAlert(false)}
/>
);
}
}
return (
<>
<ErrorNotification error={error} />
<Level
left={
<p>
<strong>{t("deleteRepo.subtitle")}</strong>
<br />
{t("deleteRepo.description")}
</p>
}
right={<DeleteButton label={t("deleteRepo.button")} action={action} loading={loading} />}
/>
</>
);
};
const mapStateToProps = (state: any, ownProps: Props) => {
const { namespace, name } = ownProps.repository;
@@ -119,4 +120,4 @@ const mapDispatchToProps = (dispatch: any) => {
};
};
export default compose(connect(mapStateToProps, mapDispatchToProps), withRouter, withTranslation("repos"))(DeleteRepo);
export default connect(mapStateToProps, mapDispatchToProps)(DeleteRepo);

View File

@@ -26,7 +26,7 @@ import { WithTranslation, withTranslation } from "react-i18next";
import { Select } from "@scm-manager/ui-components";
type Props = WithTranslation & {
availableRoles: string[];
availableRoles?: string[];
handleRoleChange: (p: string) => void;
role: string;
label?: string;

View File

@@ -21,16 +21,19 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
import React, { FC } from "react";
// eslint-disable-next-line no-restricted-imports
import { mount, shallow } from "@scm-manager/ui-tests/enzyme-router";
// eslint-disable-next-line no-restricted-imports
import "@scm-manager/ui-tests/enzyme";
// eslint-disable-next-line no-restricted-imports
import "@scm-manager/ui-tests/i18n";
import DeletePermissionButton from "./DeletePermissionButton";
import { confirmAlert } from "@scm-manager/ui-components";
jest.mock("@scm-manager/ui-components", () => ({
confirmAlert: jest.fn(),
ConfirmAlert: (({ children }) => <div className="modal">{children}</div>) as FC<never>,
DeleteButton: require.requireActual("@scm-manager/ui-components").DeleteButton
}));
@@ -40,6 +43,9 @@ describe("DeletePermissionButton", () => {
_links: {}
};
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-empty-function
const navLink = shallow(<DeletePermissionButton permission={permission} deletePermission={() => {}} />);
expect(navLink.text()).toBe("");
});
@@ -53,6 +59,9 @@ describe("DeletePermissionButton", () => {
}
};
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-empty-function
const deleteIcon = mount(<DeletePermissionButton permission={permission} deletePermission={() => {}} />);
expect(deleteIcon.html()).not.toBe("");
});
@@ -66,10 +75,13 @@ describe("DeletePermissionButton", () => {
}
};
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-empty-function
const button = mount(<DeletePermissionButton permission={permission} deletePermission={() => {}} />);
button.find(".fa-trash").simulate("click");
expect(confirmAlert.mock.calls.length).toBe(1);
expect(button.find(".modal")).toBeTruthy();
});
it("should call the delete permission function with delete url", () => {
@@ -82,11 +94,14 @@ describe("DeletePermissionButton", () => {
};
let calledUrl = null;
function capture(permission) {
calledUrl = permission._links.delete.href;
}
const button = mount(
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
<DeletePermissionButton permission={permission} confirmDialog={false} deletePermission={capture} />
);
button.find(".fa-trash").simulate("click");

View File

@@ -21,12 +21,12 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import React, { FC, useState } from "react";
import { useTranslation } from "react-i18next";
import { Permission } from "@scm-manager/ui-types";
import { confirmAlert } from "@scm-manager/ui-components";
import { ConfirmAlert } from "@scm-manager/ui-components";
type Props = WithTranslation & {
type Props = {
permission: Permission;
namespace: string;
repoName: string;
@@ -35,53 +35,62 @@ type Props = WithTranslation & {
loading: boolean;
};
class DeletePermissionButton extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
const DeletePermissionButton: FC<Props> = ({
confirmDialog = true,
permission,
namespace,
deletePermission,
repoName
}) => {
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
const [t] = useTranslation("repos");
const deletePermissionCallback = () => {
deletePermission(permission, namespace, repoName);
};
deletePermission = () => {
this.props.deletePermission(this.props.permission, this.props.namespace, this.props.repoName);
const confirmDelete = () => {
setShowConfirmAlert(true);
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("permission.delete-permission-button.confirm-alert.title"),
message: t("permission.delete-permission-button.confirm-alert.message"),
buttons: [
{
className: "is-outlined",
label: t("permission.delete-permission-button.confirm-alert.submit"),
onClick: () => this.deletePermission()
},
{
label: t("permission.delete-permission-button.confirm-alert.cancel"),
onClick: () => null
}
]
});
const isDeletable = () => {
return permission._links.delete;
};
isDeletable = () => {
return this.props.permission._links.delete;
};
const action = confirmDialog ? confirmDelete : deletePermissionCallback;
render() {
const { confirmDialog } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deletePermission;
if (!isDeletable()) {
return null;
}
if (!this.isDeletable()) {
return null;
}
if (showConfirmAlert) {
return (
<a className="level-item" onClick={action}>
<span className="icon is-small">
<i className="fas fa-trash" />
</span>
</a>
<ConfirmAlert
title={t("permission.delete-permission-button.confirm-alert.title")}
message={t("permission.delete-permission-button.confirm-alert.message")}
buttons={[
{
className: "is-outlined",
label: t("permission.delete-permission-button.confirm-alert.submit"),
onClick: () => deletePermissionCallback()
},
{
label: t("permission.delete-permission-button.confirm-alert.cancel"),
onClick: () => null
}
]}
close={() => setShowConfirmAlert(false)}
/>
);
}
}
export default withTranslation("repos")(DeletePermissionButton);
return (
<a className="level-item" onClick={action}>
<span className="icon is-small">
<i className="fas fa-trash" />
</span>
</a>
);
};
export default DeletePermissionButton;

View File

@@ -0,0 +1,145 @@
/*
* 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 { apiClient, ErrorNotification, InputField, Level, Loading, SubmitButton } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
import { CONTENT_TYPE_API_KEY } from "./SetApiKeys";
import { connect } from "react-redux";
import {
fetchAvailablePermissionsIfNeeded,
getAvailableRepositoryRoles
} from "../../../repos/permissions/modules/permissions";
import { RepositoryRole } from "@scm-manager/ui-types";
import { getRepositoryRolesLink, getRepositoryVerbsLink } from "../../../modules/indexResource";
import RoleSelector from "../../../repos/permissions/components/RoleSelector";
import ApiKeyCreatedModal from "./ApiKeyCreatedModal";
type Props = {
createLink: string;
refresh: () => void;
repositoryRolesLink: string;
repositoryVerbsLink: string;
fetchAvailablePermissionsIfNeeded: (repositoryRolesLink: string, repositoryVerbsLink: string) => void;
availableRepositoryRoles?: RepositoryRole[];
};
const AddApiKey: FC<Props> = ({
createLink,
refresh,
fetchAvailablePermissionsIfNeeded,
repositoryRolesLink,
repositoryVerbsLink,
availableRepositoryRoles
}) => {
const [t] = useTranslation("users");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<undefined | Error>();
const [displayName, setDisplayName] = useState("");
const [permissionRole, setPermissionRole] = useState("");
const [addedKey, setAddedKey] = useState("");
useEffect(() => {
if (!availableRepositoryRoles) {
fetchAvailablePermissionsIfNeeded(repositoryRolesLink, repositoryVerbsLink);
}
}, [repositoryRolesLink, repositoryVerbsLink]);
const isValid = () => {
return !!displayName && !!permissionRole;
};
const resetForm = () => {
setDisplayName("");
setPermissionRole("");
};
const addKey = () => {
setLoading(true);
apiClient
.post(createLink, { displayName: displayName, permissionRole: permissionRole }, CONTENT_TYPE_API_KEY)
.then(response => response.text())
.then(setAddedKey)
.then(() => setLoading(false))
.catch(setError);
};
if (error) {
return <ErrorNotification error={error} />;
}
if (loading) {
return <Loading />;
}
const availableRoleNames = availableRepositoryRoles ? availableRepositoryRoles.map(r => r.name) : [];
const closeModal = () => {
resetForm();
refresh();
setAddedKey("");
};
const newKeyModal = addedKey && <ApiKeyCreatedModal addedKey={addedKey} close={closeModal} />;
return (
<>
{newKeyModal}
<InputField label={t("apiKey.displayName")} value={displayName} onChange={setDisplayName} />
<RoleSelector
loading={!availableRoleNames}
availableRoles={availableRoleNames}
label={t("apiKey.permissionRole.label")}
helpText={t("apiKey.permissionRole.help")}
handleRoleChange={setPermissionRole}
role={permissionRole}
/>
<Level
right={<SubmitButton label={t("apiKey.addKey")} loading={loading} disabled={!isValid()} action={addKey} />}
/>
</>
);
};
const mapStateToProps = (state: any, ownProps: Props) => {
const availableRepositoryRoles = getAvailableRepositoryRoles(state);
const repositoryRolesLink = getRepositoryRolesLink(state);
const repositoryVerbsLink = getRepositoryVerbsLink(state);
return {
availableRepositoryRoles,
repositoryRolesLink,
repositoryVerbsLink
};
};
const mapDispatchToProps = (dispatch: any) => {
return {
fetchAvailablePermissionsIfNeeded: (repositoryRolesLink: string, repositoryVerbsLink: string) => {
dispatch(fetchAvailablePermissionsIfNeeded(repositoryRolesLink, repositoryVerbsLink));
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(AddApiKey);

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, { FC, useRef, useState } from "react";
import { Button, Icon, Modal } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
type Props = {
addedKey: string;
close: () => void;
};
const KeyArea = styled.textarea`
white-space: nowrap;
overflow: auto;
font-family: "Courier New", Monaco, Menlo, "Ubuntu Mono", "source-code-pro", monospace;
height: 3rem;
`;
const NoLeftMargin = styled.div`
margin-left: -1rem;
`;
const ApiKeyCreatedModal: FC<Props> = ({ addedKey, close }) => {
const [t] = useTranslation("users");
const [copied, setCopied] = useState(false);
const keyRef = useRef(null);
const copy = () => {
keyRef.current.select();
document.execCommand("copy");
setCopied(true);
};
const newPassphraseModalContent = (
<div className={"media-content"}>
<p>{t("apiKey.modal.text1")}</p>
<p>
<b>{t("apiKey.modal.text2")}</b>
</p>
<hr />
<div className={"columns"}>
<div className={"column is-11"}>
<KeyArea wrap={"soft"} ref={keyRef} className={"input"} value={addedKey} />
</div>
<NoLeftMargin className={"column is-1"}>
<Icon className={"is-hidden-mobile fa-2x"} name={copied ? "clipboard-check" : "clipboard"} title={t("apiKey.modal.clipboard")} onClick={copy} />
</NoLeftMargin>
</div>
</div>
);
return (
<Modal
body={newPassphraseModalContent}
closeFunction={close}
title={t("apiKey.modal.title")}
footer={<Button label={t("apiKey.modal.close")} action={close} />}
active={true}
/>
);
};
export default ApiKeyCreatedModal;

View File

@@ -0,0 +1,63 @@
/*
* 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 { DateFromNow, Icon } from "@scm-manager/ui-components";
import { ApiKey } from "./SetApiKeys";
import { Link } from "@scm-manager/ui-types";
import { useTranslation } from "react-i18next";
type Props = {
apiKey: ApiKey;
onDelete: (link: string) => void;
};
export const ApiKeyEntry: FC<Props> = ({ apiKey, onDelete }) => {
const [t] = useTranslation("users");
let deleteButton;
if (apiKey?._links?.delete) {
deleteButton = (
<a className="level-item" onClick={() => onDelete((apiKey._links.delete as Link).href)}>
<span className="icon is-small">
<Icon name="trash" className="fas" title={t("apiKey.delete")} />
</span>
</a>
);
}
return (
<>
<tr>
<td>{apiKey.displayName}</td>
<td>{apiKey.permissionRole}</td>
<td className="is-hidden-mobile">
<DateFromNow date={apiKey.created}/>
</td>
<td className="is-darker">{deleteButton}</td>
</tr>
</>
);
};
export default ApiKeyEntry;

View File

@@ -0,0 +1,62 @@
/*
* 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 { ApiKey, ApiKeysCollection } from "./SetApiKeys";
import ApiKeyEntry from "./ApiKeyEntry";
import { Notification } from "@scm-manager/ui-components";
type Props = {
apiKeys?: ApiKeysCollection;
onDelete: (link: string) => void;
};
const ApiKeyTable: FC<Props> = ({ apiKeys, onDelete }) => {
const [t] = useTranslation("users");
if (apiKeys?._embedded?.keys?.length === 0) {
return <Notification type="info">{t("apiKey.noStoredKeys")}</Notification>;
}
return (
<table className="card-table table is-hoverable is-fullwidth">
<thead>
<tr>
<th>{t("apiKey.displayName")}</th>
<th>{t("apiKey.permissionRole.label")}</th>
<th>{t("apiKey.created")}</th>
<th />
</tr>
</thead>
<tbody>
{apiKeys?._embedded?.keys?.map((apiKey: ApiKey, index: number) => {
return <ApiKeyEntry key={index} onDelete={onDelete} apiKey={apiKey} />;
})}
</tbody>
</table>
);
};
export default ApiKeyTable;

View File

@@ -0,0 +1,110 @@
/*
* 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 { Collection, Links, User, Me } from "@scm-manager/ui-types";
import React, { FC, useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { apiClient, ErrorNotification, Loading } from "@scm-manager/ui-components";
import ApiKeyTable from "./ApiKeyTable";
import AddApiKey from "./AddApiKey";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
export type ApiKeysCollection = Collection & {
_embedded: {
keys: ApiKey[];
};
};
export type ApiKey = {
id: string;
displayName: string;
permissionRole: string;
created: string;
_links: Links;
};
export const CONTENT_TYPE_API_KEY = "application/vnd.scmm-apiKey+json;v=2";
type Props = {
user: User | Me;
};
const Subtitle = styled.div`
margin-bottom: 1rem;
`;
const SetApiKeys: FC<Props> = ({ user }) => {
const [t] = useTranslation("users");
const [error, setError] = useState<undefined | Error>();
const [loading, setLoading] = useState(false);
const [apiKeys, setApiKeys] = useState<ApiKeysCollection | undefined>(undefined);
useEffect(() => {
fetchApiKeys();
}, [user]);
const fetchApiKeys = () => {
setLoading(true);
apiClient
.get((user._links.apiKeys as Link).href)
.then(r => r.json())
.then(setApiKeys)
.then(() => setLoading(false))
.catch(setError);
};
const onDelete = (link: string) => {
apiClient
.delete(link)
.then(fetchApiKeys)
.catch(setError);
};
const createLink = (apiKeys?._links?.create as Link)?.href;
if (error) {
return <ErrorNotification error={error} />;
}
if (loading) {
return <Loading />;
}
return (
<>
<div className={"media-content"}>
<p>{t("apiKey.text1")} <Link to={"/admin/roles/"}>{t("apiKey.manageRoles")}</Link></p>
<p>{t("apiKey.text2")}</p>
</div>
<hr />
<ApiKeyTable apiKeys={apiKeys} onDelete={onDelete} />
<hr />
<Subtitle className={"media-content"}><h2 className={"title is-4"}>Create new key</h2></Subtitle>
{createLink && <AddApiKey createLink={createLink} refresh={fetchApiKeys} />}
</>
);
};
export default SetApiKeys;

View File

@@ -0,0 +1,43 @@
/*
* 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 { Link, User, Me } from "@scm-manager/ui-types";
import { NavLink } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
type Props = {
user: User | Me;
apiKeyUrl: string;
};
const SetApiKeyNavLink: FC<Props> = ({ user, apiKeyUrl }) => {
const [t] = useTranslation("users");
if ((user?._links?.apiKeys as Link)?.href) {
return <NavLink to={apiKeyUrl} label={t("singleUser.menu.setApiKeyNavLink")} />;
}
return null;
};
export default SetApiKeyNavLink;

View File

@@ -21,80 +21,78 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from "react";
import React, { FC, useState } from "react";
import { connect } from "react-redux";
import { compose } from "redux";
import { withRouter } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next";
import { History } from "history";
import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { User } from "@scm-manager/ui-types";
import { confirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { ConfirmAlert, DeleteButton, ErrorNotification, Level } from "@scm-manager/ui-components";
import { deleteUser, getDeleteUserFailure, isDeleteUserPending } from "../modules/users";
type Props = WithTranslation & {
type Props = {
loading: boolean;
error: Error;
user: User;
confirmDialog?: boolean;
deleteUser: (user: User, callback?: () => void) => void;
// context props
history: History;
};
class DeleteUser extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
const DeleteUser: FC<Props> = ({ confirmDialog = true, loading, error, user, deleteUser }) => {
const [showConfirmAlert, setShowConfirmAlert] = useState(false);
const [t] = useTranslation("users");
const history = useHistory();
const userDeleted = () => {
history.push("/users/");
};
userDeleted = () => {
this.props.history.push("/users/");
const deleteUserCallback = () => {
deleteUser(user, userDeleted);
};
deleteUser = () => {
this.props.deleteUser(this.props.user, this.userDeleted);
const confirmDelete = () => {
setShowConfirmAlert(true);
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("deleteUser.confirmAlert.title"),
message: t("deleteUser.confirmAlert.message"),
buttons: [
{
className: "is-outlined",
label: t("deleteUser.confirmAlert.submit"),
onClick: () => this.deleteUser()
},
{
label: t("deleteUser.confirmAlert.cancel"),
onClick: () => null
}
]
});
const isDeletable = () => {
return user._links.delete;
};
isDeletable = () => {
return this.props.user._links.delete;
};
const action = confirmDialog ? confirmDelete : deleteUserCallback;
render() {
const { loading, error, confirmDialog, t } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deleteUser;
if (!this.isDeletable()) {
return null;
}
if (!isDeletable()) {
return null;
}
if (showConfirmAlert) {
return (
<>
<hr />
<ErrorNotification error={error} />
<Level right={<DeleteButton label={t("deleteUser.button")} action={action} loading={loading} />} />
</>
<ConfirmAlert
title={t("deleteUser.confirmAlert.title")}
message={t("deleteUser.confirmAlert.message")}
buttons={[
{
className: "is-outlined",
label: t("deleteUser.confirmAlert.submit"),
onClick: () => deleteUserCallback()
},
{
label: t("deleteUser.confirmAlert.cancel"),
onClick: () => null
}
]}
close={() => setShowConfirmAlert(false)}
/>
);
}
}
return (
<>
<hr />
<ErrorNotification error={error} />
<Level right={<DeleteButton label={t("deleteUser.button")} action={action} loading={loading} />} />
</>
);
};
const mapStateToProps = (state: any, ownProps: Props) => {
const loading = isDeleteUserPending(state, ownProps.user.name);
@@ -113,4 +111,4 @@ const mapDispatchToProps = (dispatch: any) => {
};
};
export default compose(connect(mapStateToProps, mapDispatchToProps), withRouter, withTranslation("users"))(DeleteUser);
export default connect(mapStateToProps, mapDispatchToProps)(DeleteUser);