This is an automated email from the ASF dual-hosted git repository. mintsweet pushed a commit to branch refactor-6026 in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git
commit 2857b10278bab3614c4e2748c0cd968282236e1b Author: mintsweet <[email protected]> AuthorDate: Wed Oct 18 19:01:12 2023 +1300 refactor(config-ui): use redux to create connections store --- config-ui/package.json | 3 + config-ui/src/api/connection/types.ts | 2 + config-ui/src/{main.tsx => app/hook.ts} | 10 +- config-ui/src/{main.tsx => app/store.ts} | 14 +- .../{main.tsx => features/connections/index.ts} | 8 +- .../{main.tsx => features/connections/name.tsx} | 16 +- config-ui/src/features/connections/slice.ts | 122 +++++++++++++ config-ui/src/features/connections/utils.ts | 45 +++++ config-ui/src/{main.tsx => features/index.ts} | 7 +- config-ui/src/main.tsx | 9 +- .../components/add-connection-dialog/index.tsx | 11 +- .../pages/blueprint/detail/configuration-panel.tsx | 20 +-- config-ui/src/pages/blueprint/home/index.tsx | 37 ++-- config-ui/src/pages/connection/detail/index.tsx | 19 +- config-ui/src/pages/connection/home/index.tsx | 74 ++++---- config-ui/src/pages/project/home/index.tsx | 25 +-- .../plugins/components/connection-form/index.tsx | 26 +-- .../plugins/components/connection-list/index.tsx | 16 +- .../plugins/components/connection-status/index.tsx | 33 ++-- .../register/webhook/components/create-dialog.tsx | 4 - .../register/webhook/components/delete-dialog.tsx | 4 - .../register/webhook/components/edit-dialog.tsx | 4 - .../src/plugins/register/webhook/connection.tsx | 5 +- config-ui/src/routes/layout/layout.tsx | 198 +++++++++++---------- .../connection/types.ts => types/connection.ts} | 47 ++--- config-ui/src/{main.tsx => types/index.ts} | 7 +- config-ui/yarn.lock | 133 +++++++++++++- 27 files changed, 588 insertions(+), 311 deletions(-) diff --git a/config-ui/package.json b/config-ui/package.json index e0e49a479..fb4cf6220 100644 --- a/config-ui/package.json +++ b/config-ui/package.json @@ -27,6 +27,7 @@ "@blueprintjs/datetime2": "^1.0.10", "@blueprintjs/popover2": "^2.0.10", "@blueprintjs/select": "^5.0.10", + "@reduxjs/toolkit": "^1.9.7", "ahooks": "^3.7.8", "axios": "^0.21.4", "classnames": "^2.3.2", @@ -39,8 +40,10 @@ "react-copy-to-clipboard": "^5.1.0", "react-dom": "17.0.2", "react-is": "^18.2.0", + "react-redux": "^8.1.3", "react-router-dom": "^6.14.1", "react-transition-group": "^4.4.5", + "redux": "^4.2.1", "styled-components": "^5.3.6" }, "devDependencies": { diff --git a/config-ui/src/api/connection/types.ts b/config-ui/src/api/connection/types.ts index a61f954a7..2d83c0c2b 100644 --- a/config-ui/src/api/connection/types.ts +++ b/config-ui/src/api/connection/types.ts @@ -27,6 +27,8 @@ export type Connection = { proxy: string; apiKey?: string; dbUrl?: string; + appId?: string; + secretKey?: string; }; export type ConnectionForm = { diff --git a/config-ui/src/main.tsx b/config-ui/src/app/hook.ts similarity index 72% copy from config-ui/src/main.tsx copy to config-ui/src/app/hook.ts index 682db7df5..c945a1986 100644 --- a/config-ui/src/main.tsx +++ b/config-ui/src/app/hook.ts @@ -16,9 +16,9 @@ * */ -import ReactDOM from 'react-dom'; +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import type { RootState, AppDispatch } from './store'; -import { App } from './App'; -import './index.css'; - -ReactDOM.render(<App />, document.getElementById('root')); +type DispatchFunc = () => AppDispatch; +export const useAppDispatch: DispatchFunc = useDispatch; +export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; diff --git a/config-ui/src/main.tsx b/config-ui/src/app/store.ts similarity index 64% copy from config-ui/src/main.tsx copy to config-ui/src/app/store.ts index 682db7df5..4e27d7f1d 100644 --- a/config-ui/src/main.tsx +++ b/config-ui/src/app/store.ts @@ -16,9 +16,15 @@ * */ -import ReactDOM from 'react-dom'; +import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'; +import ConnectionSlice from '@/features/connections/slice'; -import { App } from './App'; -import './index.css'; +export const store = configureStore({ + reducer: { + connections: ConnectionSlice, + }, +}); -ReactDOM.render(<App />, document.getElementById('root')); +export type AppDispatch = typeof store.dispatch; +export type RootState = ReturnType<typeof store.getState>; +export type AppThunk<ReturnType = void> = ThunkAction<ReturnType, RootState, unknown, Action<string>>; diff --git a/config-ui/src/main.tsx b/config-ui/src/features/connections/index.ts similarity index 84% copy from config-ui/src/main.tsx copy to config-ui/src/features/connections/index.ts index 682db7df5..51ef44992 100644 --- a/config-ui/src/main.tsx +++ b/config-ui/src/features/connections/index.ts @@ -16,9 +16,5 @@ * */ -import ReactDOM from 'react-dom'; - -import { App } from './App'; -import './index.css'; - -ReactDOM.render(<App />, document.getElementById('root')); +export * from './slice'; +export * from './name'; diff --git a/config-ui/src/main.tsx b/config-ui/src/features/connections/name.tsx similarity index 65% copy from config-ui/src/main.tsx copy to config-ui/src/features/connections/name.tsx index 682db7df5..51482969d 100644 --- a/config-ui/src/main.tsx +++ b/config-ui/src/features/connections/name.tsx @@ -16,9 +16,17 @@ * */ -import ReactDOM from 'react-dom'; +import { useAppSelector } from '@/app/hook'; -import { App } from './App'; -import './index.css'; +import { selectConnection } from './slice'; +import { IConnection } from '@/types'; -ReactDOM.render(<App />, document.getElementById('root')); +interface Props { + plugin: string; + connectionId: ID; +} + +export const ConnectionName = ({ plugin, connectionId }: Props) => { + const connection = useAppSelector((state) => selectConnection(state, `${plugin}-${connectionId}`)) as IConnection; + return <span>{connection.name}</span>; +}; diff --git a/config-ui/src/features/connections/slice.ts b/config-ui/src/features/connections/slice.ts new file mode 100644 index 000000000..66b534d1f --- /dev/null +++ b/config-ui/src/features/connections/slice.ts @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import { flatten } from 'lodash'; + +import API from '@/api'; +import type { ConnectionForm } from '@/api/connection/types'; +import { RootState } from '@/app/store'; +import { PluginConfig } from '@/plugins'; +import { IConnection, IConnectionStatus } from '@/types'; + +import { transformConnection } from './utils'; + +const initialState: { + connections: IConnection[]; +} = { + connections: [], +}; + +export const init = createAsyncThunk('connections/init', async () => { + const res = await Promise.all( + PluginConfig.map(async ({ plugin }) => { + const connections = await API.connection.list(plugin); + return connections.map((connection) => transformConnection(plugin, connection)); + }), + ); + return flatten(res); +}); + +export const fetchConnections = createAsyncThunk('connections/fetchConnections', async (plugin: string) => { + const connections = await API.connection.list(plugin); + return { + plugin, + connections: connections.map((connection) => transformConnection(plugin, connection)), + }; +}); + +export const testConnection = createAsyncThunk( + 'connections/testConnection', + async ({ unique, plugin, endpoint, proxy, token, username, password, authMethod, secretKey, appId }: IConnection) => { + const res = await API.connection.test(plugin, { + endpoint, + proxy, + token, + username, + password, + authMethod, + secretKey, + appId, + }); + + return { + unique, + status: res.success ? IConnectionStatus.ONLINE : IConnectionStatus.OFFLINE, + }; + }, +); + +export const addConnection = createAsyncThunk('connections/addConnection', async ({ plugin, ...payload }: any) => { + const connection = await API.connection.create(plugin, payload); + return transformConnection(plugin, connection); +}); + +export const updateConnection = createAsyncThunk('connections/updateConnection', async (payload: ConnectionForm) => {}); + +export const slice = createSlice({ + name: 'connections', + initialState, + reducers: {}, + extraReducers(builder) { + builder + .addCase(init.fulfilled, (state, action) => { + state.connections = action.payload; + }) + .addCase(fetchConnections.fulfilled, (state, action) => { + state.connections = state.connections.concat(action.payload.connections); + }) + .addCase(addConnection.fulfilled, (state, action) => { + state.connections.push(action.payload); + }) + .addCase(testConnection.pending, (state, action) => { + const existingConnection = state.connections.find((cs) => cs.unique === action.meta.arg.unique); + if (existingConnection) { + existingConnection.status = IConnectionStatus.TESTING; + } + }) + .addCase(testConnection.fulfilled, (state, action) => { + const existingConnection = state.connections.find((cs) => cs.unique === action.payload.unique); + if (existingConnection) { + existingConnection.status = action.payload.status; + } + }); + }, +}); + +export const {} = slice.actions; + +export default slice.reducer; + +export const selectAllConnections = (state: RootState) => state.connections.connections; + +export const selectConnections = (state: RootState, plugin: string) => + state.connections.connections.filter((connection) => connection.plugin === plugin); + +export const selectConnection = (state: RootState, unique: string) => + state.connections.connections.find((cs) => cs.unique === unique); diff --git a/config-ui/src/features/connections/utils.ts b/config-ui/src/features/connections/utils.ts new file mode 100644 index 000000000..00161a4ca --- /dev/null +++ b/config-ui/src/features/connections/utils.ts @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import * as T from '@/api/connection/types'; +import type { PluginConfigType } from '@/plugins'; +import { PluginConfig } from '@/plugins'; + +import { IConnection, IConnectionStatus } from '@/types'; + +export const transformConnection = (plugin: string, connection: T.Connection): IConnection => { + const config = PluginConfig.find((p) => p.plugin === plugin) as PluginConfigType; + return { + unique: `${plugin}-${connection.id}`, + plugin, + pluginName: config.name, + id: connection.id, + name: connection.name, + status: IConnectionStatus.IDLE, + icon: config.icon, + isBeta: config.isBeta ?? false, + endpoint: connection.endpoint, + proxy: connection.proxy, + authMethod: connection.authMethod, + token: connection.token, + username: connection.username, + password: connection.password, + appId: connection.appId, + secretKey: connection.secretKey, + }; +}; diff --git a/config-ui/src/main.tsx b/config-ui/src/features/index.ts similarity index 84% copy from config-ui/src/main.tsx copy to config-ui/src/features/index.ts index 682db7df5..4e6e8a4de 100644 --- a/config-ui/src/main.tsx +++ b/config-ui/src/features/index.ts @@ -16,9 +16,4 @@ * */ -import ReactDOM from 'react-dom'; - -import { App } from './App'; -import './index.css'; - -ReactDOM.render(<App />, document.getElementById('root')); +export * from './connections'; diff --git a/config-ui/src/main.tsx b/config-ui/src/main.tsx index 682db7df5..155d47978 100644 --- a/config-ui/src/main.tsx +++ b/config-ui/src/main.tsx @@ -17,8 +17,15 @@ */ import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; import { App } from './App'; +import { store } from './app/store'; import './index.css'; -ReactDOM.render(<App />, document.getElementById('root')); +ReactDOM.render( + <Provider store={store}> + <App /> + </Provider>, + document.getElementById('root'), +); diff --git a/config-ui/src/pages/blueprint/detail/components/add-connection-dialog/index.tsx b/config-ui/src/pages/blueprint/detail/components/add-connection-dialog/index.tsx index e648a0bad..6ef875322 100644 --- a/config-ui/src/pages/blueprint/detail/components/add-connection-dialog/index.tsx +++ b/config-ui/src/pages/blueprint/detail/components/add-connection-dialog/index.tsx @@ -19,10 +19,11 @@ import { useState, useMemo } from 'react'; import { Button, Intent } from '@blueprintjs/core'; +import { useAppSelector } from '@/app/hook'; import { Dialog, FormItem, Selector, Buttons } from '@/components'; -import { useConnections } from '@/hooks'; -import { DataScopeSelect, getPluginScopeId } from '@/plugins'; -import type { ConnectionItemType } from '@/store'; +import { selectAllConnections } from '@/features'; +import { DataScopeSelect } from '@/plugins'; +import { IConnection } from '@/types'; interface Props { disabled: string[]; @@ -32,9 +33,9 @@ interface Props { export const AddConnectionDialog = ({ disabled = [], onCancel, onSubmit }: Props) => { const [step, setStep] = useState(1); - const [selectedConnection, setSelectedConnection] = useState<ConnectionItemType>(); + const [selectedConnection, setSelectedConnection] = useState<IConnection>(); - const { connections } = useConnections({ filterPlugin: ['webhook'] }); + const connections = useAppSelector(selectAllConnections); const disabledItems = useMemo( () => connections.filter((cs) => (disabled.length ? disabled.includes(cs.unique) : false)), diff --git a/config-ui/src/pages/blueprint/detail/configuration-panel.tsx b/config-ui/src/pages/blueprint/detail/configuration-panel.tsx index 4ab0c135d..9890aea04 100644 --- a/config-ui/src/pages/blueprint/detail/configuration-panel.tsx +++ b/config-ui/src/pages/blueprint/detail/configuration-panel.tsx @@ -23,7 +23,7 @@ import { Button, Intent } from '@blueprintjs/core'; import API from '@/api'; import { IconButton, Table, NoData, Buttons } from '@/components'; import { getCron } from '@/config'; -import { useConnections } from '@/hooks'; +import { ConnectionName } from '@/features'; import { getPluginConfig } from '@/plugins'; import { formatTime, operator } from '@/utils'; @@ -52,20 +52,16 @@ export const ConfigurationPanel = ({ from, blueprint, onRefresh, onChangeTab }: setRawPlan(JSON.stringify(blueprint.plan, null, ' ')); }, [blueprint]); - const { onGet } = useConnections(); - const connections = useMemo( () => blueprint.connections .filter((cs) => cs.pluginName !== 'webhook') .map((cs: any) => { - const unique = `${cs.pluginName}-${cs.connectionId}`; const plugin = getPluginConfig(cs.pluginName); - const connection = onGet(unique); return { - unique, + plugin: plugin.plugin, + connectionId: cs.connectionId, icon: plugin.icon, - name: connection?.name, scope: cs.scopes, }; }) @@ -205,10 +201,10 @@ export const ConfigurationPanel = ({ from, blueprint, onRefresh, onChangeTab }: </Buttons> <S.ConnectionList> {connections.map((cs) => ( - <S.ConnectionItem key={cs.unique}> + <S.ConnectionItem key={`${cs.plugin}-${cs.connectionId}`}> <div className="title"> <img src={cs.icon} alt="" /> - <span>{cs.name}</span> + <ConnectionName plugin={cs.plugin} connectionId={cs.connectionId} /> </div> <div className="count"> <span>{cs.scope.length} data scope</span> @@ -217,8 +213,8 @@ export const ConfigurationPanel = ({ from, blueprint, onRefresh, onChangeTab }: <Link to={ from === FromEnum.blueprint - ? `/blueprints/${blueprint.id}/${cs.unique}` - : `/projects/${encodeName(blueprint.projectName)}/${cs.unique}` + ? `/blueprints/${blueprint.id}/${cs.plugin}-${cs.connectionId}` + : `/projects/${encodeName(blueprint.projectName)}/${cs.plugin}-${cs.connectionId}` } > Edit Data Scope and Scope Config @@ -273,7 +269,7 @@ export const ConfigurationPanel = ({ from, blueprint, onRefresh, onChangeTab }: )} {type === 'add-connection' && ( <AddConnectionDialog - disabled={connections.map((cs) => cs.unique)} + disabled={connections.map((cs) => `${cs.plugin}-${cs.connectionId}`)} onCancel={handleCancel} onSubmit={(connection) => handleUpdate({ diff --git a/config-ui/src/pages/blueprint/home/index.tsx b/config-ui/src/pages/blueprint/home/index.tsx index 96946ebee..98c0d7d49 100644 --- a/config-ui/src/pages/blueprint/home/index.tsx +++ b/config-ui/src/pages/blueprint/home/index.tsx @@ -24,10 +24,11 @@ import dayjs from 'dayjs'; import API from '@/api'; import { PageHeader, Table, IconButton, TextTooltip, Dialog } from '@/components'; import { getCronOptions, cronPresets, getCron } from '@/config'; -import { useConnections, useRefreshData } from '@/hooks'; +import { ConnectionName } from '@/features'; +import { useRefreshData } from '@/hooks'; import { formatTime, operator } from '@/utils'; -import { ModeEnum } from '../types'; +import { ModeEnum, BlueprintType } from '../types'; import * as S from './styled'; @@ -41,29 +42,13 @@ export const BlueprintHomePage = () => { const [mode, setMode] = useState(ModeEnum.normal); const [saving, setSaving] = useState(false); - const { onGet } = useConnections(); const { ready, data } = useRefreshData( () => API.blueprint.list({ type: type.toLocaleUpperCase(), page, pageSize }), [version, type, page, pageSize], ); const [options, presets] = useMemo(() => [getCronOptions(), cronPresets.map((preset) => preset.config)], []); - const [dataSource, total] = useMemo( - () => [ - (data?.blueprints ?? []).map((it) => { - const connections = - it.connections - .filter((cs) => cs.pluginName !== 'webhook') - .map((cs) => onGet(`${cs.pluginName}-${cs.connectionId}`) || `${cs.pluginName}-${cs.connectionId}`) ?? []; - return { - ...it, - connections: connections.map((cs) => cs.name), - }; - }), - data?.count ?? 0, - ], - [data], - ); + const [dataSource, total] = useMemo(() => [data?.blueprints ?? [], data?.count ?? 0], [data]); const handleShowDialog = () => setIsOpen(true); const handleHideDialog = () => { @@ -144,11 +129,21 @@ export const BlueprintHomePage = () => { dataIndex: ['mode', 'connections'], key: 'connections', align: 'center', - render: ({ mode, connections }) => { + render: ({ mode, connections }: Pick<BlueprintType, 'mode' | 'connections'>) => { if (mode === ModeEnum.advanced) { return 'Advanced Mode'; } - return connections.join(','); + return ( + <> + {connections.map((it) => ( + <ConnectionName + key={`${it.pluginName}-${it.connectionId}`} + plugin={it.pluginName} + connectionId={it.connectionId} + /> + ))} + </> + ); }, }, { diff --git a/config-ui/src/pages/connection/detail/index.tsx b/config-ui/src/pages/connection/detail/index.tsx index 908b403ac..2668392f3 100644 --- a/config-ui/src/pages/connection/detail/index.tsx +++ b/config-ui/src/pages/connection/detail/index.tsx @@ -16,13 +16,15 @@ * */ -import { useState, useEffect, useMemo } from 'react'; +import { useState, useMemo } from 'react'; import { useParams, useNavigate, Link } from 'react-router-dom'; import { Button, Intent } from '@blueprintjs/core'; import API from '@/api'; +import { useAppSelector } from '@/app/hook'; import { PageHeader, Buttons, Dialog, IconButton, Table, Message, toast } from '@/components'; -import { useTips, useConnections, useRefreshData } from '@/hooks'; +import { selectConnection } from '@/features'; +import { useTips, useRefreshData } from '@/hooks'; import ClearImg from '@/images/icons/clear.svg'; import { ConnectionForm, @@ -33,6 +35,7 @@ import { ScopeConfigForm, ScopeConfigSelect, } from '@/plugins'; +import { IConnection } from '@/types'; import { operator } from '@/utils'; import * as S from './styled'; @@ -68,15 +71,15 @@ const ConnectionDetail = ({ plugin, connectionId }: Props) => { const [conflict, setConflict] = useState<string[]>([]); const [errorMsg, setErrorMsg] = useState(''); + const connection = useAppSelector((state) => selectConnection(state, `${plugin}-${connectionId}`)) as IConnection; const navigate = useNavigate(); - const { onGet, onTest, onRefresh } = useConnections(); const { setTips } = useTips(); const { ready, data } = useRefreshData( () => API.scope.list(plugin, connectionId, { page, pageSize, blueprint: true }), [version, page, pageSize], ); - const { unique, status, name, icon } = onGet(`${plugin}-${connectionId}`) || {}; + const { name, icon } = connection; const pluginConfig = useMemo(() => getPluginConfig(plugin), [plugin]); @@ -94,10 +97,6 @@ const ConnectionDetail = ({ plugin, connectionId }: Props) => { [data], ); - useEffect(() => { - onTest(`${plugin}-${connectionId}`); - }, [plugin, connectionId]); - const handleHideDialog = () => { setType(undefined); }; @@ -129,7 +128,6 @@ const ConnectionDetail = ({ plugin, connectionId }: Props) => { if (res.status === 'success') { toast.success('Delete Connection Successful.'); - onRefresh(plugin); navigate('/connections'); } else if (res.status === 'conflict') { setType('deleteConnectionFailed'); @@ -146,7 +144,6 @@ const ConnectionDetail = ({ plugin, connectionId }: Props) => { }; const handleUpdate = () => { - onRefresh(plugin); handleHideDialog(); }; @@ -250,7 +247,7 @@ const ConnectionDetail = ({ plugin, connectionId }: Props) => { extra={ <S.PageHeaderExtra> <span style={{ marginRight: 4 }}>Status:</span> - <ConnectionStatus status={status} unique={unique} onTest={onTest} /> + <ConnectionStatus connection={connection} /> <Buttons style={{ marginLeft: 8 }}> <Button outlined intent={Intent.PRIMARY} icon="annotation" text="Edit" onClick={handleShowUpdateDialog} /> <Button intent={Intent.DANGER} icon="trash" text="Delete" onClick={handleShowDeleteDialog} /> diff --git a/config-ui/src/pages/connection/home/index.tsx b/config-ui/src/pages/connection/home/index.tsx index d0dcccbcc..5df3a6ad6 100644 --- a/config-ui/src/pages/connection/home/index.tsx +++ b/config-ui/src/pages/connection/home/index.tsx @@ -20,10 +20,10 @@ import { useState, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { Tag, Intent } from '@blueprintjs/core'; +import { useAppSelector } from '@/app/hook'; import { Dialog } from '@/components'; -import { useConnections } from '@/hooks'; -import type { PluginConfigType } from '@/plugins'; -import { PluginConfig, PluginType, ConnectionList, ConnectionForm } from '@/plugins'; +import { selectAllConnections } from '@/features/connections'; +import { PluginConfig, PluginConfigType, ConnectionList, ConnectionForm } from '@/plugins'; import * as S from './styled'; @@ -31,26 +31,22 @@ export const ConnectionHomePage = () => { const [type, setType] = useState<'list' | 'form'>(); const [pluginConfig, setPluginConfig] = useState<PluginConfigType>(); - const { connections, onRefresh } = useConnections(); + const connections = useAppSelector(selectAllConnections); + const navigate = useNavigate(); - const [plugins, webhook] = useMemo( - () => [ - PluginConfig.filter((p) => p.type === PluginType.Connection && p.plugin !== 'webhook').map((p) => ({ + const plugins = useMemo( + () => + PluginConfig.map((p) => ({ ...p, count: connections.filter((cs) => cs.plugin === p.plugin).length, })), - { - ...(PluginConfig.find((p) => p.plugin === 'webhook') as PluginConfigType), - count: connections.filter((cs) => cs.plugin === 'webhook').length, - }, - ], [connections], ); - const handleShowListDialog = (config: PluginConfigType) => { + const handleShowListDialog = (pluginConfig: PluginConfigType) => { setType('list'); - setPluginConfig(config); + setPluginConfig(pluginConfig); }; const handleShowFormDialog = () => { @@ -62,8 +58,7 @@ export const ConnectionHomePage = () => { setPluginConfig(undefined); }; - const handleCreateSuccess = async (plugin: string, id: ID) => { - onRefresh(plugin); + const handleSuccessAfter = async (plugin: string, id: ID) => { navigate(`/connections/${plugin}/${id}`); }; @@ -82,18 +77,20 @@ export const ConnectionHomePage = () => { You can create and manage data connections for the following data sources and use them in your Projects. </h5> <ul> - {plugins.map((p) => ( - <li key={p.plugin} onClick={() => handleShowListDialog(p)}> - <img src={p.icon} alt="" /> - <span className="name">{p.name}</span> - <S.Count>{p.count ? `${p.count} connections` : 'No connection'}</S.Count> - {p.isBeta && ( - <Tag intent={Intent.WARNING} round> - beta - </Tag> - )} - </li> - ))} + {plugins + .filter((p) => p.plugin !== 'webhook') + .map((p) => ( + <li key={p.plugin} onClick={() => handleShowListDialog(p)}> + <img src={p.icon} alt="" /> + <span className="name">{p.name}</span> + <S.Count>{p.count ? `${p.count} connections` : 'No connection'}</S.Count> + {p.isBeta && ( + <Tag intent={Intent.WARNING} round> + beta + </Tag> + )} + </li> + ))} </ul> </div> <div className="block"> @@ -103,11 +100,20 @@ export const ConnectionHomePage = () => { DORA metrics, etc. </h5> <ul> - <li onClick={() => handleShowListDialog(webhook)}> - <img src={webhook.icon} alt="" /> - <span className="name">{webhook.name}</span> - <S.Count>{webhook.count ? `${webhook.count} connections` : 'No connection'}</S.Count> - </li> + {plugins + .filter((p) => p.plugin === 'webhook') + .map((p) => ( + <li key={p.plugin} onClick={() => handleShowListDialog(p)}> + <img src={p.icon} alt="" /> + <span className="name">{p.name}</span> + <S.Count>{p.count ? `${p.count} connections` : 'No connection'}</S.Count> + {p.isBeta && ( + <Tag intent={Intent.WARNING} round> + beta + </Tag> + )} + </li> + ))} </ul> </div> {type === 'list' && pluginConfig && ( @@ -141,7 +147,7 @@ export const ConnectionHomePage = () => { > <ConnectionForm plugin={pluginConfig.plugin} - onSuccess={(id) => handleCreateSuccess(pluginConfig.plugin, id)} + onSuccess={(id) => handleSuccessAfter(pluginConfig.plugin, id)} /> </Dialog> )} diff --git a/config-ui/src/pages/project/home/index.tsx b/config-ui/src/pages/project/home/index.tsx index 54b167d44..ef79ce889 100644 --- a/config-ui/src/pages/project/home/index.tsx +++ b/config-ui/src/pages/project/home/index.tsx @@ -24,7 +24,8 @@ import dayjs from 'dayjs'; import API from '@/api'; import { PageHeader, Table, Dialog, ExternalLink, IconButton, toast } from '@/components'; import { getCron, cronPresets } from '@/config'; -import { useConnections, useRefreshData } from '@/hooks'; +import { ConnectionName } from '@/features'; +import { useRefreshData } from '@/hooks'; import { DOC_URL } from '@/release'; import { formatTime, operator } from '@/utils'; import { PipelineStatus } from '@/routes/pipeline'; @@ -44,7 +45,6 @@ export const ProjectHomePage = () => { const [saving, setSaving] = useState(false); const { ready, data } = useRefreshData(() => API.project.list({ page, pageSize }), [version, page, pageSize]); - const { onGet } = useConnections(); const navigate = useNavigate(); @@ -139,14 +139,19 @@ export const ProjectHomePage = () => { dataIndex: 'connections', key: 'connections', render: (val: BlueprintType['connections']) => - !val || !val.length - ? 'N/A' - : val - .map((it) => { - const cs = onGet(`${it.pluginName}-${it.connectionId}`); - return cs?.name; - }) - .join(', '), + !val || !val.length ? ( + 'N/A' + ) : ( + <> + {val.map((it) => ( + <ConnectionName + key={`${it.pluginName}-${it.connectionId}`} + plugin={it.pluginName} + connectionId={it.connectionId} + /> + ))} + </> + ), }, { title: 'Sync Frequency', diff --git a/config-ui/src/plugins/components/connection-form/index.tsx b/config-ui/src/plugins/components/connection-form/index.tsx index 4ef0a9573..fbfcaa9f5 100644 --- a/config-ui/src/plugins/components/connection-form/index.tsx +++ b/config-ui/src/plugins/components/connection-form/index.tsx @@ -21,9 +21,10 @@ import { Button, Intent } from '@blueprintjs/core'; import { pick } from 'lodash'; import API from '@/api'; -import { ExternalLink, PageLoading, Buttons } from '@/components'; -import { useRefreshData } from '@/hooks'; -import { getPluginConfig } from '@/plugins'; +import { useAppDispatch, useAppSelector } from '@/app/hook'; +import { ExternalLink, Buttons } from '@/components'; +import { selectConnection } from '@/features/connections'; +import { PluginConfig, PluginConfigType } from '@/plugins'; import { operator } from '@/utils'; import { Form } from './fields'; @@ -40,23 +41,18 @@ export const ConnectionForm = ({ plugin, connectionId, onSuccess }: Props) => { const [errors, setErrors] = useState<Record<string, any>>({}); const [operating, setOperating] = useState(false); + const dispatch = useAppDispatch(); + const connection = useAppSelector((state) => selectConnection(state, `${plugin}-${connectionId}`)); + const { name, connection: { docLink, fields, initialValues }, - } = useMemo(() => getPluginConfig(plugin), [plugin]); + } = useMemo(() => PluginConfig.find((p) => p.plugin === plugin) as PluginConfigType, [plugin]); const disabled = useMemo(() => { return Object.values(errors).some((value) => value); }, [errors]); - const { ready, data } = useRefreshData(async () => { - if (!connectionId) { - return {}; - } - - return API.connection.get(plugin, connectionId); - }, [plugin, connectionId]); - const handleTest = async () => { await operator( () => @@ -98,10 +94,6 @@ export const ConnectionForm = ({ plugin, connectionId, onSuccess }: Props) => { } }; - if (connectionId && !ready) { - return <PageLoading />; - } - return ( <S.Wrapper> <S.Tips> @@ -112,7 +104,7 @@ export const ConnectionForm = ({ plugin, connectionId, onSuccess }: Props) => { <Form name={name} fields={fields} - initialValues={{ ...initialValues, ...data }} + initialValues={{ ...initialValues, ...(connection ?? {}) }} values={values} errors={errors} setValues={setValues} diff --git a/config-ui/src/plugins/components/connection-list/index.tsx b/config-ui/src/plugins/components/connection-list/index.tsx index 634dd7dad..d113c1788 100644 --- a/config-ui/src/plugins/components/connection-list/index.tsx +++ b/config-ui/src/plugins/components/connection-list/index.tsx @@ -19,8 +19,9 @@ import { Link } from 'react-router-dom'; import { Button, Intent } from '@blueprintjs/core'; +import { useAppSelector } from '@/app/hook'; import { Table } from '@/components'; -import { useConnections } from '@/hooks'; +import { selectConnections } from '@/features/connections'; import { ConnectionStatus } from '@/plugins'; import { WebHookConnection } from '@/plugins/register/webhook'; @@ -31,16 +32,12 @@ interface Props { } export const ConnectionList = ({ plugin, onCreate }: Props) => { + const connections = useAppSelector((state) => selectConnections(state, plugin)); + if (plugin === 'webhook') { return <WebHookConnection />; } - return <BaseList plugin={plugin} onCreate={onCreate} />; -}; - -const BaseList = ({ plugin, onCreate }: Props) => { - const { connections, onTest } = useConnections(); - return ( <> <Table @@ -53,10 +50,9 @@ const BaseList = ({ plugin, onCreate }: Props) => { }, { title: 'Status', - dataIndex: ['status', 'unique'], key: 'status', width: 150, - render: ({ status, unique }) => <ConnectionStatus status={status} unique={unique} onTest={onTest} />, + render: (_, row) => <ConnectionStatus connection={row} />, }, { title: '', @@ -66,7 +62,7 @@ const BaseList = ({ plugin, onCreate }: Props) => { render: ({ plugin, id }) => <Link to={`/connections/${plugin}/${id}`}>Details</Link>, }, ]} - dataSource={connections.filter((cs) => cs.plugin === plugin)} + dataSource={connections} noData={{ text: 'There is no data connection yet. Please add a new connection.', }} diff --git a/config-ui/src/plugins/components/connection-status/index.tsx b/config-ui/src/plugins/components/connection-status/index.tsx index 3475194d4..9e8c83e5f 100644 --- a/config-ui/src/plugins/components/connection-status/index.tsx +++ b/config-ui/src/plugins/components/connection-status/index.tsx @@ -18,8 +18,10 @@ import styled from 'styled-components'; +import { useAppDispatch } from '@/app/hook'; import { IconButton } from '@/components'; -import { ConnectionStatusEnum } from '@/store'; +import { testConnection } from '@/features/connections'; +import { IConnection, IConnectionStatus, IPlugin } from '@/types'; const Wrapper = styled.div` display: inline-flex; @@ -35,29 +37,28 @@ const Wrapper = styled.div` `; const STATUS_MAP = { - [`${ConnectionStatusEnum.NULL}`]: 'Test', - [`${ConnectionStatusEnum.TESTING}`]: 'Testing', - [`${ConnectionStatusEnum.ONLINE}`]: 'Connected', - [`${ConnectionStatusEnum.OFFLINE}`]: 'Disconnected', + [`${IConnectionStatus.IDLE}`]: 'Test', + [`${IConnectionStatus.TESTING}`]: 'Testing', + [`${IConnectionStatus.ONLINE}`]: 'Connected', + [`${IConnectionStatus.OFFLINE}`]: 'Disconnected', }; interface Props { - status: ConnectionStatusEnum; - unique: string; - onTest: (unique: string) => void; + connection: IConnection; } -export const ConnectionStatus = ({ status, unique, onTest }: Props) => { +export const ConnectionStatus = ({ connection }: Props) => { + const { status } = connection; + + const dispatch = useAppDispatch(); + + const handleTest = () => dispatch(testConnection(connection)); + return ( <Wrapper> <span className={status}>{STATUS_MAP[status]}</span> - {status !== ConnectionStatusEnum.ONLINE && ( - <IconButton - loading={status === ConnectionStatusEnum.TESTING} - icon="repeat" - tooltip="Retry" - onClick={() => onTest(unique)} - /> + {status !== IConnectionStatus.ONLINE && ( + <IconButton loading={status === IConnectionStatus.TESTING} icon="repeat" tooltip="Retry" onClick={handleTest} /> )} </Wrapper> ); diff --git a/config-ui/src/plugins/register/webhook/components/create-dialog.tsx b/config-ui/src/plugins/register/webhook/components/create-dialog.tsx index 9d4406bba..36b7aeda8 100644 --- a/config-ui/src/plugins/register/webhook/components/create-dialog.tsx +++ b/config-ui/src/plugins/register/webhook/components/create-dialog.tsx @@ -21,7 +21,6 @@ import { InputGroup, Icon } from '@blueprintjs/core'; import API from '@/api'; import { Dialog, FormItem, CopyText, ExternalLink } from '@/components'; -import { useConnections } from '@/hooks'; import { operator } from '@/utils'; import * as S from '../styled'; @@ -44,8 +43,6 @@ export const CreateDialog = ({ isOpen, onCancel, onSubmitAfter }: Props) => { apiKey: '', }); - const { onRefresh } = useConnections(); - const prefix = useMemo(() => `${window.location.origin}/api`, []); const handleSubmit = async () => { @@ -88,7 +85,6 @@ export const CreateDialog = ({ isOpen, onCancel, onSubmitAfter }: Props) => { }'`, apiKey: res.apiKey, }); - onRefresh('webhook'); onSubmitAfter?.(res.id); } }; diff --git a/config-ui/src/plugins/register/webhook/components/delete-dialog.tsx b/config-ui/src/plugins/register/webhook/components/delete-dialog.tsx index 2479f1754..806a26d5e 100644 --- a/config-ui/src/plugins/register/webhook/components/delete-dialog.tsx +++ b/config-ui/src/plugins/register/webhook/components/delete-dialog.tsx @@ -20,7 +20,6 @@ import { useState } from 'react'; import API from '@/api'; import { Dialog, Message } from '@/components'; -import { useConnections } from '@/hooks'; import { operator } from '@/utils'; interface Props { @@ -32,15 +31,12 @@ interface Props { export const DeleteDialog = ({ initialId, onCancel, onSubmitAfter }: Props) => { const [operating, setOperating] = useState(false); - const { onRefresh } = useConnections(); - const handleSubmit = async () => { const [success] = await operator(() => API.plugin.webhook.remove(initialId), { setOperating, }); if (success) { - onRefresh('webhook'); onSubmitAfter?.(initialId); onCancel(); } diff --git a/config-ui/src/plugins/register/webhook/components/edit-dialog.tsx b/config-ui/src/plugins/register/webhook/components/edit-dialog.tsx index a9fb55cc7..f656cdf7c 100644 --- a/config-ui/src/plugins/register/webhook/components/edit-dialog.tsx +++ b/config-ui/src/plugins/register/webhook/components/edit-dialog.tsx @@ -21,7 +21,6 @@ import { InputGroup } from '@blueprintjs/core'; import API from '@/api'; import { Dialog, FormItem } from '@/components'; -import { useConnections } from '@/hooks'; import { operator } from '@/utils'; interface Props { @@ -33,8 +32,6 @@ export const EditDialog = ({ initialId, onCancel }: Props) => { const [name, setName] = useState(''); const [operating, setOperating] = useState(false); - const { onRefresh } = useConnections({ plugin: 'webhook' }); - useEffect(() => { (async () => { const res = await API.plugin.webhook.get(initialId); @@ -48,7 +45,6 @@ export const EditDialog = ({ initialId, onCancel }: Props) => { }); if (success) { - onRefresh('webhook'); onCancel(); } }; diff --git a/config-ui/src/plugins/register/webhook/connection.tsx b/config-ui/src/plugins/register/webhook/connection.tsx index ffde52da7..07e68224c 100644 --- a/config-ui/src/plugins/register/webhook/connection.tsx +++ b/config-ui/src/plugins/register/webhook/connection.tsx @@ -19,8 +19,9 @@ import { useState } from 'react'; import { Button, Intent } from '@blueprintjs/core'; +import { useAppSelector } from '@/app/hook'; import { Buttons, Table, ColumnType, ExternalLink, IconButton } from '@/components'; -import { useConnections } from '@/hooks'; +import { selectConnections } from '@/features/connections'; import { DOC_URL } from '@/release'; import { CreateDialog, ViewDialog, EditDialog, DeleteDialog } from './components'; @@ -40,7 +41,7 @@ export const WebHookConnection = ({ filterIds, onCreateAfter, onDeleteAfter }: P const [type, setType] = useState<Type>(); const [currentID, setCurrentID] = useState<ID>(); - const { connections } = useConnections({ plugin: 'webhook' }); + const connections = useAppSelector((state) => selectConnections(state, 'webhook')); const handleHideDialog = () => { setType(undefined); diff --git a/config-ui/src/routes/layout/layout.tsx b/config-ui/src/routes/layout/layout.tsx index 9ad858571..cc5b4b260 100644 --- a/config-ui/src/routes/layout/layout.tsx +++ b/config-ui/src/routes/layout/layout.tsx @@ -16,14 +16,16 @@ * */ -import { useRef } from 'react'; +import { useEffect, useRef } from 'react'; import { useLoaderData, Outlet, useNavigate, useLocation } from 'react-router-dom'; import { CSSTransition } from 'react-transition-group'; import { Menu, MenuItem, Navbar, Alignment } from '@blueprintjs/core'; +import { useAppDispatch } from '@/app/hook'; import { Logo, ExternalLink, IconButton } from '@/components'; +import { init } from '@/features'; import { DOC_URL } from '@/release'; -import { TipsContextProvider, TipsContextConsumer, ConnectionContextProvider } from '@/store'; +import { TipsContextProvider, TipsContextConsumer } from '@/store'; import DashboardIcon from '@/images/icons/dashboard.svg'; import FileIcon from '@/images/icons/file.svg'; @@ -39,6 +41,12 @@ import './tips-transition.css'; export const Layout = () => { const { version } = useLoaderData() as Awaited<ReturnType<typeof loader>>; + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(init()); + }, []); + const navigate = useNavigate(); const { pathname } = useLocation(); @@ -65,101 +73,99 @@ export const Layout = () => { <TipsContextProvider> <TipsContextConsumer> {({ tips, setTips }) => ( - <ConnectionContextProvider> - <S.Wrapper> - <S.Sider> - <Logo /> - <Menu className="menu"> - {menu.map((it) => { - const paths = [it.path, ...(it.children ?? []).map((cit) => cit.path)]; - const active = !!paths.find((path) => pathname.includes(path)); - return ( - <MenuItem - key={it.key} - className="menu-item" - text={it.title} - icon={it.icon} - active={active} - onClick={() => handlePushPath(it)} - > - {it.children?.map((cit) => ( - <MenuItem - key={cit.key} - className="sub-menu-item" - text={ - <S.SiderMenuItem> - <span>{cit.title}</span> - </S.SiderMenuItem> - } - icon={cit.icon} - active={pathname.includes(cit.path)} - disabled={cit.disabled} - onClick={() => handlePushPath(cit)} - /> - ))} - </MenuItem> - ); - })} - </Menu> - <div className="copyright"> - <div>Apache 2.0 License</div> - <div className="version">{version}</div> - </div> - </S.Sider> - <S.Main> - <S.Header> - <Navbar.Group align={Alignment.RIGHT}> - <S.DashboardIcon> - <ExternalLink link={getGrafanaUrl()}> - <img src={DashboardIcon} alt="dashboards" /> - <span>Dashboards</span> - </ExternalLink> - </S.DashboardIcon> - <Navbar.Divider /> - <ExternalLink link={DOC_URL.TUTORIAL}> - <img src={FileIcon} alt="documents" /> - <span>Docs</span> - </ExternalLink> - <Navbar.Divider /> - <ExternalLink link="/api/swagger/index.html"> - <img src={APIIcon} alt="api" /> - <span>API</span> - </ExternalLink> - <Navbar.Divider /> - <a - href="https://github.com/apache/incubator-devlake" - rel="noreferrer" - target="_blank" - className="navIconLink" - > - <img src={GitHubIcon} alt="github" /> - <span>GitHub</span> - </a> - <Navbar.Divider /> - <a - href="https://join.slack.com/t/devlake-io/shared_invite/zt-17b6vuvps-x98pqseoUagM7EAmKC82xQ" - rel="noreferrer" - target="_blank" + <S.Wrapper> + <S.Sider> + <Logo /> + <Menu className="menu"> + {menu.map((it) => { + const paths = [it.path, ...(it.children ?? []).map((cit) => cit.path)]; + const active = !!paths.find((path) => pathname.includes(path)); + return ( + <MenuItem + key={it.key} + className="menu-item" + text={it.title} + icon={it.icon} + active={active} + onClick={() => handlePushPath(it)} > - <img src={SlackIcon} alt="slack" /> - <span>Slack</span> - </a> - </Navbar.Group> - </S.Header> - <S.Inner> - <S.Content> - <Outlet /> - </S.Content> - </S.Inner> - <CSSTransition in={!!tips} unmountOnExit timeout={300} nodeRef={tipsRef} classNames="tips"> - <S.Tips ref={tipsRef}> - <div className="content">{tips}</div> - <IconButton style={{ color: '#fff' }} icon="cross" tooltip="Close" onClick={() => setTips('')} /> - </S.Tips> - </CSSTransition> - </S.Main> - </S.Wrapper> - </ConnectionContextProvider> + {it.children?.map((cit) => ( + <MenuItem + key={cit.key} + className="sub-menu-item" + text={ + <S.SiderMenuItem> + <span>{cit.title}</span> + </S.SiderMenuItem> + } + icon={cit.icon} + active={pathname.includes(cit.path)} + disabled={cit.disabled} + onClick={() => handlePushPath(cit)} + /> + ))} + </MenuItem> + ); + })} + </Menu> + <div className="copyright"> + <div>Apache 2.0 License</div> + <div className="version">{version}</div> + </div> + </S.Sider> + <S.Main> + <S.Header> + <Navbar.Group align={Alignment.RIGHT}> + <S.DashboardIcon> + <ExternalLink link={getGrafanaUrl()}> + <img src={DashboardIcon} alt="dashboards" /> + <span>Dashboards</span> + </ExternalLink> + </S.DashboardIcon> + <Navbar.Divider /> + <ExternalLink link={DOC_URL.TUTORIAL}> + <img src={FileIcon} alt="documents" /> + <span>Docs</span> + </ExternalLink> + <Navbar.Divider /> + <ExternalLink link="/api/swagger/index.html"> + <img src={APIIcon} alt="api" /> + <span>API</span> + </ExternalLink> + <Navbar.Divider /> + <a + href="https://github.com/apache/incubator-devlake" + rel="noreferrer" + target="_blank" + className="navIconLink" + > + <img src={GitHubIcon} alt="github" /> + <span>GitHub</span> + </a> + <Navbar.Divider /> + <a + href="https://join.slack.com/t/devlake-io/shared_invite/zt-17b6vuvps-x98pqseoUagM7EAmKC82xQ" + rel="noreferrer" + target="_blank" + > + <img src={SlackIcon} alt="slack" /> + <span>Slack</span> + </a> + </Navbar.Group> + </S.Header> + <S.Inner> + <S.Content> + <Outlet /> + </S.Content> + </S.Inner> + <CSSTransition in={!!tips} unmountOnExit timeout={300} nodeRef={tipsRef} classNames="tips"> + <S.Tips ref={tipsRef}> + <div className="content">{tips}</div> + <IconButton style={{ color: '#fff' }} icon="cross" tooltip="Close" onClick={() => setTips('')} /> + </S.Tips> + </CSSTransition> + </S.Main> + </S.Wrapper> )} </TipsContextConsumer> </TipsContextProvider> diff --git a/config-ui/src/api/connection/types.ts b/config-ui/src/types/connection.ts similarity index 65% copy from config-ui/src/api/connection/types.ts copy to config-ui/src/types/connection.ts index a61f954a7..6225fd63b 100644 --- a/config-ui/src/api/connection/types.ts +++ b/config-ui/src/types/connection.ts @@ -16,43 +16,28 @@ * */ -export type Connection = { +export enum IConnectionStatus { + IDLE = 'idle', + TESTING = 'testing', + ONLINE = 'online', + OFFLINE = 'offline', +} + +export interface IConnection { + unique: string; + plugin: string; + pluginName: string; id: ID; name: string; + status: IConnectionStatus; + icon: string; + isBeta: boolean; endpoint: string; - authMethod?: string; - token?: string; - username?: string; - password?: string; proxy: string; - apiKey?: string; - dbUrl?: string; -}; - -export type ConnectionForm = { - name: string; - endpoint?: string; authMethod?: string; + token?: string; username?: string; password?: string; - token?: string; appId?: string; secretKey?: string; - enableGraphql?: boolean; - proxy: string; - rateLimitPerHour?: number; - dbUrl?: string; -}; - -export type ConnectionTest = { - message: string; - success: boolean; - login?: string; - installations?: Array<{ - id: number; - account: { - login: string; - }; - }>; - warning?: string; -}; +} diff --git a/config-ui/src/main.tsx b/config-ui/src/types/index.ts similarity index 84% copy from config-ui/src/main.tsx copy to config-ui/src/types/index.ts index 682db7df5..fcc98feb8 100644 --- a/config-ui/src/main.tsx +++ b/config-ui/src/types/index.ts @@ -16,9 +16,4 @@ * */ -import ReactDOM from 'react-dom'; - -import { App } from './App'; -import './index.css'; - -ReactDOM.render(<App />, document.getElementById('root')); +export * from './connection'; diff --git a/config-ui/yarn.lock b/config-ui/yarn.lock index 587e396f0..c6fbdf122 100644 --- a/config-ui/yarn.lock +++ b/config-ui/yarn.lock @@ -1452,6 +1452,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.9.2": + version: 7.23.2 + resolution: "@babel/runtime@npm:7.23.2" + dependencies: + regenerator-runtime: ^0.14.0 + checksum: 6c4df4839ec75ca10175f636d6362f91df8a3137f86b38f6cd3a4c90668a0fe8e9281d320958f4fbd43b394988958585a17c3aab2a4ea6bf7316b22916a371fb + languageName: node + linkType: hard + "@babel/runtime@npm:^7.21.0": version: 7.22.5 resolution: "@babel/runtime@npm:7.22.5" @@ -2007,6 +2016,26 @@ __metadata: languageName: node linkType: hard +"@reduxjs/toolkit@npm:^1.9.7": + version: 1.9.7 + resolution: "@reduxjs/toolkit@npm:1.9.7" + dependencies: + immer: ^9.0.21 + redux: ^4.2.1 + redux-thunk: ^2.4.2 + reselect: ^4.1.8 + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.0.2 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + checksum: ac25dec73a5d2df9fc7fbe98c14ccc73919e5ee1d6f251db0d2ec8f90273f92ef39c26716704bf56b5a40189f72d94b4526dc3a8c7ac3986f5daf44442bcc364 + languageName: node + linkType: hard + "@remix-run/router@npm:1.7.1": version: 1.7.1 resolution: "@remix-run/router@npm:1.7.1" @@ -2052,6 +2081,16 @@ __metadata: languageName: node linkType: hard +"@types/hoist-non-react-statics@npm:^3.3.1": + version: 3.3.3 + resolution: "@types/hoist-non-react-statics@npm:3.3.3" + dependencies: + "@types/react": "*" + hoist-non-react-statics: ^3.3.0 + checksum: 107ac20ab36acdc83fb6bfca901e6f4f11307a0a307099c31ecf2a9875f8abffd731a2e1ee793162307e8aaee48fe9fd8d4e034fce88d5da480bc4178a3fc8d7 + languageName: node + linkType: hard + "@types/js-cookie@npm:^2.x.x": version: 2.2.7 resolution: "@types/js-cookie@npm:2.2.7" @@ -2185,6 +2224,13 @@ __metadata: languageName: node linkType: hard +"@types/use-sync-external-store@npm:^0.0.3": + version: 0.0.3 + resolution: "@types/use-sync-external-store@npm:0.0.3" + checksum: 161ddb8eec5dbe7279ac971531217e9af6b99f7783213566d2b502e2e2378ea19cf5e5ea4595039d730aa79d3d35c6567d48599f69773a02ffcff1776ec2a44e + languageName: node + linkType: hard + "@typescript-eslint/eslint-plugin@npm:^5.5.0": version: 5.55.0 resolution: "@typescript-eslint/eslint-plugin@npm:5.55.0" @@ -3016,6 +3062,7 @@ __metadata: "@blueprintjs/datetime2": ^1.0.10 "@blueprintjs/popover2": ^2.0.10 "@blueprintjs/select": ^5.0.10 + "@reduxjs/toolkit": ^1.9.7 "@types/file-saver": ^2.0.5 "@types/node": ^18.15.1 "@types/react": ^18.0.24 @@ -3045,8 +3092,10 @@ __metadata: react-copy-to-clipboard: ^5.1.0 react-dom: 17.0.2 react-is: ^18.2.0 + react-redux: ^8.1.3 react-router-dom: ^6.14.1 react-transition-group: ^4.4.5 + redux: ^4.2.1 styled-components: ^5.3.6 typescript: ^4.9.4 vite: ^4.1.4 @@ -4372,7 +4421,7 @@ __metadata: languageName: node linkType: hard -"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.3.0": +"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" dependencies: @@ -4450,6 +4499,13 @@ __metadata: languageName: node linkType: hard +"immer@npm:^9.0.21": + version: 9.0.21 + resolution: "immer@npm:9.0.21" + checksum: 70e3c274165995352f6936695f0ef4723c52c92c92dd0e9afdfe008175af39fa28e76aafb3a2ca9d57d1fb8f796efc4dd1e1cc36f18d33fa5b74f3dfb0375432 + languageName: node + linkType: hard + "import-fresh@npm:^3.0.0, import-fresh@npm:^3.2.1": version: 3.3.0 resolution: "import-fresh@npm:3.3.0" @@ -5824,7 +5880,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^18.2.0": +"react-is@npm:^18.0.0, react-is@npm:^18.2.0": version: 18.2.0 resolution: "react-is@npm:18.2.0" checksum: e72d0ba81b5922759e4aff17e0252bd29988f9642ed817f56b25a3e217e13eea8a7f2322af99a06edb779da12d5d636e9fda473d620df9a3da0df2a74141d53e @@ -5845,6 +5901,38 @@ __metadata: languageName: node linkType: hard +"react-redux@npm:^8.1.3": + version: 8.1.3 + resolution: "react-redux@npm:8.1.3" + dependencies: + "@babel/runtime": ^7.12.1 + "@types/hoist-non-react-statics": ^3.3.1 + "@types/use-sync-external-store": ^0.0.3 + hoist-non-react-statics: ^3.3.2 + react-is: ^18.0.0 + use-sync-external-store: ^1.0.0 + peerDependencies: + "@types/react": ^16.8 || ^17.0 || ^18.0 + "@types/react-dom": ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + react-native: ">=0.59" + redux: ^4 || ^5.0.0-beta.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + react-dom: + optional: true + react-native: + optional: true + redux: + optional: true + checksum: 192ea6f6053148ec80a4148ec607bc259403b937e515f616a1104ca5ab357e97e98b8245ed505a17afee67a72341d4a559eaca9607968b4a422aa9b44ba7eb89 + languageName: node + linkType: hard + "react-refresh@npm:^0.14.0": version: 0.14.0 resolution: "react-refresh@npm:0.14.0" @@ -5912,6 +6000,24 @@ __metadata: languageName: node linkType: hard +"redux-thunk@npm:^2.4.2": + version: 2.4.2 + resolution: "redux-thunk@npm:2.4.2" + peerDependencies: + redux: ^4 + checksum: c7f757f6c383b8ec26152c113e20087818d18ed3edf438aaad43539e9a6b77b427ade755c9595c4a163b6ad3063adf3497e5fe6a36c68884eb1f1cfb6f049a5c + languageName: node + linkType: hard + +"redux@npm:^4.2.1": + version: 4.2.1 + resolution: "redux@npm:4.2.1" + dependencies: + "@babel/runtime": ^7.9.2 + checksum: f63b9060c3a1d930ae775252bb6e579b42415aee7a23c4114e21a0b4ba7ec12f0ec76936c00f546893f06e139819f0e2855e0d55ebfce34ca9c026241a6950dd + languageName: node + linkType: hard + "regenerate-unicode-properties@npm:^10.1.0": version: 10.1.0 resolution: "regenerate-unicode-properties@npm:10.1.0" @@ -5935,6 +6041,13 @@ __metadata: languageName: node linkType: hard +"regenerator-runtime@npm:^0.14.0": + version: 0.14.0 + resolution: "regenerator-runtime@npm:0.14.0" + checksum: 1c977ad82a82a4412e4f639d65d22be376d3ebdd30da2c003eeafdaaacd03fc00c2320f18120007ee700900979284fc78a9f00da7fb593f6e6eeebc673fba9a3 + languageName: node + linkType: hard + "regenerator-transform@npm:^0.15.1": version: 0.15.1 resolution: "regenerator-transform@npm:0.15.1" @@ -5980,6 +6093,13 @@ __metadata: languageName: node linkType: hard +"reselect@npm:^4.1.8": + version: 4.1.8 + resolution: "reselect@npm:4.1.8" + checksum: a4ac87cedab198769a29be92bc221c32da76cfdad6911eda67b4d3e7136dca86208c3b210e31632eae31ebd2cded18596f0dd230d3ccc9e978df22f233b5583e + languageName: node + linkType: hard + "resize-observer-polyfill@npm:^1.5.1": version: 1.5.1 resolution: "resize-observer-polyfill@npm:1.5.1" @@ -6809,6 +6929,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.0.0": + version: 1.2.0 + resolution: "use-sync-external-store@npm:1.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 5c639e0f8da3521d605f59ce5be9e094ca772bd44a4ce7322b055a6f58eeed8dda3c94cabd90c7a41fb6fa852210092008afe48f7038792fd47501f33299116a + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2"
