Skip to content

Commit

Permalink
added widget permissions to permissions dialog (#319)
Browse files Browse the repository at this point in the history
* added widget permissions to permissions dialog

* do not render widgets user has no access to

* update readme

* enhance types

* show permissions for correct project

* improve performance dramatically
  • Loading branch information
foxriver76 authored Jan 18, 2024
1 parent dffe322 commit d21f41d
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 52 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ When one of the access rights is not granted on project level, it does not have

Note, that whenever you try to access a view, where the current user has no permission for, the user will see the project selection panel instead.

### Widget
If the user has no `read` permissions, the widget will not be rendered in the runtime. If user has no `write` permissions, the widget
will not be rendered in edit mode.

## Settings
### Reload if sleep longer than
There is a rule that after some disconnection period, the whole VIS page will be reloaded to synchronize the project.
Expand Down Expand Up @@ -259,6 +263,7 @@ E.g., if it was used in a menu and the menu is red, the circle would be red.
## Changelog
### **WORK IN PROGRESS**
* (foxriver76) increased timeout for project import
* (foxriver76) added permissions on widget level

### 2.9.19 (2024-01-17)
* (foxriver76) fixed issue when resizing widget from the left side
Expand Down
238 changes: 192 additions & 46 deletions src/src/Toolbar/ProjectsManager/PermissionsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import Collapse from '@mui/material/Collapse';
import { type Connection, I18n } from '@iobroker/adapter-react-v5';
import { Permissions, Project } from '@/types';
import {
AnyWidgetId, Permissions, Project, Widget,
} from '@/types';
import { store } from '@/Store';
import { deepClone, DEFAULT_PERMISSIONS } from '@/Utils/utils';
import IODialog from '../../Components/IODialog';
Expand All @@ -39,10 +41,30 @@ interface PermissionsDialogState {
projectPermissions: PermissionsMap;
/** The permissions assignment to users for each view */
viewPermissions: Record<string, PermissionsMap>;
/** The permissions assignment to users for each widget */
widgetPermissions: Record<string, PermissionsMap>;
/** Id for each card and open status */
cardOpen: Record<string, boolean>;
}

interface RenderViewPermissionsOptions {
/** The user which the permissions should be shown for */
user: string;
/** The currently logged-in user */
activeUser: string;
/** The view the permissions should be rendered for */
view: string;
/** The current project */
visProject: Project;
}

interface RenderWidgetPermissionsOptions extends RenderViewPermissionsOptions {
/** The widget id */
wid: AnyWidgetId;
/** The widget */
widget: Widget;
}

export default class PermissionsDialog extends React.Component<PermissionsDialogProps, PermissionsDialogState> {
/** Admin user cannot be disabled */
private readonly ADMIN_USER = 'admin';
Expand All @@ -54,6 +76,7 @@ export default class PermissionsDialog extends React.Component<PermissionsDialog
users: [],
projectPermissions: new Map(),
viewPermissions: {},
widgetPermissions: {},
cardOpen: {},
};
}
Expand All @@ -66,6 +89,7 @@ export default class PermissionsDialog extends React.Component<PermissionsDialog
const { visProject } = store.getState();
const projectPermissions = new Map<string, Permissions>();
const viewPermissions: Record<string, PermissionsMap> = {};
const widgetPermissions: Record<string, PermissionsMap> = {};

for (const user of Object.keys(userView)) {
projectPermissions.set(user, visProject.___settings.permissions?.[user] ?? DEFAULT_PERMISSIONS);
Expand All @@ -80,10 +104,20 @@ export default class PermissionsDialog extends React.Component<PermissionsDialog
}

viewPermissions[viewName].set(user, view.settings?.permissions?.[user] ?? DEFAULT_PERMISSIONS);

for (const [wid, widget] of Object.entries(view.widgets)) {
if (!widgetPermissions[wid]) {
widgetPermissions[wid] = new Map<string, Permissions>();
}

widgetPermissions[wid].set(user, widget.permissions?.[user] ?? DEFAULT_PERMISSIONS);
}
}
}

this.setState({ users: Object.keys(userView), projectPermissions, viewPermissions });
this.setState({
users: Object.keys(userView), projectPermissions, viewPermissions, widgetPermissions,
});
}

