import {Component, Fragment} from 'react'; import find from 'lodash/find'; import flatMap from 'lodash/flatMap'; import SelectField from 'sentry/components/forms/fields/selectField'; import FormContext from 'sentry/components/forms/formContext'; import {SENTRY_APP_PERMISSIONS} from 'sentry/constants'; import {t} from 'sentry/locale'; import {PermissionResource, Permissions, PermissionValue} from 'sentry/types/index'; /** * Custom form element that presents API scopes in a resource-centric way. Meaning * a dropdown for each resource, that rolls up into a flat list of scopes. * * * API Scope vs Permission * * "API Scopes" are the string identifier that gates an endpoint. For example, * `project:read` or `org:admin`. They're made up of two parts: * * : * * "Permissions" are a more user-friendly way of conveying this same information. * They're roughly the same with one exception: * * - No Access * - Read * - Read & Write * - Admin * * "Read & Write" actually maps to the `write` access level since `read` is * implied. Similarly, `admin` implies `read` and `write`. * * This components displays things per Resource. Meaning the User selects * "Read", "Read & Write", or "Admin" for Project or Organization or etc. * * === Scopes to Permissions * * The first thing this component does on instantiation is take the list of API * Scopes passed via `props` and converts them to "Permissions. * * So a list of scopes like the following: * * ['project:read', 'project:write', 'org:admin'] * * will become an object that looks like: * * { * 'Project': 'write', * 'Organization': 'admin', * } * * * State * * This component stores state like the example object from above. When the * User changes the Permission for a particular resource, it updates the * `state.permissions` object to reflect the change. * * * Updating the Form Field Value * * In addition to updating the state, when a value is changed this component * recalculates the full list of API Scopes that need to be passed to the API. * * So if the User has changed Project to "Admin" and Organization to "Read & Write", * we end up with a `state.permissions` like: * * { * 'Project': 'admin', * 'Organization': 'write', * } * * From there, we calculate the full list of API Scopes. This list includes all * implied scopes, meaning the above state would result in: * * ['project:read', 'project:write', 'project:admin', 'org:read', 'org:write'] * */ type Props = { appPublished: boolean; onChange: (permissions: Permissions) => void; permissions: Permissions; }; type State = { permissions: Permissions; }; function findResource(r: PermissionResource) { return find(SENTRY_APP_PERMISSIONS, ['resource', r]); } /** * Converts the "Permission" values held in `state` to a list of raw * API scopes we can send to the server. For example: * * ['org:read', 'org:write', ...] * */ function permissionStateToList(permissions: Permissions) { return flatMap( Object.entries(permissions), ([r, p]) => findResource(r as PermissionResource)?.choices?.[p]?.scopes ); } export default class PermissionSelection extends Component { state: State = { permissions: this.props.permissions, }; static contextType = FormContext; onChange = (resource: PermissionResource, choice: PermissionValue) => { const {permissions} = this.state; permissions[resource] = choice; this.save(permissions); }; save = (permissions: Permissions) => { this.setState({permissions}); this.props.onChange(permissions); this.context.form.setValue('scopes', permissionStateToList(this.state.permissions)); }; render() { const {permissions} = this.state; return ( {SENTRY_APP_PERMISSIONS.map(config => { const options = Object.entries(config.choices).map(([value, {label}]) => ({ value, label, })); const value = permissions[config.resource]; return ( ); })} ); } }