ui: migrating duck/roles to mobx
This commit is contained in:
parent
98e50d0e96
commit
0f89770560
13 changed files with 522 additions and 370 deletions
|
|
@ -59,7 +59,7 @@ export default class APIClient {
|
|||
|
||||
constructor() {
|
||||
const jwt = store.getState().getIn(['user', 'jwt']);
|
||||
const siteId = store.getState().site.siteId;
|
||||
const siteId = store.getState().getIn(['site', 'siteId']);
|
||||
this.init = {
|
||||
headers: new Headers({
|
||||
Accept: 'application/json',
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import { Loader, NoContent, Button, Tooltip } from 'UI';
|
|||
import { connect } from 'react-redux';
|
||||
import stl from './roles.module.css';
|
||||
import RoleForm from './components/RoleForm';
|
||||
import { init, edit, fetchList, remove as deleteRole, resetErrors } from 'Duck/roles';
|
||||
import RoleItem from './components/RoleItem';
|
||||
import { confirm } from 'UI';
|
||||
import { toast } from 'react-toastify';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { useStore } from "App/mstore";
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
|
|
@ -27,25 +27,23 @@ interface Props {
|
|||
}
|
||||
|
||||
function Roles(props: Props) {
|
||||
const { loading, roles, init, edit, deleteRole, account, permissionsMap, projectsMap, removeErrors } = props;
|
||||
const { roleStore } = useStore();
|
||||
const roles = roleStore.list;
|
||||
const loading = roleStore.loading;
|
||||
const init = roleStore.init;
|
||||
const deleteRole = roleStore.deleteRole;
|
||||
const permissionsMap: any = {};
|
||||
roleStore.permissions.forEach((p: any) => {
|
||||
permissionsMap[p.value] = p.text;
|
||||
});
|
||||
const { account, projectsMap } = props;
|
||||
const { showModal, hideModal } = useModal();
|
||||
const isAdmin = account.admin || account.superAdmin;
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchList();
|
||||
void roleStore.fetchRoles();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (removeErrors && removeErrors.size > 0) {
|
||||
removeErrors.forEach((e: any) => {
|
||||
toast.error(e);
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
props.resetErrors();
|
||||
};
|
||||
}, [removeErrors]);
|
||||
|
||||
const editHandler = (role: any) => {
|
||||
init(role);
|
||||
showModal(<RoleForm closeModal={hideModal} permissionsMap={permissionsMap} deleteHandler={deleteHandler} />, { right: true });
|
||||
|
|
@ -110,24 +108,13 @@ function Roles(props: Props) {
|
|||
|
||||
export default connect(
|
||||
(state: any) => {
|
||||
const permissions = state.getIn(['roles', 'permissions']);
|
||||
const permissionsMap: any = {};
|
||||
permissions.forEach((p: any) => {
|
||||
permissionsMap[p.value] = p.text;
|
||||
});
|
||||
const projects = state.getIn(['site', 'list']);
|
||||
return {
|
||||
instance: state.getIn(['roles', 'instance']) || null,
|
||||
permissionsMap: permissionsMap,
|
||||
roles: state.getIn(['roles', 'list']),
|
||||
removeErrors: state.getIn(['roles', 'removeRequest', 'errors']),
|
||||
loading: state.getIn(['roles', 'fetchRequest', 'loading']),
|
||||
account: state.getIn(['user', 'account']),
|
||||
projectsMap: projects.reduce((acc: any, p: any) => {
|
||||
acc[p.get('id')] = p.get('name');
|
||||
acc[p.id] = p.name;
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
},
|
||||
{ init, edit, fetchList, deleteRole, resetErrors }
|
||||
)(withPageTitle('Roles & Access - OpenReplay Preferences')(Roles));
|
||||
}
|
||||
)(withPageTitle('Roles & Access - OpenReplay Preferences')(observer(Roles)));
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
import React from 'react';
|
||||
import Role from 'Types/role'
|
||||
|
||||
interface Props {
|
||||
role: Role
|
||||
}
|
||||
function Permissions(props: Props) {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Permissions;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './Permissions';
|
||||
|
|
@ -1,196 +1,239 @@
|
|||
import React, { useRef, useEffect } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import stl from './roleForm.module.css';
|
||||
import { save, edit } from 'Duck/roles';
|
||||
import { Form, Input, Button, Checkbox, Icon } from 'UI';
|
||||
|
||||
|
||||
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Button, Checkbox, Form, Icon, Input } from 'UI';
|
||||
|
||||
|
||||
|
||||
import Select from 'Shared/Select';
|
||||
|
||||
|
||||
|
||||
import stl from './roleForm.module.css';
|
||||
|
||||
|
||||
interface Permission {
|
||||
name: string;
|
||||
value: string;
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
role: any;
|
||||
edit: (role: any) => void;
|
||||
save: (role: any) => Promise<void>;
|
||||
closeModal: (toastMessage?: string) => void;
|
||||
saving: boolean;
|
||||
permissions: Array<Permission>[];
|
||||
projectOptions: Array<any>[];
|
||||
permissionsMap: any;
|
||||
projectsMap: any;
|
||||
deleteHandler: (id: any) => Promise<void>;
|
||||
closeModal: (toastMessage?: string) => void;
|
||||
projects: any[];
|
||||
permissionsMap: any;
|
||||
deleteHandler: (id: any) => Promise<void>;
|
||||
}
|
||||
|
||||
const RoleForm = (props: Props) => {
|
||||
const { role, edit, save, closeModal, saving, permissions, projectOptions, permissionsMap, projectsMap } = props;
|
||||
let focusElement = useRef<any>(null);
|
||||
const _save = () => {
|
||||
save(role).then(() => {
|
||||
closeModal(role.exists() ? 'Role updated' : 'Role created');
|
||||
});
|
||||
};
|
||||
const { roleStore } = useStore();
|
||||
const role = roleStore.instance;
|
||||
const saving = roleStore.loading;
|
||||
const { closeModal, permissionsMap, projects } = props;
|
||||
const projectOptions = projects
|
||||
.filter(({ value }) => !role.projects.includes(value))
|
||||
.map((p: any) => ({
|
||||
key: p.id,
|
||||
value: p.id,
|
||||
label: p.name,
|
||||
}))
|
||||
.filter(({ value }: any) => !role.projects.includes(value));
|
||||
const projectsMap = projects.reduce((acc: any, p: any) => {
|
||||
acc[p.id] = p.name;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const write = ({ target: { value, name } }: any) => edit({ [name]: value });
|
||||
let focusElement = useRef<any>(null);
|
||||
const permissions: {}[] = roleStore.permissions
|
||||
.filter(({ value }) => !role.permissions.includes(value))
|
||||
.map((p) => ({
|
||||
label: p.text,
|
||||
value: p.value,
|
||||
}));
|
||||
const _save = () => {
|
||||
roleStore.saveRole(role).then(() => {
|
||||
closeModal(role.exists() ? 'Role updated' : 'Role created');
|
||||
});
|
||||
};
|
||||
|
||||
const onChangePermissions = (e: any) => {
|
||||
const { permissions } = role;
|
||||
const index = permissions.indexOf(e);
|
||||
const _perms = permissions.contains(e) ? permissions.remove(index) : permissions.push(e);
|
||||
edit({ permissions: _perms });
|
||||
};
|
||||
const write = ({ target: { value, name } }: any) => roleStore.editRole({ [name]: value });
|
||||
|
||||
const onChangeProjects = (e: any) => {
|
||||
const { projects } = role;
|
||||
const index = projects.indexOf(e);
|
||||
const _projects = index === -1 ? projects.push(e) : projects.remove(index);
|
||||
edit({ projects: _projects });
|
||||
};
|
||||
const onChangePermissions = (e: any) => {
|
||||
const { permissions } = role;
|
||||
const index = permissions.indexOf(e);
|
||||
let _perms;
|
||||
if (permissions.includes(e)) {
|
||||
permissions.splice(index, 1);
|
||||
_perms = permissions;
|
||||
} else {
|
||||
_perms = permissions.concat(e);
|
||||
}
|
||||
roleStore.editRole({ permissions: _perms });
|
||||
};
|
||||
|
||||
const writeOption = ({ name, value }: any) => {
|
||||
if (name === 'permissions') {
|
||||
onChangePermissions(value);
|
||||
} else if (name === 'projects') {
|
||||
onChangeProjects(value);
|
||||
}
|
||||
};
|
||||
const onChangeProjects = (e: any) => {
|
||||
const { projects } = role;
|
||||
const index = projects.indexOf(e);
|
||||
let _projects;
|
||||
if (index === -1) {
|
||||
_projects = projects.concat(e)
|
||||
} else {
|
||||
projects.splice(index, 1)
|
||||
_projects = projects
|
||||
}
|
||||
roleStore.editRole({ projects: _projects });
|
||||
};
|
||||
|
||||
const toggleAllProjects = () => {
|
||||
const { allProjects } = role;
|
||||
edit({ allProjects: !allProjects });
|
||||
};
|
||||
const writeOption = ({ name, value }: any) => {
|
||||
if (name === 'permissions') {
|
||||
onChangePermissions(value);
|
||||
} else if (name === 'projects') {
|
||||
onChangeProjects(value);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
focusElement && focusElement.current && focusElement.current.focus();
|
||||
}, []);
|
||||
const toggleAllProjects = () => {
|
||||
const { allProjects } = role;
|
||||
roleStore.editRole({ allProjects: !allProjects });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">{role.exists() ? 'Edit Role' : 'Create Role'}</h3>
|
||||
<div className="px-5">
|
||||
<Form onSubmit={_save}>
|
||||
<Form.Field>
|
||||
<label>{'Title'}</label>
|
||||
<Input
|
||||
ref={focusElement}
|
||||
name="name"
|
||||
value={role.name}
|
||||
onChange={write}
|
||||
maxLength={40}
|
||||
className={stl.input}
|
||||
id="name-field"
|
||||
placeholder="Ex. Admin"
|
||||
/>
|
||||
</Form.Field>
|
||||
useEffect(() => {
|
||||
focusElement && focusElement.current && focusElement.current.focus();
|
||||
}, []);
|
||||
|
||||
<Form.Field>
|
||||
<label>{'Project Access'}</label>
|
||||
return (
|
||||
<div
|
||||
className="bg-white h-screen overflow-y-auto"
|
||||
style={{ width: '350px' }}
|
||||
>
|
||||
<h3 className="p-5 text-2xl">
|
||||
{role.exists() ? 'Edit Role' : 'Create Role'}
|
||||
</h3>
|
||||
<div className="px-5">
|
||||
<Form onSubmit={_save}>
|
||||
<Form.Field>
|
||||
<label>{'Title'}</label>
|
||||
<Input
|
||||
ref={focusElement}
|
||||
name="name"
|
||||
value={role.name}
|
||||
onChange={write}
|
||||
maxLength={40}
|
||||
className={stl.input}
|
||||
id="name-field"
|
||||
placeholder="Ex. Admin"
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
<div className="flex my-3">
|
||||
<Checkbox
|
||||
name="allProjects"
|
||||
className="font-medium mr-3"
|
||||
type="checkbox"
|
||||
checked={role.allProjects}
|
||||
onClick={toggleAllProjects}
|
||||
label={''}
|
||||
/>
|
||||
<div className="cursor-pointer leading-none select-none" onClick={toggleAllProjects}>
|
||||
<div>All Projects</div>
|
||||
<span className="text-xs text-gray-600">(Uncheck to select specific projects)</span>
|
||||
</div>
|
||||
</div>
|
||||
{!role.allProjects && (
|
||||
<>
|
||||
<Select
|
||||
isSearchable
|
||||
name="projects"
|
||||
options={projectOptions}
|
||||
onChange={({ value }: any) => writeOption({ name: 'projects', value: value.value })}
|
||||
value={null}
|
||||
/>
|
||||
{role.projects.size > 0 && (
|
||||
<div className="flex flex-row items-start flex-wrap mt-4">
|
||||
{role.projects.map((p: any) => OptionLabel(projectsMap, p, onChangeProjects))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>{'Project Access'}</label>
|
||||
|
||||
<Form.Field>
|
||||
<label>{'Capability Access'}</label>
|
||||
<Select
|
||||
isSearchable
|
||||
name="permissions"
|
||||
options={permissions}
|
||||
onChange={({ value }: any) => writeOption({ name: 'permissions', value: value.value })}
|
||||
value={null}
|
||||
/>
|
||||
{role.permissions.size > 0 && (
|
||||
<div className="flex flex-row items-start flex-wrap mt-4">
|
||||
{role.permissions.map((p: any) => OptionLabel(permissionsMap, p, onChangePermissions))}
|
||||
</div>
|
||||
)}
|
||||
</Form.Field>
|
||||
</Form>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center mr-auto">
|
||||
<Button onClick={_save} disabled={!role.validate()} loading={saving} variant="primary" className="float-left mr-2">
|
||||
{role.exists() ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
{role.exists() && <Button onClick={closeModal}>{'Cancel'}</Button>}
|
||||
</div>
|
||||
{role.exists() && (
|
||||
<Button variant="text" onClick={() => props.deleteHandler(role)}>
|
||||
<Icon name="trash" size="18" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex my-3">
|
||||
<Checkbox
|
||||
name="allProjects"
|
||||
className="font-medium mr-3"
|
||||
type="checkbox"
|
||||
checked={role.allProjects}
|
||||
onClick={toggleAllProjects}
|
||||
label={''}
|
||||
/>
|
||||
<div
|
||||
className="cursor-pointer leading-none select-none"
|
||||
onClick={toggleAllProjects}
|
||||
>
|
||||
<div>All Projects</div>
|
||||
<span className="text-xs text-gray-600">
|
||||
(Uncheck to select specific projects)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{!role.allProjects && (
|
||||
<>
|
||||
<Select
|
||||
isSearchable
|
||||
name="projects"
|
||||
options={projectOptions}
|
||||
onChange={({ value }: any) =>
|
||||
writeOption({ name: 'projects', value: value.value })
|
||||
}
|
||||
value={null}
|
||||
/>
|
||||
{role.projects.size > 0 && (
|
||||
<div className="flex flex-row items-start flex-wrap mt-4">
|
||||
{role.projects.map((p: any) =>
|
||||
OptionLabel(projectsMap, p, onChangeProjects)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field>
|
||||
<label>{'Capability Access'}</label>
|
||||
<Select
|
||||
isSearchable
|
||||
name="permissions"
|
||||
options={permissions}
|
||||
onChange={({ value }: any) =>
|
||||
writeOption({ name: 'permissions', value: value.value })
|
||||
}
|
||||
value={null}
|
||||
/>
|
||||
{role.permissions.length > 0 && (
|
||||
<div className="flex flex-row items-start flex-wrap mt-4">
|
||||
{role.permissions.map((p: any) =>
|
||||
OptionLabel(permissionsMap, p, onChangePermissions)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Form.Field>
|
||||
</Form>
|
||||
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center mr-auto">
|
||||
<Button
|
||||
onClick={_save}
|
||||
disabled={!role.validate}
|
||||
loading={saving}
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{role.exists() ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
{role.exists() && <Button onClick={closeModal}>{'Cancel'}</Button>}
|
||||
</div>
|
||||
{role.exists() && (
|
||||
<Button variant="text" onClick={() => props.deleteHandler(role)}>
|
||||
<Icon name="trash" size="18" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
(state: any) => {
|
||||
const role = state.getIn(['roles', 'instance']);
|
||||
const projects = state.getIn(['site', 'list']);
|
||||
return {
|
||||
role,
|
||||
projectOptions: projects
|
||||
.map((p: any) => ({
|
||||
key: p.get('id'),
|
||||
value: p.get('id'),
|
||||
label: p.get('name'),
|
||||
// isDisabled: role.projects.includes(p.get('id')),
|
||||
}))
|
||||
.filter(({ value }: any) => !role.projects.includes(value))
|
||||
.toJS(),
|
||||
permissions: state
|
||||
.getIn(['roles', 'permissions'])
|
||||
.filter(({ value }: any) => !role.permissions.includes(value))
|
||||
.map(({ text, value }: any) => ({ label: text, value }))
|
||||
.toJS(),
|
||||
saving: state.getIn(['roles', 'saveRequest', 'loading']),
|
||||
projectsMap: projects.reduce((acc: any, p: any) => {
|
||||
acc[p.get('id')] = p.get('name');
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
},
|
||||
{ edit, save }
|
||||
)(RoleForm);
|
||||
export default connect((state: any) => {
|
||||
const projects = state.getIn(['site', 'list']);
|
||||
return {
|
||||
projects,
|
||||
};
|
||||
})(observer(RoleForm));
|
||||
|
||||
function OptionLabel(nameMap: any, p: any, onChangeOption: (e: any) => void) {
|
||||
return (
|
||||
<div className="px-2 py-1 rounded bg-gray-lightest mr-2 mb-2 border flex items-center justify-between" key={p.roleId}>
|
||||
<div>{nameMap[p]}</div>
|
||||
<div className="cursor-pointer ml-2" onClick={() => onChangeOption(p)}>
|
||||
<Icon name="close" size="12" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className="px-2 py-1 rounded bg-gray-lightest mr-2 mb-2 border flex items-center justify-between"
|
||||
key={p.roleId}
|
||||
>
|
||||
<div>{nameMap[p]}</div>
|
||||
<div className="cursor-pointer ml-2" onClick={() => onChangeOption(p)}>
|
||||
<Icon name="close" size="12" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import customFields from './customField';
|
|||
import integrations from './integrations';
|
||||
import errors from './errors';
|
||||
import funnels from './funnels';
|
||||
import roles from './roles';
|
||||
import customMetrics from './customMetrics';
|
||||
import search from './search';
|
||||
import liveSearch from './liveSearch';
|
||||
|
|
@ -31,7 +30,6 @@ const rootReducer = combineReducers({
|
|||
customFields,
|
||||
errors,
|
||||
funnels,
|
||||
roles,
|
||||
customMetrics,
|
||||
search,
|
||||
liveSearch,
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
import { List, Map } from 'immutable';
|
||||
import Role from 'Types/role';
|
||||
import crudDuckGenerator from './tools/crudDuck';
|
||||
import { reduceDucks } from 'Duck/tools';
|
||||
import { createListUpdater } from './funcTools/tools';
|
||||
|
||||
const crudDuck = crudDuckGenerator('client/role', Role, { idKey: 'roleId' });
|
||||
export const { fetchList, init, edit, remove } = crudDuck.actions;
|
||||
|
||||
const RESET_ERRORS = 'roles/RESET_ERRORS';
|
||||
|
||||
const initialState = Map({
|
||||
list: List(),
|
||||
permissions: List([
|
||||
{ text: 'Session Replay', value: 'SESSION_REPLAY' },
|
||||
{ text: 'Developer Tools', value: 'DEV_TOOLS' },
|
||||
// { text: 'Errors', value: 'ERRORS' },
|
||||
{ text: 'Dashboard', value: 'METRICS' },
|
||||
{ text: 'Assist (Live)', value: 'ASSIST_LIVE' },
|
||||
{ text: 'Assist (Call)', value: 'ASSIST_CALL' },
|
||||
{ text: 'Feature Flags', value: 'FEATURE_FLAGS' },
|
||||
{ text: 'Spots', value: "SPOT" },
|
||||
{ text: 'Change Spot Visibility', value: 'SPOT_PUBLIC' }
|
||||
]),
|
||||
});
|
||||
|
||||
// const name = "role";
|
||||
const idKey = 'roleId';
|
||||
|
||||
const updateItemInList = createListUpdater(idKey);
|
||||
const updateInstance = (state, instance) =>
|
||||
state.getIn(['instance', idKey]) === instance[idKey]
|
||||
? state.mergeIn(['instance'], instance)
|
||||
: state;
|
||||
|
||||
const reducer = (state = initialState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case RESET_ERRORS:
|
||||
return state.setIn(['removeRequest', 'errors'], null);
|
||||
case crudDuck.actionTypes.SAVE.SUCCESS:
|
||||
return updateItemInList(updateInstance(state, action.data), Role(action.data));
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export function save(instance) {
|
||||
return {
|
||||
types: crudDuck.actionTypes.SAVE.toArray(),
|
||||
call: (client) =>
|
||||
instance.roleId
|
||||
? client.post(`/client/roles/${instance.roleId}`, instance.toData())
|
||||
: client.put(`/client/roles`, instance.toData()),
|
||||
};
|
||||
}
|
||||
|
||||
export function resetErrors() {
|
||||
return {
|
||||
type: RESET_ERRORS,
|
||||
};
|
||||
}
|
||||
|
||||
export default reduceDucks(crudDuck, { initialState, reducer }).reducer;
|
||||
129
frontend/app/duck/siteSlice.ts
Normal file
129
frontend/app/duck/siteSlice.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { PayloadAction, createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
||||
import Site, { ISite } from "Types/site";
|
||||
import GDPR, { IGDPR } from 'Types/site/gdpr';
|
||||
import { apiClient } from 'App/api_client';
|
||||
|
||||
import { GLOBAL_HAS_NO_RECORDINGS, SITE_ID_STORAGE_KEY } from "../constants/storageKeys";
|
||||
import { array } from "./funcTools/tools";
|
||||
|
||||
|
||||
const storedSiteId = localStorage.getItem(SITE_ID_STORAGE_KEY);
|
||||
|
||||
interface SiteState {
|
||||
list: ISite[];
|
||||
instance: ISite | null;
|
||||
remainingSites?: number;
|
||||
siteId?: number;
|
||||
active: ISite | null;
|
||||
}
|
||||
|
||||
const initialState: SiteState = {
|
||||
list: [],
|
||||
instance: null,
|
||||
remainingSites: undefined,
|
||||
siteId: undefined,
|
||||
active: null,
|
||||
};
|
||||
|
||||
const siteSlice = createSlice({
|
||||
name: 'site',
|
||||
initialState,
|
||||
reducers: {
|
||||
init: (state, action: PayloadAction<ISite>) => {
|
||||
state.instance = action.payload;
|
||||
},
|
||||
editGDPR(state, action: PayloadAction<IGDPR>) {
|
||||
state.instance = {
|
||||
...state.instance!,
|
||||
gdpr: action.payload,
|
||||
}
|
||||
},
|
||||
setSiteId(state, action: PayloadAction<string>) {
|
||||
const siteId = action.payload;
|
||||
const site = state.list.find((s) => s.id === parseInt(siteId));
|
||||
if (site) {
|
||||
state.siteId = siteId;
|
||||
state.active = site;
|
||||
localStorage.setItem(SITE_ID_STORAGE_KEY, siteId);
|
||||
}
|
||||
},
|
||||
updateProjectRecordingStatus(state, action: PayloadAction<{ siteId: string; status: boolean }>) {
|
||||
const { siteId, status } = action.payload;
|
||||
const site = state.list.find((s) => s.id === parseInt(siteId));
|
||||
if (site) {
|
||||
site.recorded = status;
|
||||
}
|
||||
},
|
||||
fetchGDPRSuccess(state, action: { data: IGDPR }) {
|
||||
state.instance = {
|
||||
...state.instance!,
|
||||
gdpr: GDPR(action.data),
|
||||
}
|
||||
},
|
||||
saveSiteSuccess(state, action: { data: ISite }) {
|
||||
const newSite = Site(action.data);
|
||||
state.siteId = newSite.id;
|
||||
state.instance = newSite;
|
||||
state.active = newSite;
|
||||
},
|
||||
saveGDPRSuccess(state, action: { data: IGDPR }) {
|
||||
const gdpr = GDPR(action.data);
|
||||
state.instance = {
|
||||
...state.instance!,
|
||||
gdpr: gdpr,
|
||||
}
|
||||
},
|
||||
fetchListSuccess(state, action: { data: ISite[], siteIdFromPath: number }) {
|
||||
const siteId = state.siteId;
|
||||
const ids = action.data.map(s => parseInt(s.projectId))
|
||||
const siteExists = ids.includes(parseInt(siteId!));
|
||||
if (action.siteIdFromPath && ids.includes(parseInt(action.siteIdFromPath))) {
|
||||
state.siteId = action.siteIdFromPath;
|
||||
} else if (!siteId || !siteExists) {
|
||||
state.siteId = ids.includes(parseInt(storedSiteId!))
|
||||
? storedSiteId
|
||||
: action.data[0].projectId;
|
||||
}
|
||||
const list = action.data.map(Site);
|
||||
const hasRecordings = list.some(s => s.recorded);
|
||||
if (!hasRecordings) {
|
||||
localStorage.setItem(GLOBAL_HAS_NO_RECORDINGS, 'true');
|
||||
} else {
|
||||
localStorage.removeItem(GLOBAL_HAS_NO_RECORDINGS);
|
||||
}
|
||||
|
||||
state.list = list;
|
||||
state.active = list.find(s => parseInt(s.id) === parseInt(state.siteId!));
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export function save(site: ISite) {
|
||||
return {
|
||||
types: ['sites/saveSiteRequest', 'sites/saveSiteSuccess', 'sites/saveSiteFail'],
|
||||
call: (client) => client.post(`/projects`, site),
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchGDPR(siteId: number) {
|
||||
return {
|
||||
types: ['sites/fetchGDPRRequest', 'sites/fetchGDPRSuccess', 'sites/fetchGDPRFail'],
|
||||
call: (client) => client.get(`/${siteId}/gdpr`),
|
||||
};
|
||||
}
|
||||
|
||||
export const saveGDPR = (siteId: number) => (dispatch, getState) => {
|
||||
const g = getState().site.instance.gdpr;
|
||||
return dispatch({
|
||||
types: ['sites/saveGDPRRequest', 'sites/saveGDPRSuccess', 'sites/saveGDPRFail'],
|
||||
call: client => client.post(`/${siteId}/gdpr`, g)
|
||||
});
|
||||
};
|
||||
|
||||
export function fetchList(siteId: number) {
|
||||
return {
|
||||
types: ['sites/fetchListRequest', 'sites/fetchListSuccess', 'sites/fetchListFail'],
|
||||
call: client => client.get('/projects'),
|
||||
siteIdFromPath: siteId
|
||||
};
|
||||
}
|
||||
|
|
@ -1,31 +1,109 @@
|
|||
import { makeAutoObservable, observable } from "mobx"
|
||||
import { userService } from "App/services";
|
||||
import Role, { IRole } from "./types/role";
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
|
||||
import { userService } from 'App/services';
|
||||
|
||||
import Role from './types/role';
|
||||
|
||||
const permissions = [
|
||||
{ text: 'Session Replay', value: 'SESSION_REPLAY' },
|
||||
{ text: 'Developer Tools', value: 'DEV_TOOLS' },
|
||||
{ text: 'Dashboard', value: 'METRICS' },
|
||||
{ text: 'Assist (Live)', value: 'ASSIST_LIVE' },
|
||||
{ text: 'Assist (Call)', value: 'ASSIST_CALL' },
|
||||
{ text: 'Feature Flags', value: 'FEATURE_FLAGS' },
|
||||
{ text: 'Spots', value: "SPOT" },
|
||||
{ text: 'Change Spot Visibility', value: 'SPOT_PUBLIC' }
|
||||
]
|
||||
|
||||
export default class UserStore {
|
||||
list: IRole[] = [];
|
||||
loading: boolean = false;
|
||||
list: Role[] = [];
|
||||
loading: boolean = false;
|
||||
permissions = permissions;
|
||||
instance = new Role();
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
list: observable,
|
||||
loading: observable,
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
toggleLoading = (val: boolean) => {
|
||||
this.loading = val;
|
||||
}
|
||||
|
||||
setRoles = (roles: Role[]) => {
|
||||
this.list = roles;
|
||||
}
|
||||
|
||||
init = (role?: any) => {
|
||||
if (role) {
|
||||
this.instance = new Role().fromJson(role);
|
||||
} else {
|
||||
this.instance = new Role();
|
||||
}
|
||||
}
|
||||
|
||||
editRole = (role: Partial<Role>) => {
|
||||
Object.assign(this.instance, role)
|
||||
}
|
||||
|
||||
saveRole = async (role: Role): Promise<void> => {
|
||||
if (role.roleId) {
|
||||
return this.modifyRole(role);
|
||||
} else {
|
||||
return this.createRole(role);
|
||||
}
|
||||
}
|
||||
|
||||
deleteRole = async (role: Role): Promise<void> => {
|
||||
this.toggleLoading(true)
|
||||
try {
|
||||
const { data } = await userService.deleteRole(role.roleId);
|
||||
this.setRoles(data.map((role: any) => new Role().fromJson(role)));
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
this.toggleLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
createRole = async (role: Role): Promise<void> => {
|
||||
this.toggleLoading(true)
|
||||
try {
|
||||
const { data } = await userService.createRole(role);
|
||||
this.setRoles([...this.list, new Role().fromJson(data)]);
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
this.toggleLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
modifyRole = async (role: Role): Promise<void> => {
|
||||
this.toggleLoading(true)
|
||||
try {
|
||||
const { data } = await userService.modifyRole(role);
|
||||
this.setRoles(this.list.map((r) => r.roleId === data.roleId ? new Role().fromJson(data) : r));
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
this.toggleLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchRoles = (): Promise<any> => {
|
||||
this.toggleLoading(true)
|
||||
return new Promise((resolve, reject) => {
|
||||
userService
|
||||
.getRoles()
|
||||
.then((response) => {
|
||||
this.setRoles(response.map((role: any) => new Role().fromJson(role)))
|
||||
resolve(response);
|
||||
})
|
||||
}
|
||||
|
||||
fetchRoles(): Promise<any> {
|
||||
this.loading = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
userService.getRoles()
|
||||
.then(response => {
|
||||
this.list = response.map((role: any) => new Role().fromJson(role));
|
||||
resolve(response);
|
||||
}).catch(error => {
|
||||
this.loading = false;
|
||||
reject(error);
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.toggleLoading(false)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,49 @@
|
|||
import { makeAutoObservable, observable, runInAction } from "mobx";
|
||||
import { makeAutoObservable, runInAction } from 'mobx';
|
||||
|
||||
import { notEmptyString, validateName } from 'App/validate';
|
||||
|
||||
export default class Role {
|
||||
roleId: string = '';
|
||||
name: string = '';
|
||||
description: string = '';
|
||||
isProtected: boolean = false;
|
||||
roleId: string = '';
|
||||
name: string = '';
|
||||
description: string = '';
|
||||
permissions: string[] = [];
|
||||
createdAt: number = 0;
|
||||
isProtected: boolean = false;
|
||||
serviceRole: boolean = false;
|
||||
allProjects = false;
|
||||
projects: string[] = [];
|
||||
protected = false;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
roleId: observable,
|
||||
name: observable,
|
||||
description: observable,
|
||||
})
|
||||
}
|
||||
fromJson(json: any) {
|
||||
runInAction(() => {
|
||||
Object.assign(this, json);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
fromJson(json: any) {
|
||||
runInAction(() => {
|
||||
this.roleId = json.roleId;
|
||||
this.name = json.name;
|
||||
this.description = json.description;
|
||||
this.isProtected = json.protected;
|
||||
})
|
||||
return this;
|
||||
}
|
||||
get validate() {
|
||||
return (
|
||||
notEmptyString(this.name) &&
|
||||
validateName(this.name, { diacritics: true }) &&
|
||||
(this.allProjects || this.projects.length > 0)
|
||||
);
|
||||
};
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
id: this.roleId,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
}
|
||||
}
|
||||
exists() {
|
||||
return Boolean(this.roleId);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
id: this.roleId,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
permissions: this.permissions,
|
||||
allProjects: this.allProjects,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,21 @@ export default class UserService {
|
|||
.then((response: { data: any; }) => response.data || []);
|
||||
}
|
||||
|
||||
createRole(role: any) {
|
||||
return this.client.post('/client/roles/', role)
|
||||
.then(r => r.json())
|
||||
}
|
||||
|
||||
modifyRole(role: any) {
|
||||
return this.client.put(`/client/roles/${role.roleId}`, role)
|
||||
.then(r => r.json())
|
||||
}
|
||||
|
||||
deleteRole(roleId: string) {
|
||||
return this.client.delete(`/client/roles/${roleId}`)
|
||||
.then(r => r.json())
|
||||
}
|
||||
|
||||
getLimits() {
|
||||
return this.client.get('/limits')
|
||||
.then((response: { json: () => any; }) => response.json())
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
import Record from 'Types/Record';
|
||||
import { notEmptyString, validateName } from 'App/validate';
|
||||
import { List } from 'immutable';
|
||||
|
||||
export default Record({
|
||||
roleId: undefined,
|
||||
name: '',
|
||||
allProjects: true,
|
||||
permissions: List(),
|
||||
projects: List(),
|
||||
protected: false,
|
||||
description: '',
|
||||
permissionOptions: List(),
|
||||
}, {
|
||||
idKey: 'roleId',
|
||||
methods: {
|
||||
validate() {
|
||||
return notEmptyString(this.name) && validateName(this.name, { diacritics: true }) && (this.allProjects || this.projects.size > 0);
|
||||
},
|
||||
toData() {
|
||||
const js = this.toJS();
|
||||
delete js.key;
|
||||
delete js.protected;
|
||||
return js;
|
||||
},
|
||||
},
|
||||
fromJS({ projects = [], permissions = [], ...rest }) {
|
||||
return {
|
||||
...rest,
|
||||
permissions: List(permissions),
|
||||
projects: List(projects),
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import GDPR, { IGDPR } from './gdpr';
|
||||
|
||||
export interface ISite {
|
||||
id?: number;
|
||||
id?: string;
|
||||
name: string;
|
||||
host: string;
|
||||
platform: string;
|
||||
|
|
@ -37,7 +37,8 @@ export default function Site(data: Partial<ISite>): ISite {
|
|||
|
||||
return Object.assign({}, defaults, {
|
||||
...data,
|
||||
gdpr: GDPR(data.gdpr),
|
||||
host: data.name,
|
||||
id: data?.projectId?.toString(),
|
||||
gdpr: GDPR(data ? data.gdpr : undefined),
|
||||
host: data ? data.name : '',
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue