feat(user-interface): introduce user interface page (#1226)

This commit is contained in:
Rıdvan Akca
2023-08-22 00:11:25 +03:00
committed by GitHub
parent 9f9ee0bb58
commit da5d594428
10 changed files with 294 additions and 42 deletions

View File

@@ -8,7 +8,11 @@ type Params = {
};
};
const updateConfig = async (_parent: unknown, params: Params, context: Context) => {
const updateConfig = async (
_parent: unknown,
params: Params,
context: Context
) => {
context.currentUser.can('update', 'Config');
const config = params.input;
@@ -18,22 +22,26 @@ const updateConfig = async (_parent: unknown, params: Params, context: Context)
for (const key of configKeys) {
const newValue = config[key];
const entryUpdate = Config
.query()
.insert({
key,
value: {
data: newValue
}
})
.onConflict('key')
.merge({
value: {
data: newValue
}
});
if (newValue) {
const entryUpdate = Config.query()
.insert({
key,
value: {
data: newValue,
},
})
.onConflict('key')
.merge({
value: {
data: newValue,
},
});
updates.push(entryUpdate);
updates.push(entryUpdate);
} else {
const entryUpdate = Config.query().findOne({ key }).delete();
updates.push(entryUpdate);
}
}
await Promise.all(updates);

View File

@@ -30,6 +30,7 @@
"graphql": "^15.6.0",
"lodash": "^4.17.21",
"luxon": "^2.3.1",
"mui-color-input": "^2.0.0",
"notistack": "^2.0.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",

View File

@@ -6,6 +6,7 @@ import CreateUser from 'pages/CreateUser';
import Roles from 'pages/Roles/index.ee';
import CreateRole from 'pages/CreateRole/index.ee';
import EditRole from 'pages/EditRole/index.ee';
import UserInterface from 'pages/UserInterface';
import * as URLS from 'config/urls';
import Can from 'components/Can';
@@ -79,6 +80,17 @@ export default (
}
/>
<Route
path={URLS.USER_INTERFACE}
element={
<Can I="update" a="Config">
<AdminSettingsLayout>
<UserInterface />
</AdminSettingsLayout>
</Can>
}
/>
<Route
path={URLS.ADMIN_SETTINGS}
element={<Navigate to={URLS.USERS} replace />}

View File

@@ -1,6 +1,7 @@
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import GroupIcon from '@mui/icons-material/Group';
import GroupsIcon from '@mui/icons-material/Groups';
import BrushIcon from '@mui/icons-material/Brush';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import { useTheme } from '@mui/material/styles';
@@ -18,25 +19,43 @@ type SettingsLayoutProps = {
};
type DrawerLink = {
Icon: SvgIconComponent,
primary: string,
to: string,
}
Icon: SvgIconComponent;
primary: string;
to: string;
};
function createDrawerLinks({ canReadRole, canReadUser }: { canReadRole: boolean; canReadUser: boolean; }) {
function createDrawerLinks({
canReadRole,
canReadUser,
canUpdateConfig,
}: {
canReadRole: boolean;
canReadUser: boolean;
canUpdateConfig: boolean;
}) {
const items = [
canReadUser ? {
Icon: GroupIcon,
primary: 'adminSettingsDrawer.users',
to: URLS.USERS,
} : null,
canReadRole ? {
Icon: GroupsIcon,
primary: 'adminSettingsDrawer.roles',
to: URLS.ROLES,
} : null
]
.filter(Boolean) as DrawerLink[];
canReadUser
? {
Icon: GroupIcon,
primary: 'adminSettingsDrawer.users',
to: URLS.USERS,
}
: null,
canReadRole
? {
Icon: GroupsIcon,
primary: 'adminSettingsDrawer.roles',
to: URLS.ROLES,
}
: null,
canUpdateConfig
? {
Icon: BrushIcon,
primary: 'adminSettingsDrawer.userInterface',
to: URLS.USER_INTERFACE,
}
: null,
].filter(Boolean) as DrawerLink[];
return items;
}
@@ -62,6 +81,7 @@ export default function SettingsLayout({
const drawerLinks = createDrawerLinks({
canReadUser: currentUserAbility.can('read', 'User'),
canReadRole: currentUserAbility.can('read', 'Role'),
canUpdateConfig: currentUserAbility.can('update', 'Config'),
});
return (

View File

@@ -0,0 +1,41 @@
import * as React from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { MuiColorInput, MuiColorInputProps } from 'mui-color-input';
type ColorInputProps = {
shouldUnregister?: boolean;
name: string;
'data-test'?: string;
} & Partial<MuiColorInputProps>;
export default function ColorInput(props: ColorInputProps): React.ReactElement {
const { control } = useFormContext();
const {
required,
name,
shouldUnregister = false,
disabled = false,
'data-test': dataTest,
...textFieldProps
} = props;
return (
<Controller
rules={{ required }}
name={name}
control={control}
shouldUnregister={shouldUnregister}
render={({ field }) => (
<MuiColorInput
format="hex"
{...textFieldProps}
{...field}
disabled={disabled}
inputProps={{
'data-test': dataTest,
}}
/>
)}
/>
);
}

View File

@@ -3,12 +3,12 @@ import appConfig from './app';
export const CONNECTIONS = '/connections';
export const EXECUTIONS = '/executions';
export const EXECUTION_PATTERN = '/executions/:executionId';
export const EXECUTION = (executionId: string) =>
`/executions/${executionId}`;
export const EXECUTION = (executionId: string) => `/executions/${executionId}`;
export const LOGIN = '/login';
export const LOGIN_CALLBACK = `${LOGIN}/callback`;
export const SSO_LOGIN = (issuer: string) => `${appConfig.apiUrl}/login/saml/${issuer}`;
export const SSO_LOGIN = (issuer: string) =>
`${appConfig.apiUrl}/login/saml/${issuer}`;
export const SIGNUP = '/sign-up';
export const FORGOT_PASSWORD = '/forgot-password';
export const RESET_PASSWORD = '/reset-password';
@@ -17,18 +17,19 @@ export const APPS = '/apps';
export const NEW_APP_CONNECTION = '/apps/new';
export const APP = (appKey: string) => `/app/${appKey}`;
export const APP_PATTERN = '/app/:appKey';
export const APP_CONNECTIONS = (appKey: string) =>
`/app/${appKey}/connections`;
export const APP_CONNECTIONS = (appKey: string) => `/app/${appKey}/connections`;
export const APP_CONNECTIONS_PATTERN = '/app/:appKey/connections';
export const APP_ADD_CONNECTION = (appKey: string, shared = false) =>
`/app/${appKey}/connections/add?shared=${shared}`;
export const APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID = (appKey: string, appAuthClientId: string) =>
`/app/${appKey}/connections/add?appAuthClientId=${appAuthClientId}`;
export const APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID = (
appKey: string,
appAuthClientId: string
) => `/app/${appKey}/connections/add?appAuthClientId=${appAuthClientId}`;
export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add';
export const APP_RECONNECT_CONNECTION = (
appKey: string,
connectionId: string,
appAuthClientId?: string,
appAuthClientId?: string
) => {
const path = `/app/${appKey}/connections/${connectionId}/reconnect`;
@@ -96,6 +97,7 @@ export const ROLES = `${ADMIN_SETTINGS}/roles`;
export const ROLE = (roleId: string) => `${ROLES}/${roleId}`;
export const ROLE_PATTERN = `${ROLES}/:roleId`;
export const CREATE_ROLE = `${ROLES}/create`;
export const USER_INTERFACE = `${ADMIN_SETTINGS}/user-interface`;
export const DASHBOARD = FLOWS;

View File

@@ -0,0 +1,18 @@
import { IJSONObject } from '@automatisch/types';
import set from 'lodash/set';
export default function nestObject<T = IJSONObject>(
config: IJSONObject | undefined
): Partial<T> {
if (!config) return {};
const result = {};
for (const key in config) {
if (Object.prototype.hasOwnProperty.call(config, key)) {
const value = config[key];
set(result, key, value);
}
}
return result;
}

View File

@@ -15,6 +15,7 @@
"settingsDrawer.billingAndUsage": "Billing and usage",
"adminSettingsDrawer.users": "Users",
"adminSettingsDrawer.roles": "Roles",
"adminSettingsDrawer.userInterface": "User Interface",
"adminSettingsDrawer.goBack": "Go to the dashboard",
"app.connectionCount": "{count} connections",
"app.flowCount": "{count} flows",
@@ -213,5 +214,12 @@
"permissionSettings.cancel": "Cancel",
"permissionSettings.apply": "Apply",
"permissionSettings.title": "Conditions",
"appAuthClientsDialog.title": "Choose your authentication client"
"appAuthClientsDialog.title": "Choose your authentication client",
"userInterfacePage.title": "User Interface",
"userInterfacePage.successfullyUpdated": "User interface has been updated.",
"userInterfacePage.mainColor": "Primary main color",
"userInterfacePage.darkColor": "Primary dark color",
"userInterfacePage.lightColor": "Primary light color",
"userInterfacePage.svgData": "Logo SVG code",
"userInterfacePage.submit": "Update"
}

View File

@@ -0,0 +1,130 @@
import * as React from 'react';
import { useMutation } from '@apollo/client';
import Container from '@mui/material/Container';
import Grid from '@mui/material/Grid';
import Stack from '@mui/material/Stack';
import LoadingButton from '@mui/lab/LoadingButton';
import { useSnackbar } from 'notistack';
import { UPDATE_CONFIG } from 'graphql/mutations/update-config.ee';
import useConfig from 'hooks/useConfig';
import PageTitle from 'components/PageTitle';
import Form from 'components/Form';
import TextField from 'components/TextField';
import useFormatMessage from 'hooks/useFormatMessage';
import ColorInput from 'components/ColorInput';
import nestObject from 'helpers/nestObject';
import { Skeleton } from '@mui/material';
type UserInterface = {
palette: {
primary: {
dark: string;
light: string;
main: string;
};
};
logo: {
svgData: string;
};
};
export default function UserInterface(): React.ReactElement {
const formatMessage = useFormatMessage();
const [updateConfig, { loading }] = useMutation(UPDATE_CONFIG, {
refetchQueries: ['GetConfig'],
});
const { config, loading: configLoading } = useConfig([
'palette.primary.main',
'palette.primary.light',
'palette.primary.dark',
'logo.svgData',
]);
const { enqueueSnackbar } = useSnackbar();
const handleUserInterfaceUpdate = async (uiData: Partial<UserInterface>) => {
try {
await updateConfig({
variables: {
input: {
'palette.primary.main': uiData?.palette?.primary.main,
'palette.primary.dark': uiData?.palette?.primary.dark,
'palette.primary.light': uiData?.palette?.primary.light,
'logo.svgData': uiData?.logo?.svgData,
},
},
});
enqueueSnackbar(formatMessage('userInterfacePage.successfullyUpdated'), {
variant: 'success',
});
} catch (error) {
throw new Error('Failed while updating!');
}
};
return (
<Container sx={{ py: 3, display: 'flex', justifyContent: 'center' }}>
<Grid container item xs={12} sm={9} md={8} lg={6}>
<Grid item xs={12} sx={{ mb: [2, 5] }}>
<PageTitle>{formatMessage('userInterfacePage.title')}</PageTitle>
</Grid>
<Grid item xs={12} justifyContent="flex-end" sx={{ pt: 5 }}>
{configLoading && (
<Stack direction="column" gap={2}>
<Skeleton variant="rounded" height={55} />
<Skeleton variant="rounded" height={55} />
<Skeleton variant="rounded" height={55} />
<Skeleton variant="rounded" height={85} />
<Skeleton variant="rounded" height={45} />
</Stack>
)}
{!configLoading && (
<Form
onSubmit={handleUserInterfaceUpdate}
defaultValues={nestObject<UserInterface>(config)}
>
<Stack direction="column" gap={2}>
<ColorInput
name="palette.primary.main"
label={formatMessage('userInterfacePage.mainColor')}
fullWidth
/>
<ColorInput
name="palette.primary.dark"
label={formatMessage('userInterfacePage.darkColor')}
fullWidth
/>
<ColorInput
name="palette.primary.light"
label={formatMessage('userInterfacePage.lightColor')}
fullWidth
/>
<TextField
name="logo.svgData"
label={formatMessage('userInterfacePage.svgData')}
multiline
fullWidth
/>
<LoadingButton
type="submit"
variant="contained"
color="primary"
sx={{ boxShadow: 2 }}
loading={loading}
>
{formatMessage('userInterfacePage.submit')}
</LoadingButton>
</Stack>
</Form>
)}
</Grid>
</Grid>
</Container>
);
}

View File

@@ -1469,6 +1469,11 @@
resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-12.0.0.tgz#a9583a75c3f150667771f30b60d9f059473e62c4"
integrity sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg==
"@ctrl/tinycolor@^3.6.0":
version "3.6.0"
resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.6.0.tgz#53fa5fe9c34faee89469e48f91d51a3766108bc8"
integrity sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ==
"@dabh/diagnostics@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31"
@@ -12375,6 +12380,13 @@ msgpackr@^1.6.2:
optionalDependencies:
msgpackr-extract "^2.1.2"
mui-color-input@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/mui-color-input/-/mui-color-input-2.0.0.tgz#49c8df63d3d18f1a1817572c0efc15bd970b35a2"
integrity sha512-Xw6OGsZVbtlZEAUVgJ08Lyv4u0YDQH+aTMJhhWm2fRin+1T+0IrVFyBtbSjJjrH4aBkkQPMCm75//7qO9zncLw==
dependencies:
"@ctrl/tinycolor" "^3.6.0"
multer@1.4.5-lts.1:
version "1.4.5-lts.1"
resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.5-lts.1.tgz#803e24ad1984f58edffbc79f56e305aec5cfd1ac"