Merge with 2.0.0-m3

This commit is contained in:
René Pfeuffer
2019-01-31 09:42:16 +01:00
135 changed files with 5997 additions and 1630 deletions

View File

@@ -0,0 +1,31 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Checkbox } from "@scm-manager/ui-components";
type Props = {
t: string => string,
disabled: boolean,
name: string,
checked: boolean,
onChange?: (value: boolean, name?: string) => void
};
class PermissionCheckbox extends React.Component<Props> {
render() {
const { t } = this.props;
return (
<Checkbox
key={this.props.name}
name={this.props.name}
helpText={t("verbs.repository." + this.props.name + ".description")}
label={t("verbs.repository." + this.props.name + ".displayName")}
checked={this.props.checked}
onChange={this.props.onChange}
disabled={this.props.disabled}
/>
);
}
}
export default translate("plugins")(PermissionCheckbox);

View File

@@ -0,0 +1,55 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Select } from "@scm-manager/ui-components";
type Props = {
t: string => string,
availableRoles: string[],
handleRoleChange: string => void,
role: string,
label?: string,
helpText?: string,
loading?: boolean
};
class RoleSelector extends React.Component<Props> {
render() {
const {
availableRoles,
role,
handleRoleChange,
loading,
label,
helpText
} = this.props;
if (!availableRoles) return null;
const options = role
? this.createSelectOptions(availableRoles)
: ["", ...this.createSelectOptions(availableRoles)];
return (
<Select
onChange={handleRoleChange}
value={role ? role : ""}
options={options}
loading={loading}
label={label}
helpText={helpText}
/>
);
}
createSelectOptions(roles: string[]) {
return roles.map(role => {
return {
label: role,
value: role
};
});
}
}
export default translate("repos")(RoleSelector);

View File

@@ -1,42 +0,0 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Select } from "@scm-manager/ui-components";
type Props = {
t: string => string,
handleTypeChange: string => void,
type: string,
label?: string,
helpText?: string,
loading?: boolean
};
class TypeSelector extends React.Component<Props> {
render() {
const { type, handleTypeChange, loading, label, helpText } = this.props;
const types = ["READ", "OWNER", "WRITE"];
return (
<Select
onChange={handleTypeChange}
value={type ? type : "READ"}
options={this.createSelectOptions(types)}
loading={loading}
label={label}
helpText={helpText}
/>
);
}
createSelectOptions(types: string[]) {
return types.map(type => {
return {
label: type,
value: type
};
});
}
}
export default translate("repos")(TypeSelector);

View File

@@ -18,7 +18,8 @@ describe("permission validation", () => {
name: "PermissionName",
groupPermission: true,
type: "READ",
_links: {}
_links: {},
verbs: []
}
];
const name = "PermissionName";
@@ -35,7 +36,8 @@ describe("permission validation", () => {
name: "PermissionName",
groupPermission: false,
type: "READ",
_links: {}
_links: {},
verbs: []
}
];
const name = "PermissionName";

View File

@@ -0,0 +1,96 @@
// @flow
import React from "react";
import { Button, SubmitButton } from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import PermissionCheckbox from "../components/PermissionCheckbox";
type Props = {
readOnly: boolean,
availableVerbs: string[],
selectedVerbs: string[],
onSubmit: (string[]) => void,
onClose: () => void,
// context props
t: string => string
};
type State = {
verbs: any
};
class AdvancedPermissionsDialog extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
const verbs = {};
props.availableVerbs.forEach(
verb => (verbs[verb] = props.selectedVerbs.includes(verb))
);
this.state = { verbs };
}
render() {
const { t, onClose, readOnly } = this.props;
const { verbs } = this.state;
const verbSelectBoxes = Object.entries(verbs).map(e => (
<PermissionCheckbox
disabled={readOnly}
name={e[0]}
checked={e[1]}
onChange={this.handleChange}
/>
));
const submitButton = !readOnly ? (
<SubmitButton label={t("permission.advanced.dialog.submit")} />
) : null;
return (
<div className={"modal is-active"}>
<div className="modal-background" />
<div className="modal-card">
<header className="modal-card-head">
<p className="modal-card-title">
{t("permission.advanced.dialog.title")}
</p>
<button
className="delete"
aria-label="close"
onClick={() => onClose()}
/>
</header>
<section className="modal-card-body">
<div className="content">{verbSelectBoxes}</div>
<form onSubmit={this.onSubmit}>
{submitButton}
<Button
label={t("permission.advanced.dialog.abort")}
action={onClose}
/>
</form>
</section>
</div>
</div>
);
}
handleChange = (value: boolean, name: string) => {
const { verbs } = this.state;
const newVerbs = { ...verbs, [name]: value };
this.setState({ verbs: newVerbs });
};
onSubmit = () => {
this.props.onSubmit(
Object.entries(this.state.verbs)
.filter(e => e[1])
.map(e => e[0])
);
};
}
export default translate("repos")(AdvancedPermissionsDialog);

View File