/**
Expand Down Expand Up @@ -117,6 +151,14 @@ export default class PermissionsDialog extends React.Component<PermissionsDialog
}

view.settings.permissions[user] = this.state.viewPermissions[viewName].get(user) ?? DEFAULT_PERMISSIONS;

for (const [wid, widget] of Object.entries(view.widgets)) {
if (widget.permissions === undefined) {
widget.permissions = {};
}

widget.permissions[user] = this.state.widgetPermissions[wid].get(user) ?? DEFAULT_PERMISSIONS;
}
}
}

Expand All @@ -129,10 +171,10 @@ export default class PermissionsDialog extends React.Component<PermissionsDialog
*/
renderInfoDialog(): React.JSX.Element {
return <div style={{
display: 'inline-flex', alignItems: 'center', border: '1px solid', borderRadius: '5px', padding: '2px',
display: 'inline-flex', alignItems: 'center', border: '1px solid', borderRadius: '5px', width: '100%',
}}
>
<InfoIcon />
<InfoIcon sx={{ margin: '4px' }} />
<div style={{ margin: '6px', fontSize: '12px' }}>
<p style={{ margin: 0 }}>
{I18n.t('Only the admin user can change permissions')}
Expand All @@ -145,6 +187,148 @@ export default class PermissionsDialog extends React.Component<PermissionsDialog
</div>;
}

/**
* Render the widget permissions
*
* @param options project, wid, user and view info
*/
renderWidgetPermissions(options: RenderWidgetPermissionsOptions): React.JSX.Element {
const {
view, user, activeUser, wid, widget,
} = options;

return <div style={{ display: 'flex' }} key={`${user}-${view}-${wid}`}>
<p style={{ margin: 'auto', fontSize: 12 }}>{`${widget.data.name ? `${widget.data.name} (${wid})` : wid}:`}</p>
<div style={{
width: '100%',
alignSelf: 'center',
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
}}
>
<Checkbox
disabled={user === this.ADMIN_USER || activeUser !== this.ADMIN_USER || !this.state.projectPermissions.get(user)?.read || !this.state.viewPermissions[view].get(user)?.read}
checked={this.state.widgetPermissions[wid]?.get(user)?.read}
onClick={() => {
const newState = this.state;
const currVal = this.state.widgetPermissions[wid].get(user);

newState.widgetPermissions[wid].set(user, {
read: !currVal?.read,
write: !!currVal?.write,
});
this.setState(newState);
}}
/>
{I18n.t('Read')}
<Checkbox
disabled={user === this.ADMIN_USER || activeUser !== this.ADMIN_USER || !this.state.projectPermissions.get(user)?.write || !this.state.viewPermissions[view].get(user)?.write}
checked={this.state.widgetPermissions[wid]?.get(user)?.write}
onClick={() => {
const newState = this.state;
const currVal = this.state.widgetPermissions[wid].get(user);

newState.widgetPermissions[wid].set(user, {
read: !!currVal?.read,
write: !currVal?.write,
});
this.setState(newState);
}}
/>
{I18n.t('Write')}
</div>
</div>;
}

/**
* Render the view permissions dialog
*
* @param options information about view and user
*/
renderViewPermissions(options: RenderViewPermissionsOptions): React.JSX.Element {
const {
view, user, activeUser, visProject,
} = options;

const viewId = `${user}-${view}`;

return <Card sx={{ border: '1px solid rgba(211,211,211,0.6)', marginTop: '5px' }}>
<CardHeader
title={view}
titleTypographyProps={{ fontWeight: 'bold', fontSize: 12 }}
action={<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}
key={viewId}
>
<div style={{ display: 'flex' }}>
<div style={{
width: '100%',
alignSelf: 'center',
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
}}
>
<Checkbox
disabled={user === this.ADMIN_USER || activeUser !== this.ADMIN_USER || !this.state.projectPermissions.get(user)?.read}
checked={this.state.viewPermissions[view]?.get(user)?.read}
onClick={() => {
const newState = this.state;
const currVal = this.state.viewPermissions[view].get(user);

newState.viewPermissions[view].set(user, {
read: !currVal?.read,
write: !!currVal?.write,
});
this.setState(newState);
}}
/>
{I18n.t('Read')}
<Checkbox
disabled={user === this.ADMIN_USER || activeUser !== this.ADMIN_USER || !this.state.projectPermissions.get(user)?.write}
checked={this.state.viewPermissions[view]?.get(user)?.write}
onClick={() => {
const newState = this.state;
const currVal = this.state.viewPermissions[view].get(user);

newState.viewPermissions[view].set(user, {
read: !!currVal?.read,
write: !currVal?.write,
});
this.setState(newState);
}}
/>
{I18n.t('Write')}
</div>
<IconButton
onClick={() => {
this.setState({
cardOpen: {
...this.state.cardOpen,
[viewId]: !this.state.cardOpen[viewId],
},
});
}}
aria-label="expand"
size="small"
>
{this.state.cardOpen[viewId] ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</div>
</div>}
/>
<Collapse
in={this.state.cardOpen[viewId]}
sx={{ borderTop: '1px solid rgba(211,211,211,0.6)' }}
>
<CardContent>
{this.state.cardOpen[viewId] ? Object.entries(visProject[view].widgets).map(([wid, widget]) => this.renderWidgetPermissions({ ...options, wid: wid as AnyWidgetId, widget })) : null}
</CardContent>
</Collapse>
</Card>;
}

