Files
2024-02-15 17:01:50 +00:00

161 lines
4.9 KiB
TypeScript

import {Component, Fragment} from 'react';
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 type {PermissionResource, Permissions, PermissionValue} from 'sentry/types';
/**
* 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:
*
* <resource>:<access>
*
* "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 SENTRY_APP_PERMISSIONS.find(permissions => 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 Object.entries(permissions).flatMap(
([r, p]) => findResource(r as PermissionResource)?.choices?.[p]?.scopes
);
}
export default class PermissionSelection extends Component<Props, State> {
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 (
<Fragment>
{SENTRY_APP_PERMISSIONS.map(config => {
const options = Object.entries(config.choices).map(([value, {label}]) => ({
value,
label,
}));
const value = permissions[config.resource];
return (
<SelectField
// These are not real fields we want submitted, so we use
// `--permission` as a suffix here, then filter these
// fields out when submitting the form in
// sentryApplicationDetails.jsx
name={`${config.resource}--permission`}
key={config.resource}
options={options}
help={config.help}
label={config.label || config.resource}
onChange={this.onChange.bind(this, config.resource)}
value={value}
defaultValue={value}
disabled={this.props.appPublished}
disabledReason={t('Cannot update permissions on a published integration')}
/>
);
})}
</Fragment>
);
}
}