@@ -1,17 +1,26 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Autocomplete, Radio, SubmitButton } from "@scm-manager/ui-components";
import TypeSelector from "./TypeSelector";
import {
Autocomplete,
SubmitButton,
Button,
LabelWithHelpIcon
} from "@scm-manager/ui-components";
import RoleSelector from "../components/RoleSelector";
import type {
AvailableRepositoryPermissions,
PermissionCollection,
PermissionCreateEntry,
SelectValue
} from "@scm-manager/ui-types";
import * as validator from "./permissionValidation";
import * as validator from "../components/permissionValidation";
import { findMatchingRoleName } from "../modules/permissions";
import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog";
type Props = {
t: string => string,
availablePermissions: AvailableRepositoryPermissions,
createPermission: (permission: PermissionCreateEntry) => void,
loading: boolean,
currentPermissions: PermissionCollection,
@@ -21,10 +30,11 @@ type Props = {
type State = {
name: string,
type: string,
verbs: string[],
groupPermission: boolean,
valid: boolean,
value?: SelectValue
value?: SelectValue,
showAdvancedDialog: boolean
};
class CreatePermissionForm extends React.Component<Props, State> {
@@ -33,10 +43,11 @@ class CreatePermissionForm extends React.Component<Props, State> {
this.state = {
name: "",
type: "READ",
verbs: props.availablePermissions.availableRoles[0].verbs,
groupPermission: false,
valid: true,
value: undefined
value: undefined,
showAdvancedDialog: false
};
}
@@ -121,9 +132,23 @@ class CreatePermissionForm extends React.Component<Props, State> {
};
render() {
const { t, loading } = this.props;
const { t, availablePermissions, loading } = this.props;
const { type } = this.state;
const { verbs, showAdvancedDialog } = this.state;
const availableRoleNames = availablePermissions.availableRoles.map(
r => r.name
);
const matchingRole = findMatchingRoleName(availablePermissions, verbs);
const advancedDialog = showAdvancedDialog ? (
<AdvancedPermissionsDialog
availableVerbs={availablePermissions.availableVerbs}
selectedVerbs={verbs}
onClose={this.closeAdvancedPermissionsDialog}
onSubmit={this.submitAdvancedPermissionsDialog}
/>
) : null;
return (
<div>
@@ -131,32 +156,57 @@ class CreatePermissionForm extends React.Component<Props, State> {
<h2 className="subtitle">
{t("permission.add-permission.add-permission-heading")}
</h2>
{advancedDialog}
<form onSubmit={this.submit}>
<Radio
name="permission_scope"
value="USER_PERMISSION"
checked={!this.state.groupPermission}
label={t("permission.user-permission")}
onChange={this.permissionScopeChanged}
/>
<Radio
name="permission_scope"
value="GROUP_PERMISSION"
checked={this.state.groupPermission}
label={t("permission.group-permission")}
onChange={this.permissionScopeChanged}
/>
<div className="control">
<label className="radio">
<input
type="radio"
name="permission_scope"
checked={!this.state.groupPermission}
value="USER_PERMISSION"
onChange={this.permissionScopeChanged}
/>
{t("permission.user-permission")}
</label>
<label className="radio">
<input
type="radio"
name="permission_scope"
value="GROUP_PERMISSION"
checked={this.state.groupPermission}
onChange={this.permissionScopeChanged}
/>
{t("permission.group-permission")}
</label>
</div>
<div className="columns">
<div className="column is-three-quarters">
<div className="column is-two-thirds">
{this.renderAutocompletionField()}
</div>
<div className="column is-one-quarter">
<TypeSelector
label={t("permission.type")}
helpText={t("permission.help.typeHelpText")}
handleTypeChange={this.handleTypeChange}
type={type ? type : "READ"}
/>
<div className="column is-one-third">
<div className="columns">
<div className="column is-half">
<RoleSelector
availableRoles={availableRoleNames}
label={t("permission.role")}
helpText={t("permission.help.roleHelpText")}
handleRoleChange={this.handleRoleChange}
role={matchingRole}
/>
</div>
<div className="column is-half">
<LabelWithHelpIcon
label={t("permission.permissions")}
helpText={t("permission.help.permissionsHelpText")}
/>
<Button
label={t("permission.advanced-button.label")}
action={this.handleDetailedPermissionsPressed}
/>
</div>
</div>
</div>
</div>
<div className="columns">
@@ -173,10 +223,25 @@ class CreatePermissionForm extends React.Component<Props, State> {
);
}
handleDetailedPermissionsPressed = () => {
this.setState({ showAdvancedDialog: true });
};
closeAdvancedPermissionsDialog = () => {
this.setState({ showAdvancedDialog: false });
};
submitAdvancedPermissionsDialog = (newVerbs: string[]) => {
this.setState({
showAdvancedDialog: false,
verbs: newVerbs
});
};
submit = e => {
this.props.createPermission({
name: this.state.name,
type: this.state.type,
verbs: this.state.verbs,
groupPermission: this.state.groupPermission
});
this.removeState();
@@ -186,17 +251,24 @@ class CreatePermissionForm extends React.Component<Props, State> {
removeState = () => {
this.setState({
name: "",
type: "READ",
verbs: this.props.availablePermissions.availableRoles[0].verbs,
groupPermission: false,
valid: true
});
};
handleTypeChange = (type: string) => {
handleRoleChange = (role: string) => {
const selectedRole = this.findAvailableRole(role);
this.setState({
type: type
verbs: selectedRole.verbs
});
};
findAvailableRole = (roleName: string) => {
return this.props.availablePermissions.availableRoles.find(
role => role.name === roleName
);
};
}
export default translate("repos")(CreatePermissionForm);

View File

@@ -1,225 +1,268 @@
//@flow
import React from "react";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import {
fetchPermissions,
getFetchPermissionsFailure,
isFetchPermissionsPending,
getPermissionsOfRepo,
hasCreatePermission,
createPermission,
isCreatePermissionPending,
getCreatePermissionFailure,
createPermissionReset,
getDeletePermissionsFailure,
getModifyPermissionsFailure,
modifyPermissionReset,
deletePermissionReset
} from "../modules/permissions";
import { Loading, ErrorPage } from "@scm-manager/ui-components";
import type {
Permission,
PermissionCollection,
PermissionCreateEntry
} from "@scm-manager/ui-types";
import SinglePermission from "./SinglePermission";
import CreatePermissionForm from "../components/CreatePermissionForm";
import type { History } from "history";
import { getPermissionsLink } from "../../modules/repos";
import {
getGroupAutoCompleteLink,
getUserAutoCompleteLink
} from "../../../modules/indexResource";
type Props = {
namespace: string,
repoName: string,
loading: boolean,
error: Error,
permissions: PermissionCollection,
hasPermissionToCreate: boolean,
loadingCreatePermission: boolean,
permissionsLink: string,
groupAutoCompleteLink: string,
userAutoCompleteLink: string,
//dispatch functions
fetchPermissions: (link: string, namespace: string, repoName: string) => void,
createPermission: (
link: string,
permission: PermissionCreateEntry,
namespace: string,
repoName: string,
callback?: () => void
) => void,
createPermissionReset: (string, string) => void,
modifyPermissionReset: (string, string) => void,
deletePermissionReset: (string, string) => void,
// context props
t: string => string,
match: any,
history: History
};
class Permissions extends React.Component<Props> {
componentDidMount() {
const {
fetchPermissions,
namespace,
repoName,
modifyPermissionReset,
createPermissionReset,
deletePermissionReset,
permissionsLink
} = this.props;
createPermissionReset(namespace, repoName);
modifyPermissionReset(namespace, repoName);
deletePermissionReset(namespace, repoName);
fetchPermissions(permissionsLink, namespace, repoName);
}
createPermission = (permission: Permission) => {
this.props.createPermission(
this.props.permissionsLink,
permission,
this.props.namespace,
this.props.repoName
);
};
render() {
const {
loading,
error,
permissions,
t,
namespace,
repoName,
loadingCreatePermission,
hasPermissionToCreate,
userAutoCompleteLink,
groupAutoCompleteLink
} = this.props;
if (error) {
return (
<ErrorPage
title={t("permission.error-title")}
subtitle={t("permission.error-subtitle")}
error={error}
/>
);
}
if (loading || !permissions) {
return <Loading />;
}
const createPermissionForm = hasPermissionToCreate ? (
<CreatePermissionForm
createPermission={permission => this.createPermission(permission)}
loading={loadingCreatePermission}
currentPermissions={permissions}
userAutoCompleteLink={userAutoCompleteLink}
groupAutoCompleteLink={groupAutoCompleteLink}
/>
) : null;
return (
<div>
<table className="has-background-light table is-hoverable is-fullwidth">
<thead>
<tr>
<th>{t("permission.name")}</th>
<th className="is-hidden-mobile">
{t("permission.group-permission")}
</th>
<th>{t("permission.type")}</th>
<th />
</tr>
</thead>
<tbody>
{permissions.map(permission => {
return (
<SinglePermission
key={permission.name + permission.groupPermission.toString()}
namespace={namespace}
repoName={repoName}
permission={permission}
/>
);
})}
</tbody>
</table>
{createPermissionForm}
</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
const namespace = ownProps.namespace;
const repoName = ownProps.repoName;
const error =
getFetchPermissionsFailure(state, namespace, repoName) ||
getCreatePermissionFailure(state, namespace, repoName) ||
getDeletePermissionsFailure(state, namespace, repoName) ||
getModifyPermissionsFailure(state, namespace, repoName);
const loading = isFetchPermissionsPending(state, namespace, repoName);
const permissions = getPermissionsOfRepo(state, namespace, repoName);
const loadingCreatePermission = isCreatePermissionPending(
state,
namespace,
repoName
);
const hasPermissionToCreate = hasCreatePermission(state, namespace, repoName);
const permissionsLink = getPermissionsLink(state, namespace, repoName);
const groupAutoCompleteLink = getGroupAutoCompleteLink(state);
const userAutoCompleteLink = getUserAutoCompleteLink(state);
return {
namespace,
repoName,
error,
loading,
permissions,
hasPermissionToCreate,
loadingCreatePermission,
permissionsLink,
groupAutoCompleteLink,
userAutoCompleteLink
};
};
const mapDispatchToProps = dispatch => {
return {
fetchPermissions: (link: string, namespace: string, repoName: string) => {
dispatch(fetchPermissions(link, namespace, repoName));
},
createPermission: (
link: string,
permission: PermissionCreateEntry,
namespace: string,
repoName: string,
callback?: () => void
) => {
dispatch(
createPermission(link, permission, namespace, repoName, callback)
);
},
createPermissionReset: (namespace: string, repoName: string) => {
dispatch(createPermissionReset(namespace, repoName));
},
modifyPermissionReset: (namespace: string, repoName: string) => {
dispatch(modifyPermissionReset(namespace, repoName));
},
deletePermissionReset: (namespace: string, repoName: string) => {
dispatch(deletePermissionReset(namespace, repoName));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(translate("repos")(Permissions));
//@flow
import React from "react";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import {
fetchAvailablePermissionsIfNeeded,
fetchPermissions,
getFetchAvailablePermissionsFailure,
getAvailablePermissions,
getFetchPermissionsFailure,
isFetchAvailablePermissionsPending,
isFetchPermissionsPending,
getPermissionsOfRepo,
hasCreatePermission,
createPermission,
isCreatePermissionPending,
getCreatePermissionFailure,
createPermissionReset,
getDeletePermissionsFailure,
getModifyPermissionsFailure,
modifyPermissionReset,
deletePermissionReset
} from "../modules/permissions";
import {
Loading,
ErrorPage,
LabelWithHelpIcon
} from "@scm-manager/ui-components";
import type {
AvailableRepositoryPermissions,
Permission,
PermissionCollection,
PermissionCreateEntry
} from "@scm-manager/ui-types";
import SinglePermission from "./SinglePermission";
import CreatePermissionForm from "./CreatePermissionForm";
import type { History } from "history";
import { getPermissionsLink } from "../../modules/repos";
import {
getGroupAutoCompleteLink,
getUserAutoCompleteLink
} from "../../../modules/indexResource";
type Props = {
availablePermissions: AvailableRepositoryPermissions,
namespace: string,
repoName: string,
loading: boolean,
error: Error,
permissions: PermissionCollection,
hasPermissionToCreate: boolean,
loadingCreatePermission: boolean,
permissionsLink: string,
groupAutoCompleteLink: string,
userAutoCompleteLink: string,
//dispatch functions
fetchAvailablePermissionsIfNeeded: () => void,
fetchPermissions: (link: string, namespace: string, repoName: string) => void,
createPermission: (
link: string,
permission: PermissionCreateEntry,
namespace: string,
repoName: string,
callback?: () => void
) => void,
createPermissionReset: (string, string) => void,
modifyPermissionReset: (string, string) => void,
deletePermissionReset: (string, string) => void,
// context props
t: string => string,
match: any,
history: History
};
class Permissions extends React.Component<Props> {
componentDidMount() {
const {
fetchAvailablePermissionsIfNeeded,
fetchPermissions,
namespace,
repoName,
modifyPermissionReset,
createPermissionReset,
deletePermissionReset,
permissionsLink
} = this.props;
createPermissionReset(namespace, repoName);
modifyPermissionReset(namespace, repoName);
deletePermissionReset(namespace, repoName);
fetchAvailablePermissionsIfNeeded();
fetchPermissions(permissionsLink, namespace, repoName);
}
createPermission = (permission: Permission) => {
this.props.createPermission(
this.props.permissionsLink,
permission,
this.props.namespace,
this.props.repoName
);
};
render() {
const {
availablePermissions,
loading,
error,
permissions,
t,
namespace,
repoName,
loadingCreatePermission,
hasPermissionToCreate,
userAutoCompleteLink,
groupAutoCompleteLink
} = this.props;
if (error) {
return (
<ErrorPage
title={t("permission.error-title")}
subtitle={t("permission.error-subtitle")}
error={error}
/>
);
}
if (loading || !permissions || !availablePermissions) {
return <Loading />;
}
const createPermissionForm = hasPermissionToCreate ? (
<CreatePermissionForm
availablePermissions={availablePermissions}
createPermission={permission => this.createPermission(permission)}
loading={loadingCreatePermission}
currentPermissions={permissions}
userAutoCompleteLink={userAutoCompleteLink}
groupAutoCompleteLink={groupAutoCompleteLink}
/>
) : null;
return (
<div>
<table className="has-background-light table is-hoverable is-fullwidth">
<thead>
<tr>
<th>
<LabelWithHelpIcon
label={t("permission.name")}
helpText={t("permission.help.nameHelpText")}
/>
</th>
<th className="is-hidden-mobile">
<LabelWithHelpIcon
label={t("permission.group-permission")}
helpText={t("permission.help.groupPermissionHelpText")}
/>
</th>
<th>
<LabelWithHelpIcon
label={t("permission.role")}
helpText={t("permission.help.roleHelpText")}
/>
</th>
<th>
<LabelWithHelpIcon
label={t("permission.permissions")}
helpText={t("permission.help.permissionsHelpText")}
/>
</th>
<th />
</tr>
</thead>
<tbody>
{permissions.map(permission => {
return (
<SinglePermission
availablePermissions={availablePermissions}
key={permission.name + permission.groupPermission.toString()}
namespace={namespace}
repoName={repoName}
permission={permission}
/>
);
})}
</tbody>
</table>
{createPermissionForm}
</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
const namespace = ownProps.namespace;
const repoName = ownProps.repoName;
const error =
getFetchPermissionsFailure(state, namespace, repoName) ||
getCreatePermissionFailure(state, namespace, repoName) ||
getDeletePermissionsFailure(state, namespace, repoName) ||
getModifyPermissionsFailure(state, namespace, repoName) ||
getFetchAvailablePermissionsFailure(state);
const loading =
isFetchPermissionsPending(state, namespace, repoName) ||
isFetchAvailablePermissionsPending(state);
const permissions = getPermissionsOfRepo(state, namespace, repoName);
const loadingCreatePermission = isCreatePermissionPending(
state,
namespace,
repoName
);
const hasPermissionToCreate = hasCreatePermission(state, namespace, repoName);
const permissionsLink = getPermissionsLink(state, namespace, repoName);
const groupAutoCompleteLink = getGroupAutoCompleteLink(state);
const userAutoCompleteLink = getUserAutoCompleteLink(state);
const availablePermissions = getAvailablePermissions(state);
return {
availablePermissions,
namespace,
repoName,
error,
loading,
permissions,
hasPermissionToCreate,
loadingCreatePermission,
permissionsLink,
groupAutoCompleteLink,
userAutoCompleteLink
};
};
const mapDispatchToProps = dispatch => {
return {
fetchPermissions: (link: string, namespace: string, repoName: string) => {
dispatch(fetchPermissions(link, namespace, repoName));
},
fetchAvailablePermissionsIfNeeded: () => {
dispatch(fetchAvailablePermissionsIfNeeded());
},
createPermission: (
link: string,
permission: PermissionCreateEntry,
namespace: string,
repoName: string,
callback?: () => void
) => {
dispatch(
createPermission(link, permission, namespace, repoName, callback)
);
},
createPermissionReset: (namespace: string, repoName: string) => {
dispatch(createPermissionReset(namespace, repoName));
},
modifyPermissionReset: (namespace: string, repoName: string) => {
dispatch(modifyPermissionReset(namespace, repoName));
},
deletePermissionReset: (namespace: string, repoName: string) => {
dispatch(deletePermissionReset(namespace, repoName));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(translate("repos")(Permissions));

View File

@@ -1,176 +1,256 @@
// @flow
import React from "react";
import type { Permission } from "@scm-manager/ui-types";
import { translate } from "react-i18next";
import {
modifyPermission,
isModifyPermissionPending,
deletePermission,
isDeletePermissionPending
} from "../modules/permissions";
import { connect } from "react-redux";
import type { History } from "history";
import { Checkbox } from "@scm-manager/ui-components";
import DeletePermissionButton from "../components/buttons/DeletePermissionButton";
import TypeSelector from "../components/TypeSelector";
type Props = {
submitForm: Permission => void,
modifyPermission: (Permission, string, string) => void,
permission: Permission,
t: string => string,
namespace: string,
repoName: string,
match: any,
history: History,
loading: boolean,
deletePermission: (Permission, string, string) => void,
deleteLoading: boolean
};
type State = {
permission: Permission
};
class SinglePermission extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
permission: {
name: "",
type: "READ",
groupPermission: false,
_links: {}
}
};
}
componentDidMount() {
const { permission } = this.props;
if (permission) {
this.setState({
permission: {
name: permission.name,
type: permission.type,
groupPermission: permission.groupPermission,
_links: permission._links
}
});
}
}
deletePermission = () => {
this.props.deletePermission(
this.props.permission,
this.props.namespace,
this.props.repoName
);
};
render() {
const { permission } = this.state;
const { loading, namespace, repoName } = this.props;
const typeSelector =
this.props.permission._links && this.props.permission._links.update ? (
<td>
<TypeSelector
handleTypeChange={this.handleTypeChange}
type={permission.type ? permission.type : "READ"}
loading={loading}
/>
</td>
) : (
<td>{permission.type}</td>
);
return (
<tr>
<td>{permission.name}</td>
<td>
<Checkbox checked={permission ? permission.groupPermission : false} />
</td>
{typeSelector}
<td>
<DeletePermissionButton
permission={permission}
namespace={namespace}
repoName={repoName}
deletePermission={this.deletePermission}
loading={this.props.deleteLoading}
/>
</td>
</tr>
);
}
handleTypeChange = (type: string) => {
this.setState({
permission: {
...this.state.permission,
type: type
}
});
this.modifyPermission(type);
};
modifyPermission = (type: string) => {
let permission = this.state.permission;
permission.type = type;
this.props.modifyPermission(
permission,
this.props.namespace,
this.props.repoName
);
};
createSelectOptions(types: string[]) {
return types.map(type => {
return {
label: type,
value: type
};
});
}
}
const mapStateToProps = (state, ownProps) => {
const permission = ownProps.permission;
const loading = isModifyPermissionPending(
state,
ownProps.namespace,
ownProps.repoName,
permission
);
const deleteLoading = isDeletePermissionPending(
state,
ownProps.namespace,
ownProps.repoName,
permission
);
return { loading, deleteLoading };
};
const mapDispatchToProps = dispatch => {
return {
modifyPermission: (
permission: Permission,
namespace: string,
repoName: string
) => {
dispatch(modifyPermission(permission, namespace, repoName));
},
deletePermission: (
permission: Permission,
namespace: string,
repoName: string
) => {
dispatch(deletePermission(permission, namespace, repoName));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(translate("repos")(SinglePermission));
// @flow
import React from "react";
import type {
AvailableRepositoryPermissions,
Permission
} from "@scm-manager/ui-types";
import { translate } from "react-i18next";
import {
modifyPermission,
isModifyPermissionPending,
deletePermission,
isDeletePermissionPending,
findMatchingRoleName
} from "../modules/permissions";
import { connect } from "react-redux";
import type { History } from "history";
import { Button, Checkbox } from "@scm-manager/ui-components";
import DeletePermissionButton from "../components/buttons/DeletePermissionButton";
import RoleSelector from "../components/RoleSelector";
import AdvancedPermissionsDialog from "./AdvancedPermissionsDialog";
type Props = {
availablePermissions: AvailableRepositoryPermissions,
submitForm: Permission => void,
modifyPermission: (permission: Permission, namespace: string, name: string) => void,
permission: Permission,
t: string => string,
namespace: string,
repoName: string,
match: any,
history: History,
loading: boolean,
deletePermission: (permission: Permission, namespace: string, name: string) => void,
deleteLoading: boolean
};
type State = {
role: string,
permission: Permission,
showAdvancedDialog: boolean
};
class SinglePermission extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
const defaultPermission = props.availablePermissions.availableRoles
? props.availablePermissions.availableRoles[0]
: {};
this.state = {
permission: {
name: "",
verbs: defaultPermission.verbs,
groupPermission: false,
_links: {}
},
role: defaultPermission.name,
showAdvancedDialog: false
};
}
componentDidMount() {
const { availablePermissions, permission } = this.props;
const matchingRole = findMatchingRoleName(
availablePermissions,
permission.verbs
);
if (permission) {
this.setState({
permission: {
name: permission.name,
verbs: permission.verbs,
groupPermission: permission.groupPermission,
_links: permission._links
},
role: matchingRole
});
}
}
deletePermission = () => {
this.props.deletePermission(
this.props.permission,
this.props.namespace,
this.props.repoName
);
};
render() {
const { role, permission, showAdvancedDialog } = this.state;
const {
t,
availablePermissions,
loading,
namespace,
repoName
} = this.props;
const availableRoleNames = availablePermissions.availableRoles.map(
r => r.name
);
const readOnly = !this.mayChangePermissions();
const roleSelector = readOnly ? (
<td>{role}</td>
) : (
<td>
<RoleSelector
handleRoleChange={this.handleRoleChange}
availableRoles={availableRoleNames}
role={role}
loading={loading}
/>
</td>
);
const advancedDialg = showAdvancedDialog ? (
<AdvancedPermissionsDialog
readOnly={readOnly}
availableVerbs={availablePermissions.availableVerbs}
selectedVerbs={permission.verbs}
onClose={this.closeAdvancedPermissionsDialog}
onSubmit={this.submitAdvancedPermissionsDialog}
/>
) : null;
return (
<tr>
<td>{permission.name}</td>
<td>
<Checkbox
checked={permission ? permission.groupPermission : false}
disabled={true}
/>
</td>
{roleSelector}
<td>
<Button
label={t("permission.advanced-button.label")}
action={this.handleDetailedPermissionsPressed}
/>
</td>
<td>
<DeletePermissionButton
permission={permission}
namespace={namespace}
repoName={repoName}
deletePermission={this.deletePermission}
loading={this.props.deleteLoading}
/>
{advancedDialg}
</td>
</tr>
);
}
mayChangePermissions = () => {
return this.props.permission._links && this.props.permission._links.update;
};
handleDetailedPermissionsPressed = () => {
this.setState({ showAdvancedDialog: true });
};
closeAdvancedPermissionsDialog = () => {
this.setState({ showAdvancedDialog: false });
};
submitAdvancedPermissionsDialog = (newVerbs: string[]) => {
const { permission } = this.state;
const newRole = findMatchingRoleName(
this.props.availablePermissions,
newVerbs
);
this.setState(
{
showAdvancedDialog: false,
permission: { ...permission, verbs: newVerbs },
role: newRole
},
() => this.modifyPermission(newVerbs)
);
};
handleRoleChange = (role: string) => {
const selectedRole = this.findAvailableRole(role);
this.setState(
{
permission: {
...this.state.permission,
verbs: selectedRole.verbs
},
role: role
},
() => this.modifyPermission(selectedRole.verbs)
);
};
findAvailableRole = (roleName: string) => {
return this.props.availablePermissions.availableRoles.find(
role => role.name === roleName
);
};
modifyPermission = (verbs: string[]) => {
let permission = this.state.permission;
permission.verbs = verbs;
this.props.modifyPermission(
permission,
this.props.namespace,
this.props.repoName
);
};
}
const mapStateToProps = (state, ownProps) => {
const permission = ownProps.permission;
const loading = isModifyPermissionPending(
state,
ownProps.namespace,
ownProps.repoName,
permission
);
const deleteLoading = isDeletePermissionPending(
state,
ownProps.namespace,
ownProps.repoName,
permission
);
return { loading, deleteLoading };
};
const mapDispatchToProps = dispatch => {
return {
modifyPermission: (
permission: Permission,
namespace: string,
repoName: string
) => {
dispatch(modifyPermission(permission, namespace, repoName));
},
deletePermission: (
permission: Permission,
namespace: string,
repoName: string
) => {
dispatch(deletePermission(permission, namespace, repoName));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(translate("repos")(SinglePermission));

View File

@@ -4,6 +4,7 @@ import type { Action } from "@scm-manager/ui-components";
import { apiClient } from "@scm-manager/ui-components";
import * as types from "../../../modules/types";
import type {
AvailableRepositoryPermissions,
Permission,
PermissionCollection,
PermissionCreateEntry
@@ -11,7 +12,18 @@ import type {
import { isPending } from "../../../modules/pending";
import { getFailure } from "../../../modules/failure";
import { Dispatch } from "redux";
import { getLinks } from "../../../modules/indexResource";
export const FETCH_AVAILABLE = "scm/permissions/FETCH_AVAILABLE";
export const FETCH_AVAILABLE_PENDING = `${FETCH_AVAILABLE}_${
types.PENDING_SUFFIX
}`;
export const FETCH_AVAILABLE_SUCCESS = `${FETCH_AVAILABLE}_${
types.SUCCESS_SUFFIX
}`;
export const FETCH_AVAILABLE_FAILURE = `${FETCH_AVAILABLE}_${
types.FAILURE_SUFFIX
}`;
export const FETCH_PERMISSIONS = "scm/permissions/FETCH_PERMISSIONS";
export const FETCH_PERMISSIONS_PENDING = `${FETCH_PERMISSIONS}_${
types.PENDING_SUFFIX
@@ -62,7 +74,71 @@ export const DELETE_PERMISSION_RESET = `${DELETE_PERMISSION}_${
types.RESET_SUFFIX
}`;
const CONTENT_TYPE = "application/vnd.scmm-permission+json";
const CONTENT_TYPE = "application/vnd.scmm-repositoryPermission+json";
// fetch available permissions
export function fetchAvailablePermissionsIfNeeded() {
return function(dispatch: any, getState: () => Object) {
if (shouldFetchAvailablePermissions(getState())) {
return fetchAvailablePermissions(dispatch, getState);
}
};
}
export function fetchAvailablePermissions(
dispatch: any,
getState: () => Object
) {
dispatch(fetchAvailablePending());
return apiClient
.get(getLinks(getState()).availableRepositoryPermissions.href)
.then(response => response.json())
.then(available => {
dispatch(fetchAvailableSuccess(available));
})
.catch(err => {
dispatch(fetchAvailableFailure(err));
});
}
export function shouldFetchAvailablePermissions(state: Object) {
if (
isFetchAvailablePermissionsPending(state) ||
getFetchAvailablePermissionsFailure(state)
) {
return false;
}
return !state.available;
}
export function fetchAvailablePending(): Action {
return {
type: FETCH_AVAILABLE_PENDING,
payload: {},
itemId: "available"
};
}
export function fetchAvailableSuccess(
available: AvailableRepositoryPermissions
): Action {
return {
type: FETCH_AVAILABLE_SUCCESS,
payload: available,
itemId: "available"
};
}
export function fetchAvailableFailure(error: Error): Action {
return {
type: FETCH_AVAILABLE_FAILURE,
payload: {
error
},
itemId: "available"
};
}
// fetch permissions
@@ -368,6 +444,7 @@ export function deletePermissionReset(namespace: string, repoName: string) {
itemId: namespace + "/" + repoName
};
}
function deletePermissionFromState(
oldPermissions: PermissionCollection,
permission: Permission
@@ -399,12 +476,17 @@ export default function reducer(
return state;
}
switch (action.type) {
case FETCH_AVAILABLE_SUCCESS:
return {
...state,
available: action.payload
};
case FETCH_PERMISSIONS_SUCCESS:
return {
...state,
[action.itemId]: {
entries: action.payload._embedded.permissions,
createPermission: action.payload._links.create ? true : false
createPermission: !!action.payload._links.create
}
};
case MODIFY_PERMISSION_SUCCESS:
@@ -452,6 +534,12 @@ export default function reducer(
// selectors
export function getAvailablePermissions(state: Object) {
if (state.permissions) {
return state.permissions.available;
}
}
export function getPermissionsOfRepo(
state: Object,
namespace: string,
@@ -463,6 +551,10 @@ export function getPermissionsOfRepo(
}
}
export function isFetchAvailablePermissionsPending(state: Object) {
return isPending(state, FETCH_AVAILABLE, "available");
}
export function isFetchPermissionsPending(
state: Object,
namespace: string,
@@ -471,6 +563,10 @@ export function isFetchPermissionsPending(
return isPending(state, FETCH_PERMISSIONS, namespace + "/" + repoName);
}
export function getFetchAvailablePermissionsFailure(state: Object) {
return getFailure(state, FETCH_AVAILABLE, "available");
}
export function getFetchPermissionsFailure(
state: Object,
namespace: string,
@@ -522,6 +618,7 @@ export function isCreatePermissionPending(
) {
return isPending(state, CREATE_PERMISSION, namespace + "/" + repoName);
}
export function getCreatePermissionFailure(
state: Object,
namespace: string,
@@ -603,3 +700,33 @@ export function getModifyPermissionsFailure(
}
return null;
}
export function findMatchingRoleName(
availablePermissions: AvailableRepositoryPermissions,
verbs: string[]
) {
if (!verbs) {
return "";
}
const matchingRole = availablePermissions.availableRoles.find(role => {
return equalVerbs(role.verbs, verbs);
});
if (matchingRole) {
return matchingRole.name;
} else {
return "";
}
}
function equalVerbs(verbs1: string[], verbs2: string[]) {
if (!verbs1 || !verbs2) {
return false;
}
if (verbs1.length !== verbs2.length) {
return false;
}
return verbs1.every(verb => verbs2.includes(verb));
}

View File

@@ -59,7 +59,8 @@ const hitchhiker_puzzle42Permission_user_eins: Permission = {
href:
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_eins"
}
}
},
verbs: []
};
const hitchhiker_puzzle42Permission_user_zwei: Permission = {
@@ -79,7 +80,8 @@ const hitchhiker_puzzle42Permission_user_zwei: Permission = {
href:
"http://localhost:8081/scm/api/rest/v2/repositories/hitchhiker/puzzle42/permissions/user_zwei"
}
}
},
verbs: []
};
const hitchhiker_puzzle42Permissions: PermissionCollection = [
@@ -175,8 +177,7 @@ describe("permission fetch", () => {
}
);
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins };
editedPermission.type = "OWNER";
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
const store = mockStore({});
@@ -197,8 +198,7 @@ describe("permission fetch", () => {
}
);
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins };
editedPermission.type = "OWNER";
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
const store = mockStore({});
@@ -227,8 +227,7 @@ describe("permission fetch", () => {
}
);
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins };
editedPermission.type = "OWNER";
let editedPermission = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
const store = mockStore({});
@@ -451,8 +450,7 @@ describe("permissions reducer", () => {
entries: [hitchhiker_puzzle42Permission_user_eins]
}
};
let permissionEdited = { ...hitchhiker_puzzle42Permission_user_eins };
permissionEdited.type = "OWNER";
let permissionEdited = { ...hitchhiker_puzzle42Permission_user_eins, type: "OWNER" };
let expectedState = {
"hitchhiker/puzzle42": {
entries: [permissionEdited]

View File

@@ -1,7 +1,7 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Button } from "@scm-manager/ui-components";
import { ButtonGroup } from "@scm-manager/ui-components";
type Props = {
t: string => string,
@@ -9,7 +9,7 @@ type Props = {
showHistory: boolean => void
};
class ButtonGroup extends React.Component<Props> {
class FileButtonGroup extends React.Component<Props> {
showHistory = () => {
this.props.showHistory(true);
};
@@ -21,15 +21,6 @@ class ButtonGroup extends React.Component<Props> {
render() {
const { t, historyIsSelected } = this.props;
let sourcesColor = "";
let historyColor = "";
if (historyIsSelected) {
historyColor = "link is-selected";
} else {
sourcesColor = "link is-selected";
}
const sourcesLabel = (
<>
<span className="icon">
@@ -53,20 +44,15 @@ class ButtonGroup extends React.Component<Props> {
);
return (
<div className="buttons has-addons">
<Button
label={sourcesLabel}
color={sourcesColor}
action={this.showSources}
/>
<Button
label={historyLabel}
color={historyColor}
action={this.showHistory}
/>
</div>
<ButtonGroup
firstlabel={sourcesLabel}
secondlabel={historyLabel}
firstAction={this.showSources}
secondAction={this.showHistory}
firstIsSelected={!historyIsSelected}
/>
);
}
}
export default translate("repos")(ButtonGroup);
export default translate("repos")(FileButtonGroup);

View File

@@ -6,7 +6,7 @@ import { DateFromNow } from "@scm-manager/ui-components";
import FileSize from "../components/FileSize";
import injectSheet from "react-jss";
import classNames from "classnames";
import ButtonGroup from "../components/content/ButtonGroup";
import FileButtonGroup from "../components/content/FileButtonGroup";
import SourcesView from "./SourcesView";
import HistoryView from "./HistoryView";
import { getSources } from "../modules/sources";
@@ -76,7 +76,7 @@ class Content extends React.Component<Props, State> {
const icon = collapsed ? "fa-angle-right" : "fa-angle-down";
const selector = file._links.history ? (
<ButtonGroup
<FileButtonGroup
file={file}
historyIsSelected={showHistory}
showHistory={(changeShowHistory: boolean) =>