/**
* Render the actual component
*/
Expand All @@ -161,6 +345,7 @@ export default class PermissionsDialog extends React.Component<PermissionsDialog
ActionIcon={SaveIcon}
actionDisabled={false}
closeDisabled={false}
minWidth="600px"
>
{this.renderInfoDialog()}
{this.state.users.map(user =>
Expand Down Expand Up @@ -227,48 +412,9 @@ export default class PermissionsDialog extends React.Component<PermissionsDialog
sx={{ borderTop: '1px solid rgba(211,211,211,0.6)' }}
>
<CardContent>
{Object.keys(visProject).map(view => (view === '___settings' ? null : <div style={{ display: 'flex' }}>
<p style={{ margin: 'auto' }}>{`${view}:`}</p>
<div style={{
width: '100%',
alignSelf: 'center',
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
}}
>
<Checkbox
disabled={user === this.ADMIN_USER || activeUser !== this.ADMIN_USER || !this.state.projectPermissions.get(user)?.read}
checked={this.state.viewPermissions[view]?.get(user)?.read}
onClick={() => {
const newState = this.state;
const currVal = this.state.viewPermissions[view].get(user);

newState.viewPermissions[view].set(user, {
read: !currVal?.read,
write: !!currVal?.write,
});
this.setState(newState);
}}
/>
{I18n.t('Read')}
<Checkbox
disabled={user === this.ADMIN_USER || activeUser !== this.ADMIN_USER || !this.state.projectPermissions.get(user)?.write}
checked={this.state.viewPermissions[view]?.get(user)?.write}
onClick={() => {
const newState = this.state;
const currVal = this.state.viewPermissions[view].get(user);

newState.viewPermissions[view].set(user, {
read: !!currVal?.read,
write: !currVal?.write,
});
this.setState(newState);
}}
/>
{I18n.t('Write')}
</div>
</div>))}
{this.state.cardOpen[user] ? Object.keys(visProject).map(view => (view === '___settings' ? null : this.renderViewPermissions({
visProject, view, user, activeUser,
}))) : null}
</CardContent>
</Collapse>
</Card>)}
Expand Down
5 changes: 5 additions & 0 deletions src/src/Toolbar/ProjectsManager/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ const ProjectsManage = props => {
<IconButton
onClick={event => {
setAnchorEl(event.currentTarget);
// TODO ensure correct project is opened
if (props.projectName !== projectName) {
props.loadProject(projectName);
}
setShowPermissionsDialog(projectName);
}}
size="small"
Expand Down Expand Up @@ -248,6 +252,7 @@ const ProjectsManage = props => {
socket={props.socket}
changeProject={props.changeProject}
onClose={() => setShowPermissionsDialog(false)}
loadProject={props.loadProject}
/> : null}
{importDialog ? <ImportProjectDialog
projects={props.projects}
Expand Down
1 change: 0 additions & 1 deletion src/src/Utils.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { useEffect, useRef } from 'react';
// @ts-expect-error does not affect react, maybe fix later
import { usePreview } from 'react-dnd-preview';
import { Timer } from '@/types';

Expand Down
Loading

0 comments on commit d21f41d

Please sign in to comment.