From f13d6b5adebfc191b64612a052e683b74955b263 Mon Sep 17 00:00:00 2001 From: Stas Date: Thu, 18 Aug 2022 19:43:20 +0300 Subject: [PATCH 1/9] feat(webapp): add settings/apps page --- .../Settings/Apps/AppTableItem.module.css | 5 + .../components/Settings/Apps/AppTableItem.tsx | 43 +++++++ .../components/Settings/Apps/Apps.module.css | 18 +++ .../components/Settings/Apps/index.tsx | 92 +++++++++++++++ .../javascript/components/Settings/index.tsx | 22 ++++ webapp/javascript/models/app.ts | 16 +++ webapp/javascript/redux/reducers/settings.ts | 110 +++++++++++++++--- webapp/javascript/services/apps.ts | 37 ++++++ 8 files changed, 328 insertions(+), 15 deletions(-) create mode 100644 webapp/javascript/components/Settings/Apps/AppTableItem.module.css create mode 100644 webapp/javascript/components/Settings/Apps/AppTableItem.tsx create mode 100644 webapp/javascript/components/Settings/Apps/Apps.module.css create mode 100644 webapp/javascript/components/Settings/Apps/index.tsx create mode 100644 webapp/javascript/models/app.ts create mode 100644 webapp/javascript/services/apps.ts diff --git a/webapp/javascript/components/Settings/Apps/AppTableItem.module.css b/webapp/javascript/components/Settings/Apps/AppTableItem.module.css new file mode 100644 index 0000000000..0d7fb267ea --- /dev/null +++ b/webapp/javascript/components/Settings/Apps/AppTableItem.module.css @@ -0,0 +1,5 @@ +.actions { + display: flex; + flex-direction: row; + justify-content: center; +} diff --git a/webapp/javascript/components/Settings/Apps/AppTableItem.tsx b/webapp/javascript/components/Settings/Apps/AppTableItem.tsx new file mode 100644 index 0000000000..4062906826 --- /dev/null +++ b/webapp/javascript/components/Settings/Apps/AppTableItem.tsx @@ -0,0 +1,43 @@ +import { faTimes } from '@fortawesome/free-solid-svg-icons/faTimes'; +import Button from '@webapp/ui/Button'; +import Icon from '@webapp/ui/Icon'; +import React from 'react'; + +import confirmDelete from '@webapp/components/Modals/ConfirmDelete'; +import { App } from '@webapp/models/app'; +import cx from 'classnames'; +import styles from './AppTableItem.module.css'; + +function DeleteButton(props: ShamefulAny) { + const { onDelete, app } = props; + + const handleDeleteClick = () => { + confirmDelete('this app', () => { + onDelete(app); + }); + }; + + return ( + + ); +} + +function AppTableItem(props: ShamefulAny) { + const { app, onDelete } = props; + const { name } = app as App; + + return ( + + {name} + +
+ +
+ + + ); +} + +export default AppTableItem; diff --git a/webapp/javascript/components/Settings/Apps/Apps.module.css b/webapp/javascript/components/Settings/Apps/Apps.module.css new file mode 100644 index 0000000000..1a5ab0edee --- /dev/null +++ b/webapp/javascript/components/Settings/Apps/Apps.module.css @@ -0,0 +1,18 @@ +.searchContainer { + display: flex; + flex-direction: column; +} + +.searchContainer button { + padding: 5px 20px; +} + +.appsTable { + margin: 20px 0; + width: 100%; +} + +.appsTableEmptyMessage { + text-align: center; + color: var(--ps-ui-foreground-text); +} diff --git a/webapp/javascript/components/Settings/Apps/index.tsx b/webapp/javascript/components/Settings/Apps/index.tsx new file mode 100644 index 0000000000..a821dcd5e9 --- /dev/null +++ b/webapp/javascript/components/Settings/Apps/index.tsx @@ -0,0 +1,92 @@ +import React, { useEffect, useState } from 'react'; +import { useAppDispatch, useAppSelector } from '@webapp/redux/hooks'; +import { + selectApps, + reloadApps, + deleteApp, +} from '@webapp/redux/reducers/settings'; +import { addNotification } from '@webapp/redux/reducers/notifications'; +import { type App } from '@webapp/models/app'; +import Input from '@webapp/ui/Input'; +import AppTableItem from './AppTableItem'; + +import appsStyles from './Apps.module.css'; +import tableStyles from '../SettingsTable.module.css'; + +function Apps() { + const dispatch = useAppDispatch(); + const apps = useAppSelector(selectApps); + const [search, setSearchField] = useState(''); + + useEffect(() => { + dispatch(reloadApps()); + }, []); + const displayApps = + (apps && + apps.filter( + (x) => + JSON.stringify(x).toLowerCase().indexOf(search.toLowerCase()) !== -1 + )) || + []; + + const handleDeleteApp = (app: App) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + dispatch(deleteApp(app)) + .unwrap() + .then(() => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + dispatch( + addNotification({ + type: 'success', + title: 'App has been deleted', + message: `App name#${app.name} has been successfully deleted`, + }) + ); + }); + }; + + return ( + <> +

Apps

+
+ setSearchField(v.target.value)} + name="Search app input" + /> +
+ + + + + + + + {displayApps.length ? ( + displayApps.map((app) => ( + + )) + ) : ( + + + + )} + +
Name +
+ The list is empty +
+ + ); +} + +export default Apps; diff --git a/webapp/javascript/components/Settings/index.tsx b/webapp/javascript/components/Settings/index.tsx index 8ddf97bfb1..d453ab8965 100644 --- a/webapp/javascript/components/Settings/index.tsx +++ b/webapp/javascript/components/Settings/index.tsx @@ -8,6 +8,7 @@ import { faKey } from '@fortawesome/free-solid-svg-icons/faKey'; import { faLock } from '@fortawesome/free-solid-svg-icons/faLock'; import { faSlidersH } from '@fortawesome/free-solid-svg-icons/faSlidersH'; import { faUserAlt } from '@fortawesome/free-solid-svg-icons/faUserAlt'; +import { faNetworkWired } from '@fortawesome/free-solid-svg-icons/faNetworkWired'; import cx from 'classnames'; import { useAppSelector } from '@webapp/redux/hooks'; import { selectCurrentUser } from '@webapp/redux/reducers/user'; @@ -16,6 +17,7 @@ import PageTitle from '@webapp/components/PageTitle'; import Preferences from './Preferences'; import Security from './Security'; import Users from './Users'; +import Apps from './Apps'; import ApiKeys from './APIKeys'; import styles from './Settings.module.css'; @@ -93,6 +95,20 @@ function Settings() { API keys +
  • + + cx({ + [styles.navLink]: true, + [styles.navLinkActive]: isActive, + }) + } + data-testid="settings-appstab" + > + Apps + +
  • ) : null} @@ -136,6 +152,12 @@ function Settings() { + + <> + + + + diff --git a/webapp/javascript/models/app.ts b/webapp/javascript/models/app.ts new file mode 100644 index 0000000000..ff3a2fa6c6 --- /dev/null +++ b/webapp/javascript/models/app.ts @@ -0,0 +1,16 @@ +import { z, ZodError } from 'zod'; +import { Result } from '@webapp/util/fp'; +import { modelToResult } from './utils'; + +export const appModel = z.object({ + name: z.string(), +}); + +export const appsModel = z.array(appModel); + +export type Apps = z.infer; +export type App = z.infer; + +export function parse(a: unknown): Result { + return modelToResult(appsModel, a); +} diff --git a/webapp/javascript/redux/reducers/settings.ts b/webapp/javascript/redux/reducers/settings.ts index 614bc5a710..bdc2a317f8 100644 --- a/webapp/javascript/redux/reducers/settings.ts +++ b/webapp/javascript/redux/reducers/settings.ts @@ -1,6 +1,7 @@ import { createSlice, combineReducers } from '@reduxjs/toolkit'; import { Users, type User } from '@webapp/models/users'; import { APIKey, APIKeys } from '@webapp/models/apikeys'; +import { Apps, type App } from '@webapp/models/app'; import { fetchUsers, @@ -15,25 +16,36 @@ import { createAPIKey as createAPIKeyAPI, deleteAPIKey as deleteAPIKeyAPI, } from '@webapp/services/apiKeys'; +import { fetchApps, deleteApp as deleteAppAPI } from '@webapp/services/apps'; import type { RootState } from '@webapp/redux/store'; import { addNotification } from './notifications'; import { createAsyncThunk } from '../async-thunk'; -type UsersState = { - type: 'pristine' | 'loading' | 'loaded' | 'failed'; - data?: Users; -}; +enum FetchStatus { + pristine = 'pristine', + loading = 'loading', + loaded = 'loaded', + failed = 'failed', +} +type FetchedState = { type: FetchStatus; data?: T }; +type UsersState = FetchedState; const usersInitialState: UsersState = { - type: 'pristine', + type: FetchStatus.pristine, + data: undefined, +}; + +type ApiKeysState = FetchedState; +const apiKeysInitialState: ApiKeysState = { + type: FetchStatus.pristine, data: undefined, }; -type ApiKeysState = { - type: 'pristine' | 'loading' | 'loaded' | 'failed'; - data?: APIKeys; +type AppsState = FetchedState; +const appsInitialState: AppsState = { + type: FetchStatus.pristine, + data: undefined, }; -const apiKeysInitialState: ApiKeysState = { type: 'pristine', data: undefined }; export const reloadApiKeys = createAsyncThunk( 'newRoot/reloadAPIKeys', @@ -76,6 +88,28 @@ export const reloadUsers = createAsyncThunk( } ); +export const reloadApps = createAsyncThunk( + 'newRoot/reloadApps', + async (_, thunkAPI) => { + const res = await fetchApps(); + + if (res.isOk) { + return Promise.resolve(res.value); + } + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + thunkAPI.dispatch( + addNotification({ + type: 'danger', + title: 'Failed to load apps', + message: res.error.message, + }) + ); + + return Promise.reject(res.error); + } +); + export const enableUser = createAsyncThunk( 'newRoot/enableUser', async (user: User, thunkAPI) => { @@ -231,20 +265,44 @@ export const deleteAPIKey = createAsyncThunk( } ); +export const deleteApp = createAsyncThunk( + 'newRoot/deleteApp', + async (app: App, thunkAPI) => { + const res = await deleteAppAPI({ name: app.name }); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + thunkAPI.dispatch(reloadApps()); + + if (res.isOk) { + return Promise.resolve(true); + } + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + thunkAPI.dispatch( + addNotification({ + type: 'danger', + title: 'Failed to delete app', + message: res.error.message, + }) + ); + return Promise.reject(res.error); + } +); + export const usersSlice = createSlice({ name: 'users', initialState: usersInitialState, reducers: {}, extraReducers: (builder) => { builder.addCase(reloadUsers.fulfilled, (state, action) => { - return { type: 'loaded', data: action.payload }; + return { type: FetchStatus.loaded, data: action.payload }; }); builder.addCase(reloadUsers.pending, (state) => { - return { type: 'loading', data: state.data }; + return { type: FetchStatus.loading, data: state.data }; }); builder.addCase(reloadUsers.rejected, (state) => { - return { type: 'failed', data: state.data }; + return { type: FetchStatus.failed, data: state.data }; }); }, }); @@ -255,13 +313,30 @@ export const apiKeysSlice = createSlice({ reducers: {}, extraReducers: (builder) => { builder.addCase(reloadApiKeys.fulfilled, (_, action) => { - return { type: 'loaded', data: action.payload }; + return { type: FetchStatus.loaded, data: action.payload }; }); builder.addCase(reloadApiKeys.pending, (state) => { - return { type: 'loading', data: state.data }; + return { type: FetchStatus.loading, data: state.data }; }); builder.addCase(reloadApiKeys.rejected, (state) => { - return { type: 'failed', data: state.data }; + return { type: FetchStatus.failed, data: state.data }; + }); + }, +}); + +export const appsSlice = createSlice({ + name: 'apps', + initialState: appsInitialState, + reducers: {}, + extraReducers: (builder) => { + builder.addCase(reloadApps.fulfilled, (_, action) => { + return { type: FetchStatus.loaded, data: action.payload }; + }); + builder.addCase(reloadApps.pending, (state) => { + return { type: FetchStatus.loading, data: state.data }; + }); + builder.addCase(reloadApps.rejected, (state) => { + return { type: FetchStatus.failed, data: state.data }; }); }, }); @@ -274,7 +349,12 @@ export const selectUsers = (state: RootState) => state.settings.users.data; export const apiKeysState = (state: RootState) => state.settings.apiKeys; export const selectAPIKeys = (state: RootState) => state.settings.apiKeys.data; +export const appsState = (state: RootState) => state.settings.apps; +export const selectApps = (state: RootState) => state.settings.apps.data; + +// TODO: split setting reducers into separate files export default combineReducers({ users: usersSlice.reducer, apiKeys: apiKeysSlice.reducer, + apps: appsSlice.reducer, }); diff --git a/webapp/javascript/services/apps.ts b/webapp/javascript/services/apps.ts new file mode 100644 index 0000000000..c58a86afed --- /dev/null +++ b/webapp/javascript/services/apps.ts @@ -0,0 +1,37 @@ +import { Apps, appsModel } from '@webapp/models/app'; +import { Result } from '@webapp/util/fp'; +import type { ZodError } from 'zod'; +import type { RequestError } from './base'; +import { parseResponse, request } from './base'; + +export interface FetchAppsError { + message?: string; +} + +export async function fetchApps(): Promise< + Result +> { + const response = await request('/api/apps'); + + if (response.isOk) { + return parseResponse(response, appsModel); + } + + return Result.err(response.error); +} + +export async function deleteApp(data: { + name: string; +}): Promise> { + const { name } = data; + const response = await request(`/api/apps`, { + method: 'DELETE', + body: JSON.stringify({ name }), + }); + + if (response.isOk) { + return Result.ok(true); + } + + return Result.err(response.error); +} From cee6097d735d0d907fca5d5bc2f503a6d5aaaecd Mon Sep 17 00:00:00 2001 From: Stas Date: Thu, 18 Aug 2022 20:40:25 +0300 Subject: [PATCH 2/9] fix(webapp): Table component for settings/apps --- .../components/Settings/Apps/AppTableItem.tsx | 43 -------------- .../Settings/Apps/getAppTableRows.tsx | 54 +++++++++++++++++ .../components/Settings/Apps/index.tsx | 58 +++++++++---------- webapp/javascript/models/app.ts | 8 +-- 4 files changed, 82 insertions(+), 81 deletions(-) delete mode 100644 webapp/javascript/components/Settings/Apps/AppTableItem.tsx create mode 100644 webapp/javascript/components/Settings/Apps/getAppTableRows.tsx diff --git a/webapp/javascript/components/Settings/Apps/AppTableItem.tsx b/webapp/javascript/components/Settings/Apps/AppTableItem.tsx deleted file mode 100644 index 4062906826..0000000000 --- a/webapp/javascript/components/Settings/Apps/AppTableItem.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { faTimes } from '@fortawesome/free-solid-svg-icons/faTimes'; -import Button from '@webapp/ui/Button'; -import Icon from '@webapp/ui/Icon'; -import React from 'react'; - -import confirmDelete from '@webapp/components/Modals/ConfirmDelete'; -import { App } from '@webapp/models/app'; -import cx from 'classnames'; -import styles from './AppTableItem.module.css'; - -function DeleteButton(props: ShamefulAny) { - const { onDelete, app } = props; - - const handleDeleteClick = () => { - confirmDelete('this app', () => { - onDelete(app); - }); - }; - - return ( - - ); -} - -function AppTableItem(props: ShamefulAny) { - const { app, onDelete } = props; - const { name } = app as App; - - return ( - - {name} - -
    - -
    - - - ); -} - -export default AppTableItem; diff --git a/webapp/javascript/components/Settings/Apps/getAppTableRows.tsx b/webapp/javascript/components/Settings/Apps/getAppTableRows.tsx new file mode 100644 index 0000000000..5f87b838ff --- /dev/null +++ b/webapp/javascript/components/Settings/Apps/getAppTableRows.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { faTimes } from '@fortawesome/free-solid-svg-icons/faTimes'; + +import Button from '@webapp/ui/Button'; +import Icon from '@webapp/ui/Icon'; +import { App, Apps } from '@webapp/models/app'; +import type { BodyRow } from '@webapp/ui/Table'; + +import confirmDelete from '@webapp/components/Modals/ConfirmDelete'; +import styles from './AppTableItem.module.css'; + +function DeleteButton(props: { onDelete: (app: App) => void; app: App }) { + const { onDelete, app } = props; + + const handleDeleteClick = () => { + confirmDelete('this app', () => { + onDelete(app); + }); + }; + + return ( + + ); +} + +export function getAppTableRows( + displayApps: Apps, + handleDeleteApp: (app: App) => void +): BodyRow[] { + const bodyRows = displayApps.reduce((acc, app) => { + const { name } = app; + + const row = { + cells: [ + { value: name }, + { + value: ( +
    + +
    + ), + align: 'center', + }, + ], + }; + + acc.push(row); + return acc; + }, [] as BodyRow[]); + + return bodyRows; +} diff --git a/webapp/javascript/components/Settings/Apps/index.tsx b/webapp/javascript/components/Settings/Apps/index.tsx index a821dcd5e9..043f9f5d0d 100644 --- a/webapp/javascript/components/Settings/Apps/index.tsx +++ b/webapp/javascript/components/Settings/Apps/index.tsx @@ -8,10 +8,18 @@ import { import { addNotification } from '@webapp/redux/reducers/notifications'; import { type App } from '@webapp/models/app'; import Input from '@webapp/ui/Input'; -import AppTableItem from './AppTableItem'; +import TableUI from '@webapp/ui/Table'; +import cl from 'classnames'; import appsStyles from './Apps.module.css'; -import tableStyles from '../SettingsTable.module.css'; +import tableStyles from '../SettingsTable.module.scss'; +import { getAppTableRows } from './getAppTableRows'; + +const headRow = [ + { name: '', label: '', sortable: 0 }, + { name: '', label: 'Name', sortable: 0 }, + { name: '', label: '', sortable: 0 }, +]; function Apps() { const dispatch = useAppDispatch(); @@ -20,7 +28,7 @@ function Apps() { useEffect(() => { dispatch(reloadApps()); - }, []); + }); const displayApps = (apps && apps.filter( @@ -45,6 +53,18 @@ function Apps() { }); }; + const tableBodyProps = + displayApps.length > 0 + ? { + bodyRows: getAppTableRows(displayApps, handleDeleteApp), + type: 'filled' as const, + } + : { + type: 'not-filled' as const, + value: 'The list is empty', + bodyClassName: appsStyles.appsTableEmptyMessage, + }; + return ( <>

    Apps

    @@ -57,34 +77,10 @@ function Apps() { name="Search app input" /> - - - - - - - - {displayApps.length ? ( - displayApps.map((app) => ( - - )) - ) : ( - - - - )} - -
    Name -
    - The list is empty -
    + ); } diff --git a/webapp/javascript/models/app.ts b/webapp/javascript/models/app.ts index ff3a2fa6c6..66296b4987 100644 --- a/webapp/javascript/models/app.ts +++ b/webapp/javascript/models/app.ts @@ -1,6 +1,4 @@ -import { z, ZodError } from 'zod'; -import { Result } from '@webapp/util/fp'; -import { modelToResult } from './utils'; +import { z } from 'zod'; export const appModel = z.object({ name: z.string(), @@ -10,7 +8,3 @@ export const appsModel = z.array(appModel); export type Apps = z.infer; export type App = z.infer; - -export function parse(a: unknown): Result { - return modelToResult(appsModel, a); -} From 66cc28df6dc973c3295e501e65807ebd888b5195 Mon Sep 17 00:00:00 2001 From: Dmitry Filimonov Date: Fri, 19 Aug 2022 03:09:19 -0700 Subject: [PATCH 3/9] small improvements --- webapp/javascript/components/Settings/Apps/index.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/webapp/javascript/components/Settings/Apps/index.tsx b/webapp/javascript/components/Settings/Apps/index.tsx index 043f9f5d0d..1e70844cce 100644 --- a/webapp/javascript/components/Settings/Apps/index.tsx +++ b/webapp/javascript/components/Settings/Apps/index.tsx @@ -16,7 +16,6 @@ import tableStyles from '../SettingsTable.module.scss'; import { getAppTableRows } from './getAppTableRows'; const headRow = [ - { name: '', label: '', sortable: 0 }, { name: '', label: 'Name', sortable: 0 }, { name: '', label: '', sortable: 0 }, ]; @@ -28,12 +27,12 @@ function Apps() { useEffect(() => { dispatch(reloadApps()); - }); + }, []); + const displayApps = (apps && apps.filter( - (x) => - JSON.stringify(x).toLowerCase().indexOf(search.toLowerCase()) !== -1 + (x) => x.name.toLowerCase().indexOf(search.toLowerCase()) !== -1 )) || []; @@ -47,7 +46,7 @@ function Apps() { addNotification({ type: 'success', title: 'App has been deleted', - message: `App name#${app.name} has been successfully deleted`, + message: `App ${app.name} has been successfully deleted`, }) ); }); From e73eb546d74340e139c2851e4b9cd1e5eb686462 Mon Sep 17 00:00:00 2001 From: Stas Date: Fri, 19 Aug 2022 16:52:43 +0300 Subject: [PATCH 4/9] feat(webapp): spinner for app under deletion --- .../Settings/Apps/getAppTableRows.tsx | 24 +++++++++++++++---- .../components/Settings/Apps/index.tsx | 9 ++++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/webapp/javascript/components/Settings/Apps/getAppTableRows.tsx b/webapp/javascript/components/Settings/Apps/getAppTableRows.tsx index 5f87b838ff..9b3e28e929 100644 --- a/webapp/javascript/components/Settings/Apps/getAppTableRows.tsx +++ b/webapp/javascript/components/Settings/Apps/getAppTableRows.tsx @@ -5,12 +5,19 @@ import Button from '@webapp/ui/Button'; import Icon from '@webapp/ui/Icon'; import { App, Apps } from '@webapp/models/app'; import type { BodyRow } from '@webapp/ui/Table'; - import confirmDelete from '@webapp/components/Modals/ConfirmDelete'; +import LoadingSpinner from '@webapp/ui/LoadingSpinner'; + import styles from './AppTableItem.module.css'; -function DeleteButton(props: { onDelete: (app: App) => void; app: App }) { - const { onDelete, app } = props; +interface IDeleteButtorProps { + onDelete: (app: App) => void; + isLoading: boolean; + app: App; +} + +function DeleteButton(props: IDeleteButtorProps) { + const { onDelete, app, isLoading } = props; const handleDeleteClick = () => { confirmDelete('this app', () => { @@ -18,7 +25,9 @@ function DeleteButton(props: { onDelete: (app: App) => void; app: App }) { }); }; - return ( + return isLoading ? ( + + ) : ( @@ -27,6 +36,7 @@ function DeleteButton(props: { onDelete: (app: App) => void; app: App }) { export function getAppTableRows( displayApps: Apps, + appsInProcessing: string[], handleDeleteApp: (app: App) => void ): BodyRow[] { const bodyRows = displayApps.reduce((acc, app) => { @@ -38,7 +48,11 @@ export function getAppTableRows( { value: (
    - +
    ), align: 'center', diff --git a/webapp/javascript/components/Settings/Apps/index.tsx b/webapp/javascript/components/Settings/Apps/index.tsx index 1e70844cce..16f093c586 100644 --- a/webapp/javascript/components/Settings/Apps/index.tsx +++ b/webapp/javascript/components/Settings/Apps/index.tsx @@ -24,6 +24,7 @@ function Apps() { const dispatch = useAppDispatch(); const apps = useAppSelector(selectApps); const [search, setSearchField] = useState(''); + const [appsInProcessing, setAppsInProcessing] = useState([] as string[]); useEffect(() => { dispatch(reloadApps()); @@ -37,10 +38,12 @@ function Apps() { []; const handleDeleteApp = (app: App) => { + setAppsInProcessing([...appsInProcessing, app.name]); // eslint-disable-next-line @typescript-eslint/no-floating-promises dispatch(deleteApp(app)) .unwrap() .then(() => { + setAppsInProcessing(appsInProcessing.filter((x) => x !== app.name)); // eslint-disable-next-line @typescript-eslint/no-floating-promises dispatch( addNotification({ @@ -55,7 +58,11 @@ function Apps() { const tableBodyProps = displayApps.length > 0 ? { - bodyRows: getAppTableRows(displayApps, handleDeleteApp), + bodyRows: getAppTableRows( + displayApps, + appsInProcessing, + handleDeleteApp + ), type: 'filled' as const, } : { From d128322b28f8efa5571c6af631ccd44b7224d7f0 Mon Sep 17 00:00:00 2001 From: Stas Date: Mon, 22 Aug 2022 12:11:07 +0300 Subject: [PATCH 5/9] fix(webapp): spinner's logic and styles for apps page --- .../Settings/Apps/AppTableItem.module.css | 6 ++++- .../components/Settings/Apps/Apps.module.css | 6 +++++ .../Settings/Apps/getAppTableRows.tsx | 2 +- .../components/Settings/Apps/index.tsx | 23 +++++++++++++++---- webapp/javascript/redux/reducers/settings.ts | 4 +++- webapp/javascript/ui/LoadingSpinner.tsx | 8 +++++-- webapp/javascript/ui/Table.module.scss | 5 ++++ webapp/javascript/ui/Table.tsx | 9 +++++++- 8 files changed, 53 insertions(+), 10 deletions(-) diff --git a/webapp/javascript/components/Settings/Apps/AppTableItem.module.css b/webapp/javascript/components/Settings/Apps/AppTableItem.module.css index 0d7fb267ea..52588664e8 100644 --- a/webapp/javascript/components/Settings/Apps/AppTableItem.module.css +++ b/webapp/javascript/components/Settings/Apps/AppTableItem.module.css @@ -1,5 +1,9 @@ .actions { display: flex; flex-direction: row; - justify-content: center; + justify-content: right; +} + +.loadingIcon { + margin-right: 8px; } diff --git a/webapp/javascript/components/Settings/Apps/Apps.module.css b/webapp/javascript/components/Settings/Apps/Apps.module.css index 1a5ab0edee..6496643173 100644 --- a/webapp/javascript/components/Settings/Apps/Apps.module.css +++ b/webapp/javascript/components/Settings/Apps/Apps.module.css @@ -3,6 +3,12 @@ flex-direction: column; } +.tabNameContrainer { + display: flex; + align-items: center; + gap: 10px; +} + .searchContainer button { padding: 5px 20px; } diff --git a/webapp/javascript/components/Settings/Apps/getAppTableRows.tsx b/webapp/javascript/components/Settings/Apps/getAppTableRows.tsx index 9b3e28e929..98e08cd112 100644 --- a/webapp/javascript/components/Settings/Apps/getAppTableRows.tsx +++ b/webapp/javascript/components/Settings/Apps/getAppTableRows.tsx @@ -26,7 +26,7 @@ function DeleteButton(props: IDeleteButtorProps) { }; return isLoading ? ( - + ) : (