mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
a1a84d99c0
Summary: This particular combination of features is not built out - data will be censored but changes to data will not. So the user will now get an error if they try to do it. Existing rules of this kind will continue to operate as before, and can be set via the api. Test Plan: added test Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2751
180 lines
6.0 KiB
TypeScript
180 lines
6.0 KiB
TypeScript
/**
|
|
* Implements a widget showing 3-state boxes for permissions
|
|
* (for Allow / Deny / Pass-Through).
|
|
*/
|
|
import {colors, testId} from 'app/client/ui2018/cssVars';
|
|
import {cssIconButton, icon} from 'app/client/ui2018/icons';
|
|
import {menu, menuIcon, menuItem} from 'app/client/ui2018/menus';
|
|
import {PartialPermissionSet, PartialPermissionValue} from 'app/common/ACLPermissions';
|
|
import {ALL_PERMISSION_PROPS, emptyPermissionSet} from 'app/common/ACLPermissions';
|
|
import {capitalize} from 'app/common/gutil';
|
|
import {dom, DomElementArg, Observable, styled} from 'grainjs';
|
|
import isEqual = require('lodash/isEqual');
|
|
|
|
// One of the strings 'read', 'update', etc.
|
|
export type PermissionKey = keyof PartialPermissionSet;
|
|
|
|
/**
|
|
* Renders a box for each of availableBits, and a dropdown with a description and some shortcuts.
|
|
*/
|
|
export function permissionsWidget(
|
|
availableBits: PermissionKey[],
|
|
pset: Observable<PartialPermissionSet>,
|
|
options: {disabled: boolean, sanityCheck?: (p: PartialPermissionSet) => void},
|
|
...args: DomElementArg[]
|
|
) {
|
|
// These are the permission sets available to set via the dropdown.
|
|
const empty: PartialPermissionSet = emptyPermissionSet();
|
|
const allowAll: PartialPermissionSet = makePermissionSet(availableBits, () => 'allow');
|
|
const denyAll: PartialPermissionSet = makePermissionSet(availableBits, () => 'deny');
|
|
const readOnly: PartialPermissionSet = makePermissionSet(availableBits, (b) => b === 'read' ? 'allow' : 'deny');
|
|
const setPermissions = (p: PartialPermissionSet) => {
|
|
options.sanityCheck?.(p);
|
|
pset.set(p);
|
|
};
|
|
|
|
return cssPermissions(
|
|
dom.forEach(availableBits, (bit) => {
|
|
return cssBit(
|
|
bit.slice(0, 1).toUpperCase(), // Show the first letter of the property (e.g. "R" for "read")
|
|
cssBit.cls((use) => '-' + use(pset)[bit]), // -allow, -deny class suffixes.
|
|
dom.attr('title', (use) => capitalize(`${use(pset)[bit]} ${bit}`.trim())), // Explanation on hover
|
|
dom.cls('disabled', options.disabled),
|
|
// Cycle the bit's value on click, unless disabled.
|
|
(options.disabled ? null :
|
|
dom.on('click', () => setPermissions({...pset.get(), [bit]: next(pset.get()[bit])}))
|
|
)
|
|
);
|
|
}),
|
|
cssIconButton(icon('Dropdown'), testId('permissions-dropdown'), menu(() => {
|
|
// Show a disabled "Custom" menu item if the permission set isn't a recognized one, for
|
|
// information purposes.
|
|
const isCustom = [allowAll, denyAll, readOnly, empty].every(ps => !isEqual(ps, pset.get()));
|
|
return [
|
|
(isCustom ?
|
|
cssMenuItem(() => null, dom.cls('disabled'), menuIcon('Tick'),
|
|
cssMenuItemContent(
|
|
'Custom',
|
|
cssMenuItemDetails(dom.text((use) => psetDescription(use(pset))))
|
|
),
|
|
) :
|
|
null
|
|
),
|
|
// If the set matches any recognized pattern, mark that item with a tick (checkmark).
|
|
cssMenuItem(() => setPermissions(allowAll), tick(isEqual(pset.get(), allowAll)), 'Allow All',
|
|
dom.cls('disabled', options.disabled)
|
|
),
|
|
cssMenuItem(() => setPermissions(denyAll), tick(isEqual(pset.get(), denyAll)), 'Deny All',
|
|
dom.cls('disabled', options.disabled)
|
|
),
|
|
cssMenuItem(() => setPermissions(readOnly), tick(isEqual(pset.get(), readOnly)), 'Read Only',
|
|
dom.cls('disabled', options.disabled)
|
|
),
|
|
cssMenuItem(() => setPermissions(empty),
|
|
// For the empty permission set, it seems clearer to describe it as "No Effect", but to
|
|
// all it "Clear" when offering to the user as the action.
|
|
isEqual(pset.get(), empty) ? [tick(true), 'No Effect'] : [tick(false), 'Clear'],
|
|
dom.cls('disabled', options.disabled),
|
|
),
|
|
];
|
|
})),
|
|
...args
|
|
);
|
|
}
|
|
|
|
function next(pvalue: PartialPermissionValue): PartialPermissionValue {
|
|
switch (pvalue) {
|
|
case 'allow': return '';
|
|
case 'deny': return 'allow';
|
|
}
|
|
return 'deny';
|
|
}
|
|
|
|
// Helper to build up permission sets.
|
|
function makePermissionSet(bits: PermissionKey[], makeValue: (bit: PermissionKey) => PartialPermissionValue) {
|
|
const pset = emptyPermissionSet();
|
|
for (const bit of bits) {
|
|
pset[bit] = makeValue(bit);
|
|
}
|
|
return pset;
|
|
}
|
|
|
|
// Helper for a tick (checkmark) icon, replacing it with an equialent space when not shown.
|
|
function tick(show: boolean) {
|
|
return show ? menuIcon('Tick') : cssMenuIconSpace();
|
|
}
|
|
|
|
// Human-readable summary of the permission set. E.g. "Allow Read. Deny Update, Create.".
|
|
function psetDescription(permissionSet: PartialPermissionSet): string {
|
|
const allow: string[] = [];
|
|
const deny: string[] = [];
|
|
for (const prop of ALL_PERMISSION_PROPS) {
|
|
const value = permissionSet[prop];
|
|
if (value === "allow") {
|
|
allow.push(capitalize(prop));
|
|
} else if (value === "deny") {
|
|
deny.push(capitalize(prop));
|
|
}
|
|
}
|
|
const parts: string[] = [];
|
|
if (allow.length) { parts.push(`Allow ${allow.join(", ")}.`); }
|
|
if (deny.length) { parts.push(`Deny ${deny.join(", ")}.`); }
|
|
return parts.join(' ');
|
|
}
|
|
|
|
const cssPermissions = styled('div', `
|
|
display: flex;
|
|
gap: 4px;
|
|
`);
|
|
|
|
const cssBit = styled('div', `
|
|
flex: none;
|
|
height: 24px;
|
|
width: 24px;
|
|
border-radius: 2px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
border: 1px dashed ${colors.darkGrey};
|
|
color: ${colors.darkGrey};
|
|
cursor: pointer;
|
|
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
|
|
&-allow {
|
|
background-color: ${colors.lightGreen};
|
|
border: 1px solid ${colors.lightGreen};
|
|
color: white;
|
|
}
|
|
&-deny {
|
|
background-image: linear-gradient(-45deg, ${colors.error} 14px, white 15px 16px, ${colors.error} 16px);
|
|
border: 1px solid ${colors.error};
|
|
color: white;
|
|
}
|
|
&.disabled {
|
|
opacity: 0.5;
|
|
}
|
|
`);
|
|
|
|
const cssMenuIconSpace = styled('div', `
|
|
width: 24px;
|
|
`);
|
|
|
|
// Don't make disabled item too hard to see here.
|
|
const cssMenuItem = styled(menuItem, `
|
|
align-items: start;
|
|
&.disabled {
|
|
opacity: unset;
|
|
}
|
|
`);
|
|
|
|
const cssMenuItemContent = styled('div', `
|
|
display: flex;
|
|
flex-direction: column;
|
|
`);
|
|
|
|
const cssMenuItemDetails = styled('div', `
|
|
font-size: 12px;
|
|
`);
|