Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/api/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export interface TaskableDocumentMapping extends AssessableDataMapping {
}

export interface TypeDataMapping extends TaskableDocumentMapping, ContainerTypeDataMapping {
['_unknown_']: { name: string };
['code']: CodeData;
// TODO: rename to `code_version`
['script_version']: ScriptVersionData;
Expand Down Expand Up @@ -187,6 +188,7 @@ null as unknown as EnsureAllTaskable<TaskableTypeModelMapping>;

export interface TypeModelMapping
extends TaskableTypeModelMapping, ContainerTypeModelMapping, AssessableTypeModelMapping {
['_unknown_']: any;
['code']: Code;
// TODO: rename to `code_version`?
['script_version']: ScriptVersion;
Expand Down
20 changes: 7 additions & 13 deletions src/api/documentRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,20 @@ export function find(id: string, signal: AbortSignal): AxiosPromise<DocumentRoot
return api.get(`/documentRoots/${id}`, { signal });
}

interface FindManyForData {
documentRootIds: string[];
ignoreMissingRoots?: boolean;
type?: string;
}

export function findManyFor(
userId: string,
ids: string[],
ignoreMissingRoots: boolean,
documentType: DocumentType | undefined,
signal: AbortSignal
): AxiosPromise<DocumentRoot[]> {
const params = new URLSearchParams();
if (ignoreMissingRoots) {
params.append('ignoreMissingRoots', '1');
}
ids.forEach((id) => params.append('ids', id));
if (documentType) {
params.append('type', documentType);
}
const data: {
documentRootIds: string[];
ignoreMissingRoots?: boolean;
type?: string;
} = {
const data: FindManyForData = {
documentRootIds: ids
};
if (ignoreMissingRoots) {
Expand Down
12 changes: 12 additions & 0 deletions src/api/permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export interface UserPermission extends UserPermissionBase {

export type Permissions = {
id: string;
access: Access;
sharedAccess: Access;
userPermissions: UserPermissionBase[];
groupPermissions: GroupPermissionBase[];
};
Expand Down Expand Up @@ -69,3 +71,13 @@ export function deleteGroupPermission(id: string, signal: AbortSignal): AxiosPro
export function permissionsFor(documentRootId: string, signal: AbortSignal): AxiosPromise<Permissions> {
return api.get(`/documentRoots/${documentRootId}/permissions`, { signal });
}

export function documentRootPermissions(
documentRootIds: string[],
signal: AbortSignal
): AxiosPromise<Permissions[]> {
const data = {
documentRootIds: documentRootIds
};
return api.post(`/documentRoots/permissions`, data, { signal });
}
6 changes: 5 additions & 1 deletion src/components/Admin/AdminPanel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import StudentGroupPanel from '@tdev-components/Admin/StudentGroupPanel';
import UserTable from '@tdev-components/Admin/UserTable';
import AllowedActions from '../AllowedActions';
import CreateUser from '../CreateUser';
import PermissionsControl from '../PermissionsControl';

const AdminPanel = observer(() => {
const userStore = useStore('userStore');
Expand All @@ -24,13 +25,16 @@ const AdminPanel = observer(() => {
}
return (
<div>
<Tabs queryString="panel">
<Tabs queryString="panel" lazy>
<TabItem value="studentGroups" label="Lerngruppen">
<StudentGroupPanel />
</TabItem>
<TabItem value="accounts" label="Accounts">
<UserTable />
</TabItem>
<TabItem value="permissions" label="Berechtigungen">
<PermissionsControl />
</TabItem>
<TabItem value="createUser" label="Account erstellen">
<CreateUser />
</TabItem>
Expand Down
64 changes: 64 additions & 0 deletions src/components/Admin/PermissionsControl/AccessOverview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import clsx from 'clsx';
import styles from './styles.module.scss';
import { observer } from 'mobx-react-lite';
import { useStore } from '@tdev-hooks/useStore';
import Badge from '@tdev-components/shared/Badge';
import CopyBadge from '@tdev-components/shared/CopyBadge';
import { AccessColor, AccessIcon } from '@tdev-components/PermissionsPanel/AccessBadge';
import Icon from '@mdi/react';
import { SIZE_XS } from '@tdev-components/shared/iconSizes';
import { mdiAccount, mdiAccountGroup, mdiFileMultipleOutline, mdiShareVariantOutline } from '@mdi/js';
import { DocumentType } from '@tdev-api/document';

interface Props {
doc: { id: string; type: DocumentType };
}

const AccessOverview = observer((props: Props) => {
const { doc } = props;
const docRootStore = useStore('documentRootStore');
const viewStore = useStore('viewStore');
const view = viewStore.permissionControl;
const root = docRootStore.find(doc.id);
return (
<div className={clsx(styles.accessOverview)}>
<Badge className={clsx(styles.typeBadge)} color={view.typeColors.get(doc.type)}>
{doc.type}
</Badge>
<Badge type="secondary" title={`DocumentRoot Access: ${root?._access}`}>
<Icon path={mdiFileMultipleOutline} size={SIZE_XS} />
<Icon path={AccessIcon(root?._access)} size={SIZE_XS} color={AccessColor(root?._access)} />
</Badge>
<Badge type="secondary" title={`Shared Access: ${root?._sharedAccess}`}>
<Icon path={mdiShareVariantOutline} size={SIZE_XS} />
<Icon
path={AccessIcon(root?._sharedAccess)}
size={SIZE_XS}
color={AccessColor(root?._sharedAccess)}
/>
</Badge>
<Badge
color={root?.groupPermissions?.length ? 'orange' : 'gray'}
title={`Group Permissions: ${root?.groupPermissions?.length}`}
>
<Icon path={mdiAccountGroup} size={SIZE_XS} /> {root?.groupPermissions?.length}
</Badge>
<Badge
color={root?.userPermissions?.length ? 'orange' : 'gray'}
title={`User Permissions: ${root?.userPermissions?.length}`}
>
<Icon path={mdiAccount} size={SIZE_XS} /> {root?.userPermissions?.length}
</Badge>
<CopyBadge
label={`${doc.id.slice(0, 8)}...`}
title={`DocumentRoot ID: ${doc.id}`}
value={doc.id}
/>
</div>
);
});

export default AccessOverview;
89 changes: 89 additions & 0 deletions src/components/Admin/PermissionsControl/DocumentTypeSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react';
import clsx from 'clsx';
import styles from './styles.module.scss';
import { observer } from 'mobx-react-lite';
import { useStore } from '@tdev-hooks/useStore';
import Select, { StylesConfig } from 'react-select';
import { DocumentType, TypeModelMapping } from '@tdev-api/document';
import Button from '@tdev-components/shared/Button';

interface Props {}
const colourStyles: StylesConfig<{ value: DocumentType; hslDeg: number; label: DocumentType }, true> = {
control: (styles) => ({ ...styles, backgroundColor: 'white' }),
option: (styles, { data, isFocused, isSelected }) => {
return {
...styles,
backgroundColor: isFocused ? `hsl(${data.hslDeg}, 70%, 70%)` : undefined,
color: `hsl(${data.hslDeg}, 100%, 25%)`
};
},
multiValue: (styles, { data }) => {
return {
...styles,
backgroundColor: `hsl(${data.hslDeg}, 90%, 70%)`
};
},
multiValueLabel: (styles, { data }) => ({
...styles,
color: `hsl(${data.hslDeg}, 100%, 30%)`
}),
multiValueRemove: (styles, { data }) => ({
...styles,
color: `hsl(${data.hslDeg}, 100%, 30%)`,
':hover': {
backgroundColor: `hsl(${data.hslDeg}, 100%, 50%)`,
color: 'white'
}
}),
menuPortal: (base) => ({ ...base, zIndex: 'var(--ifm-z-index-overlay)' }),
container: (base) => ({ ...base, minWidth: '15em' })
};

const DocumentTypeSelector = observer((props: Props) => {
const viewStore = useStore('viewStore');
const view = viewStore.permissionControl;

return (
<div className={clsx(styles.documentTypeSelector)}>
<Select
closeMenuOnSelect={false}
menuPortalTarget={document.body}
isMulti
options={view.selectTypeOptions}
value={view.selectTypeOptions.filter((opt) => view.typeFilter.has(opt.value))}
onChange={(newVal, prop) => {
console.log('DocumentTypeSelector onChange', newVal, prop);
switch (prop.action) {
case 'clear':
return view.clearTypeFilter();
case 'remove-value':
if (prop.removedValue) {
return view.setTypeFilter(prop.removedValue.value, false);
}
break;
case 'select-option':
if (prop.option) {
return view.setTypeFilter(prop.option.value, true);
}
break;
case 'deselect-option':
if (prop.option) {
return view.setTypeFilter(prop.option.value, false);
}
break;
}
}}
styles={colourStyles}
/>
<Button
text="Alle"
onClick={() => {
view.selectAllTypeFilters();
}}
className={clsx(styles.selectAllButton)}
/>
</div>
);
});

export default DocumentTypeSelector;
64 changes: 64 additions & 0 deletions src/components/Admin/PermissionsControl/PageActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import clsx from 'clsx';
import styles from './styles.module.scss';
import { observer } from 'mobx-react-lite';
import { useStore } from '@tdev-hooks/useStore';
import { mdiAccountMultipleRemove, mdiAccountRemove } from '@mdi/js';
import { Confirm } from '@tdev-components/shared/Button/Confirm';

interface Props {
docs: { id: string; type: string }[];
}

const PageActions = observer((props: Props) => {
const { docs } = props;
const docRootStore = useStore('documentRootStore');
const permissionStore = useStore('permissionStore');

return (
<div className={clsx(styles.docActions)}>
{docs.some((doc) => docRootStore.find(doc.id)?.userPermissions?.length) && (
<Confirm
icon={mdiAccountRemove}
iconSide="left"
text="User"
confirmText="Userberechtigungen entfernen?"
title="Alle Benutzerberechtigungen entfernen"
color="red"
onConfirm={() => {
docs.forEach((doc) => {
const root = docRootStore.find(doc.id);
if (root) {
root.userPermissions.forEach((userPermission) => {
permissionStore.deleteUserPermission(userPermission);
});
}
});
}}
/>
)}
{docs.some((doc) => docRootStore.find(doc.id)?.groupPermissions?.length) && (
<Confirm
icon={mdiAccountMultipleRemove}
iconSide="left"
text="Gruppe"
title="Alle Gruppenberechtigungen entfernen"
confirmText="Gruppenberechtigungen entfernen?"
color="red"
onConfirm={() => {
docs.forEach((doc) => {
const root = docRootStore.find(doc.id);
if (root) {
root.groupPermissions.forEach((groupPermission) => {
permissionStore.deleteGroupPermission(groupPermission);
});
}
});
}}
/>
)}
</div>
);
});

export default PageActions;
Loading
Loading