This is an automated email from the ASF dual-hosted git repository.
klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git
The following commit(s) were added to refs/heads/main by this push:
new b2e83943e refactor(config-ui): use redux to reorganize connection data
(#6283)
b2e83943e is described below
commit b2e83943ef7585b66c2ca2672b6cffa0c055afc4
Author: 青湛 <[email protected]>
AuthorDate: Thu Oct 19 21:10:33 2023 +1300
refactor(config-ui): use redux to reorganize connection data (#6283)
* refactor(config-ui): remove connections context
* refactor(config-ui): use redux to create connections store
* refactor(config-ui): remove field connection type from connection
* fix(config-ui): lint error
---
config-ui/package.json | 3 +
config-ui/src/api/connection/types.ts | 2 +
.../connection-fields/styled.ts => app/hook.ts} | 18 +-
.../register/webhook/config.ts => app/store.ts} | 26 ++-
.../src/{store => features}/connections/index.ts | 4 +-
.../config.ts => features/connections/name.tsx} | 26 ++-
config-ui/src/features/connections/slice.ts | 122 +++++++++++++
config-ui/src/features/connections/utils.ts | 45 +++++
.../connection-fields => features}/index.ts | 3 +-
config-ui/src/hooks/index.ts | 1 -
config-ui/src/hooks/use-connections.ts | 47 -----
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 | 28 ++-
.../plugins/components/connection-list/index.tsx | 16 +-
.../plugins/components/connection-status/index.tsx | 33 ++--
config-ui/src/plugins/config.ts | 2 -
config-ui/src/plugins/register/azure/config.tsx | 2 -
config-ui/src/plugins/register/bamboo/config.ts | 2 -
.../src/plugins/register/bitbucket/config.tsx | 2 -
config-ui/src/plugins/register/github/config.tsx | 2 -
config-ui/src/plugins/register/gitlab/config.tsx | 2 -
config-ui/src/plugins/register/jenkins/config.ts | 2 -
config-ui/src/plugins/register/jira/config.tsx | 2 -
.../src/plugins/register/pagerduty/config.tsx | 2 -
config-ui/src/plugins/register/sonarqube/config.ts | 2 -
config-ui/src/plugins/register/tapd/config.tsx | 2 -
.../plugins/register/teambition/assets/icon.svg | 5 -
.../src/plugins/register/teambition/config.tsx | 97 ----------
.../teambition/connection-fields/tenant-id.tsx | 72 --------
.../teambition/connection-fields/tenant-type.tsx | 76 --------
.../register/webhook/components/create-dialog.tsx | 4 -
.../register/webhook/components/delete-dialog.tsx | 4 -
.../register/webhook/components/edit-dialog.tsx | 4 -
config-ui/src/plugins/register/webhook/config.ts | 2 -
.../src/plugins/register/webhook/connection.tsx | 5 +-
config-ui/src/plugins/register/zentao/config.tsx | 2 -
config-ui/src/plugins/types.ts | 6 -
config-ui/src/plugins/utils.ts | 3 +-
config-ui/src/routes/layout/layout.tsx | 198 +++++++++++----------
config-ui/src/store/connections/context.tsx | 182 -------------------
config-ui/src/store/index.ts | 1 -
.../connections/types.ts => types/connection.ts} | 16 +-
.../register/teambition => types}/index.ts | 2 +-
config-ui/yarn.lock | 133 +++++++++++++-
50 files changed, 580 insertions(+), 823 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/plugins/register/teambition/connection-fields/styled.ts
b/config-ui/src/app/hook.ts
similarity index 72%
rename from
config-ui/src/plugins/register/teambition/connection-fields/styled.ts
rename to config-ui/src/app/hook.ts
index 11e47a3d0..c945a1986 100644
--- a/config-ui/src/plugins/register/teambition/connection-fields/styled.ts
+++ b/config-ui/src/app/hook.ts
@@ -16,17 +16,9 @@
*
*/
-import styled from 'styled-components';
+import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
+import type { RootState, AppDispatch } from './store';
-export const Label = styled.label`
- font-size: 16px;
- font-weight: 600;
-`;
-
-export const LabelInfo = styled.i`
- color: #ff8b8b;
-`;
-
-export const LabelDescription = styled.p`
- margin: 0;
-`;
+type DispatchFunc = () => AppDispatch;
+export const useAppDispatch: DispatchFunc = useDispatch;
+export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
diff --git a/config-ui/src/plugins/register/webhook/config.ts
b/config-ui/src/app/store.ts
similarity index 65%
copy from config-ui/src/plugins/register/webhook/config.ts
copy to config-ui/src/app/store.ts
index 25088545d..4e27d7f1d 100644
--- a/config-ui/src/plugins/register/webhook/config.ts
+++ b/config-ui/src/app/store.ts
@@ -16,21 +16,15 @@
*
*/
-import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
+import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
+import ConnectionSlice from '@/features/connections/slice';
-import Icon from './assets/icon.svg';
-
-export const WebhookConfig: PluginConfigType = {
- plugin: 'webhook',
- name: 'Webhook',
- type: PluginType.Connection,
- icon: Icon,
- sort: 100,
- connection: {
- docLink: '',
- fields: [],
- initialValues: {},
+export const store = configureStore({
+ reducer: {
+ connections: ConnectionSlice,
},
- dataScope: {},
-};
+});
+
+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/store/connections/index.ts
b/config-ui/src/features/connections/index.ts
similarity index 93%
rename from config-ui/src/store/connections/index.ts
rename to config-ui/src/features/connections/index.ts
index 34ae5c1d0..51ef44992 100644
--- a/config-ui/src/store/connections/index.ts
+++ b/config-ui/src/features/connections/index.ts
@@ -16,5 +16,5 @@
*
*/
-export * from './types';
-export * from './context';
+export * from './slice';
+export * from './name';
diff --git a/config-ui/src/plugins/register/webhook/config.ts
b/config-ui/src/features/connections/name.tsx
similarity index 66%
copy from config-ui/src/plugins/register/webhook/config.ts
copy to config-ui/src/features/connections/name.tsx
index 25088545d..51482969d 100644
--- a/config-ui/src/plugins/register/webhook/config.ts
+++ b/config-ui/src/features/connections/name.tsx
@@ -16,21 +16,17 @@
*
*/
-import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
+import { useAppSelector } from '@/app/hook';
-import Icon from './assets/icon.svg';
+import { selectConnection } from './slice';
+import { IConnection } from '@/types';
-export const WebhookConfig: PluginConfigType = {
- plugin: 'webhook',
- name: 'Webhook',
- type: PluginType.Connection,
- icon: Icon,
- sort: 100,
- connection: {
- docLink: '',
- fields: [],
- initialValues: {},
- },
- dataScope: {},
+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/plugins/register/teambition/connection-fields/index.ts
b/config-ui/src/features/index.ts
similarity index 93%
rename from config-ui/src/plugins/register/teambition/connection-fields/index.ts
rename to config-ui/src/features/index.ts
index b2481eb94..4e6e8a4de 100644
--- a/config-ui/src/plugins/register/teambition/connection-fields/index.ts
+++ b/config-ui/src/features/index.ts
@@ -16,5 +16,4 @@
*
*/
-export * from './tenant-id';
-export * from './tenant-type';
+export * from './connections';
diff --git a/config-ui/src/hooks/index.ts b/config-ui/src/hooks/index.ts
index cba922842..7f38b5d6f 100644
--- a/config-ui/src/hooks/index.ts
+++ b/config-ui/src/hooks/index.ts
@@ -17,7 +17,6 @@
*/
export * from './use-auto-refresh';
-export * from './use-connections';
export * from './use-refresh-data';
export * from './use-tips';
export * from './user-proxy-prefix';
diff --git a/config-ui/src/hooks/use-connections.ts
b/config-ui/src/hooks/use-connections.ts
deleted file mode 100644
index f129c13ee..000000000
--- a/config-ui/src/hooks/use-connections.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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 { useContext } from 'react';
-
-import { ConnectionContext } from '@/store';
-
-type UseConnectionsProps = {
- unique?: string;
- plugin?: string;
- filter?: string[];
- filterBeta?: boolean;
- filterPlugin?: string[];
-};
-
-export const useConnections = (props?: UseConnectionsProps) => {
- const { unique, plugin, filter, filterBeta, filterPlugin } = props || {};
-
- const { connections, onGet, onTest, onRefresh } =
useContext(ConnectionContext);
-
- return {
- connection: unique ? connections.find((cs) => cs.unique === unique) : null,
- connections: connections
- .filter((cs) => (plugin ? cs.plugin === plugin : true))
- .filter((cs) => (filter ? !filter.includes(cs.unique) : true))
- .filter((cs) => (filterBeta ? !cs.isBeta : true))
- .filter((cs) => (filterPlugin ? !filterPlugin.includes(cs.plugin) :
true)),
- onGet,
- onTest,
- onRefresh,
- };
-};
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..cb7ffa549 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(
() =>
@@ -73,7 +69,7 @@ export const ConnectionForm = ({ plugin, connectionId,
onSuccess }: Props) => {
'secretKey',
'tenantId',
'tenantType',
- "dbUrl",
+ 'dbUrl',
]),
),
{
@@ -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..136f9b59b 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 } 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/config.ts b/config-ui/src/plugins/config.ts
index ebac6b5fe..d840f84a8 100644
--- a/config-ui/src/plugins/config.ts
+++ b/config-ui/src/plugins/config.ts
@@ -28,7 +28,6 @@ import { PagerDutyConfig } from './register/pagerduty';
import { SonarQubeConfig } from './register/sonarqube';
import { TAPDConfig } from './register/tapd';
import { WebhookConfig } from './register/webhook';
-import { TeambitionConfig } from './register/teambition';
import { ZenTaoConfig } from './register/zentao';
export const PluginConfig: PluginConfigType[] = [
@@ -42,7 +41,6 @@ export const PluginConfig: PluginConfigType[] = [
PagerDutyConfig,
SonarQubeConfig,
TAPDConfig,
- TeambitionConfig,
ZenTaoConfig,
WebhookConfig,
].sort((a, b) => a.sort - b.sort);
diff --git a/config-ui/src/plugins/register/azure/config.tsx
b/config-ui/src/plugins/register/azure/config.tsx
index 500a4e389..799d15a3e 100644
--- a/config-ui/src/plugins/register/azure/config.tsx
+++ b/config-ui/src/plugins/register/azure/config.tsx
@@ -20,13 +20,11 @@ import { ExternalLink } from '@/components';
import { DOC_URL } from '@/release';
import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
import Icon from './assets/icon.svg';
import { BaseURL } from './connection-fields';
export const AzureConfig: PluginConfigType = {
- type: PluginType.Connection,
plugin: 'azuredevops',
name: 'Azure DevOps',
icon: Icon,
diff --git a/config-ui/src/plugins/register/bamboo/config.ts
b/config-ui/src/plugins/register/bamboo/config.ts
index 10ccbf44b..b2c78850c 100644
--- a/config-ui/src/plugins/register/bamboo/config.ts
+++ b/config-ui/src/plugins/register/bamboo/config.ts
@@ -19,12 +19,10 @@
import { DOC_URL } from '@/release';
import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
import Icon from './assets/icon.svg';
export const BambooConfig: PluginConfigType = {
- type: PluginType.Connection,
plugin: 'bamboo',
name: 'Bamboo',
icon: Icon,
diff --git a/config-ui/src/plugins/register/bitbucket/config.tsx
b/config-ui/src/plugins/register/bitbucket/config.tsx
index 11c03bb3d..97840f382 100644
--- a/config-ui/src/plugins/register/bitbucket/config.tsx
+++ b/config-ui/src/plugins/register/bitbucket/config.tsx
@@ -19,12 +19,10 @@
import { DOC_URL } from '@/release';
import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
import Icon from './assets/icon.svg';
export const BitBucketConfig: PluginConfigType = {
- type: PluginType.Connection,
plugin: 'bitbucket',
name: 'BitBucket',
icon: Icon,
diff --git a/config-ui/src/plugins/register/github/config.tsx
b/config-ui/src/plugins/register/github/config.tsx
index fc255675f..f79b56eaa 100644
--- a/config-ui/src/plugins/register/github/config.tsx
+++ b/config-ui/src/plugins/register/github/config.tsx
@@ -21,13 +21,11 @@ import { pick } from 'lodash';
import { DOC_URL } from '@/release';
import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
import Icon from './assets/icon.svg';
import { Token, Graphql, GithubApp, Authentication } from
'./connection-fields';
export const GitHubConfig: PluginConfigType = {
- type: PluginType.Connection,
plugin: 'github',
name: 'GitHub',
icon: Icon,
diff --git a/config-ui/src/plugins/register/gitlab/config.tsx
b/config-ui/src/plugins/register/gitlab/config.tsx
index 53d11d044..74bd7f007 100644
--- a/config-ui/src/plugins/register/gitlab/config.tsx
+++ b/config-ui/src/plugins/register/gitlab/config.tsx
@@ -20,12 +20,10 @@ import { ExternalLink } from '@/components';
import { DOC_URL } from '@/release';
import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
import Icon from './assets/icon.svg';
export const GitLabConfig: PluginConfigType = {
- type: PluginType.Connection,
plugin: 'gitlab',
name: 'GitLab',
icon: Icon,
diff --git a/config-ui/src/plugins/register/jenkins/config.ts
b/config-ui/src/plugins/register/jenkins/config.ts
index fc77932ac..fb08df4e7 100644
--- a/config-ui/src/plugins/register/jenkins/config.ts
+++ b/config-ui/src/plugins/register/jenkins/config.ts
@@ -19,12 +19,10 @@
import { DOC_URL } from '@/release';
import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
import Icon from './assets/icon.svg';
export const JenkinsConfig: PluginConfigType = {
- type: PluginType.Connection,
plugin: 'jenkins',
name: 'Jenkins',
icon: Icon,
diff --git a/config-ui/src/plugins/register/jira/config.tsx
b/config-ui/src/plugins/register/jira/config.tsx
index 3daa049f5..ce81ecd44 100644
--- a/config-ui/src/plugins/register/jira/config.tsx
+++ b/config-ui/src/plugins/register/jira/config.tsx
@@ -19,13 +19,11 @@
import { DOC_URL } from '@/release';
import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
import Icon from './assets/icon.svg';
import { Auth } from './connection-fields';
export const JiraConfig: PluginConfigType = {
- type: PluginType.Connection,
plugin: 'jira',
name: 'Jira',
icon: Icon,
diff --git a/config-ui/src/plugins/register/pagerduty/config.tsx
b/config-ui/src/plugins/register/pagerduty/config.tsx
index 5c115711f..b7f7ab351 100644
--- a/config-ui/src/plugins/register/pagerduty/config.tsx
+++ b/config-ui/src/plugins/register/pagerduty/config.tsx
@@ -19,12 +19,10 @@
import { DOC_URL } from '@/release';
import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
import Icon from './assets/icon.svg';
export const PagerDutyConfig: PluginConfigType = {
- type: PluginType.Connection,
plugin: 'pagerduty',
name: 'PagerDuty',
icon: Icon,
diff --git a/config-ui/src/plugins/register/sonarqube/config.ts
b/config-ui/src/plugins/register/sonarqube/config.ts
index db6bfe17e..c64db0837 100644
--- a/config-ui/src/plugins/register/sonarqube/config.ts
+++ b/config-ui/src/plugins/register/sonarqube/config.ts
@@ -19,12 +19,10 @@
import { DOC_URL } from '@/release';
import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
import Icon from './assets/icon.svg';
export const SonarQubeConfig: PluginConfigType = {
- type: PluginType.Connection,
plugin: 'sonarqube',
name: 'SonarQube',
icon: Icon,
diff --git a/config-ui/src/plugins/register/tapd/config.tsx
b/config-ui/src/plugins/register/tapd/config.tsx
index 0625f6aab..a3a2026b3 100644
--- a/config-ui/src/plugins/register/tapd/config.tsx
+++ b/config-ui/src/plugins/register/tapd/config.tsx
@@ -20,13 +20,11 @@ import { ExternalLink } from '@/components';
import { DOC_URL } from '@/release';
import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
import { DataScope } from './data-scope';
import Icon from './assets/icon.svg';
export const TAPDConfig: PluginConfigType = {
- type: PluginType.Connection,
plugin: 'tapd',
name: 'TAPD',
icon: Icon,
diff --git a/config-ui/src/plugins/register/teambition/assets/icon.svg
b/config-ui/src/plugins/register/teambition/assets/icon.svg
deleted file mode 100644
index 93d227f72..000000000
--- a/config-ui/src/plugins/register/teambition/assets/icon.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-<svg width="36" height="36" viewBox="0 0 36 36" fill="none"
xmlns="http://www.w3.org/2000/svg">
-<path d="M24.8601 7.75227C24.8606 6.58118 25.3105 5.45494 26.1169
4.60578C26.9234 3.75663 28.025 3.24932 29.1945 3.18845C26.7937 3.06965 24.1226
3 21.345 3C10.6606 3 2 3.98733 2 5.20407C2 6.42082 10.6606 7.40814 21.345
7.40814V12.3243H28.9487C28.7076 12.3059 28.4704 12.2535 28.244 12.1686C27.2724
11.9079 26.414 11.3336 25.8021 10.5351C25.1902 9.73651 24.8591 8.7583 24.8601
7.75227Z" fill="#BDCEFB"/>
-<path d="M21.345 7.40329C10.6606 7.40329 2 6.41596 2 5.19922V13.1757C2 14.0892
6.86288 14.8717 13.7987 15.2241V30.9066L21.4024 32.3609L21.345 7.40329Z"
fill="#7497F7"/>
-<path d="M29.428 12.3237C31.953 12.3237 34 10.2768 34 7.7517C34 5.22665 31.953
3.17969 29.428 3.17969C26.9029 3.17969 24.856 5.22665 24.856 7.7517C24.856
10.2768 26.9029 12.3237 29.428 12.3237Z" fill="#7497F7"/>
-</svg>
diff --git a/config-ui/src/plugins/register/teambition/config.tsx
b/config-ui/src/plugins/register/teambition/config.tsx
deleted file mode 100644
index 079ca4d1a..000000000
--- a/config-ui/src/plugins/register/teambition/config.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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 { DOC_URL } from '@/release';
-
-import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
-
-import Icon from './assets/icon.svg';
-import { ConnectionTenantId, ConnectionTenantType } from './connection-fields';
-
-export const TeambitionConfig: PluginConfigType = {
- type: PluginType.Pipeline,
- plugin: 'teambition',
- name: 'Teambition',
- isBeta: true,
- icon: Icon,
- sort: 100,
- connection: {
- docLink: DOC_URL.PLUGIN.TEAMBITION.BASIS,
- initialValues: {
- endpoint: 'https://open.teambition.com/api/',
- tenantType: 'organization',
- },
- fields: [
- 'name',
- {
- key: 'endpoint',
- subLabel: 'You do not need to enter the endpoint URL, because all
versions use the same URL.',
- disabled: true,
- },
- {
- key: 'appId',
- label: 'Application App Id',
- subLabel: 'Your teambition application App Id.',
- },
- {
- key: 'secretKey',
- label: 'Application Secret Key',
- subLabel: 'Your teambition application App Secret.',
- },
- ({ initialValues, values, errors, setValues, setErrors }: any) => (
- <ConnectionTenantId
- key="tenantId"
- name="tenantId"
- value={values.tenantId ?? ''}
- error={errors.tenantId ?? ''}
- setValue={(value) => setValues({ tenantId: value })}
- setError={(value) => setErrors({ tenantId: value })}
- initialValue={initialValues.tenantId}
- />
- ),
- ({ initialValues, values, errors, setValues, setErrors }: any) => (
- <ConnectionTenantType
- key="tenantType"
- name="tenantType"
- value={values.tenantType ?? ''}
- error={errors.tenantType ?? ''}
- setValue={(value) => setValues({ tenantType: value })}
- setError={(value) => setErrors({ tenantType: value })}
- initialValue={initialValues.tenantType}
- />
- ),
- 'proxy',
- {
- key: 'rateLimitPerHour',
- subLabel:
- 'By default, DevLake uses dynamic rate limit for optimized data
collection for Teambition. But you can adjust the collection speed by entering
a fixed value. Please note: the rate limit setting applies to all tokens you
have entered above.',
- learnMore: DOC_URL.PLUGIN.TEAMBITION.RATE_LIMIT,
- externalInfo: 'Teambition does not specify a maximum value of rate
limit.',
- defaultValue: 5000,
- },
- ],
- },
- dataScope: {
- title: '',
- },
- scopeConfig: {
- entities: ['TICKET'],
- transformation: {},
- },
-};
diff --git
a/config-ui/src/plugins/register/teambition/connection-fields/tenant-id.tsx
b/config-ui/src/plugins/register/teambition/connection-fields/tenant-id.tsx
deleted file mode 100644
index a5d1a64d4..000000000
--- a/config-ui/src/plugins/register/teambition/connection-fields/tenant-id.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * 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.
- *
- */
-/*
- * 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 { useEffect } from 'react';
-import { FormGroup, InputGroup } from '@blueprintjs/core';
-
-import * as S from './styled';
-
-interface Props {
- name: string;
- initialValue: string;
- value: string;
- error: string;
- setValue: (value: string) => void;
- setError: (error: string) => void;
-}
-
-export const ConnectionTenantId = ({ initialValue, value, setValue, setError
}: Props) => {
- useEffect(() => {
- setValue(initialValue);
- }, [initialValue]);
-
- useEffect(() => {
- setError(value ? '' : 'TenantId is required');
- }, [value]);
-
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- setValue(e.target.value);
- };
-
- return (
- <FormGroup
- label={<S.Label>Tenant Id</S.Label>}
- labelInfo={<S.LabelInfo>*</S.LabelInfo>}
- subLabel={<S.LabelDescription>Your teambition organization
id.</S.LabelDescription>}
- >
- <InputGroup placeholder="Your TenantId" value={value}
onChange={handleChange} />
- </FormGroup>
- );
-};
diff --git
a/config-ui/src/plugins/register/teambition/connection-fields/tenant-type.tsx
b/config-ui/src/plugins/register/teambition/connection-fields/tenant-type.tsx
deleted file mode 100644
index 613f48b87..000000000
---
a/config-ui/src/plugins/register/teambition/connection-fields/tenant-type.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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.
- *
- */
-/*
- * 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 { useEffect } from 'react';
-import { FormGroup, InputGroup } from '@blueprintjs/core';
-
-import * as S from './styled';
-
-interface Props {
- name: string;
- initialValue: string;
- value: string;
- error: string;
- setValue: (value: string) => void;
- setError: (error: string) => void;
-}
-
-export const ConnectionTenantType = ({ initialValue, value, setValue, setError
}: Props) => {
- useEffect(() => {
- setValue(initialValue);
- }, [initialValue]);
-
- useEffect(() => {
- setError(value ? '' : 'TenantType is required');
- }, [value]);
-
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- setValue(e.target.value);
- };
-
- return (
- <FormGroup
- label={<S.Label>Tenant Type</S.Label>}
- labelInfo={<S.LabelInfo>*</S.LabelInfo>}
- subLabel={
- <S.LabelDescription>
- You do not need to enter the tenant type, because teambition only
supports 'organization' type currently.
- </S.LabelDescription>
- }
- >
- <InputGroup disabled={true} placeholder="Your API Tenant Type"
value={value} onChange={handleChange} />
- </FormGroup>
- );
-};
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/config.ts
b/config-ui/src/plugins/register/webhook/config.ts
index 25088545d..b0f74c680 100644
--- a/config-ui/src/plugins/register/webhook/config.ts
+++ b/config-ui/src/plugins/register/webhook/config.ts
@@ -17,14 +17,12 @@
*/
import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
import Icon from './assets/icon.svg';
export const WebhookConfig: PluginConfigType = {
plugin: 'webhook',
name: 'Webhook',
- type: PluginType.Connection,
icon: Icon,
sort: 100,
connection: {
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/plugins/register/zentao/config.tsx
b/config-ui/src/plugins/register/zentao/config.tsx
index c5ee0ccb1..705225dc7 100644
--- a/config-ui/src/plugins/register/zentao/config.tsx
+++ b/config-ui/src/plugins/register/zentao/config.tsx
@@ -19,13 +19,11 @@
import { DOC_URL } from '@/release';
import type { PluginConfigType } from '../../types';
-import { PluginType } from '../../types';
import { DBUrl } from './connection-fields';
import Icon from './assets/icon.svg';
export const ZenTaoConfig: PluginConfigType = {
- type: PluginType.Connection,
plugin: 'zentao',
name: 'ZenTao',
icon: Icon,
diff --git a/config-ui/src/plugins/types.ts b/config-ui/src/plugins/types.ts
index b79e6c7c7..941a4715e 100644
--- a/config-ui/src/plugins/types.ts
+++ b/config-ui/src/plugins/types.ts
@@ -16,13 +16,7 @@
*
*/
-export enum PluginType {
- Connection = 'connection',
- Pipeline = 'pipeline',
-}
-
export type PluginConfigType = {
- type: PluginType;
plugin: string;
name: string;
icon: string;
diff --git a/config-ui/src/plugins/utils.ts b/config-ui/src/plugins/utils.ts
index f58667ffd..23e7af5f4 100644
--- a/config-ui/src/plugins/utils.ts
+++ b/config-ui/src/plugins/utils.ts
@@ -19,7 +19,7 @@
import PluginIcon from '@/images/plugin-icon.svg';
import { PluginConfig } from './config';
-import { PluginConfigType, PluginType } from './types';
+import { PluginConfigType } from './types';
export const getPluginScopeId = (plugin: string, scope: any) => {
switch (plugin) {
@@ -46,7 +46,6 @@ export const getPluginConfig = (name: string):
PluginConfigType => {
let pluginConfig = PluginConfig.find((plugin) => plugin.plugin === name) as
PluginConfigType;
if (!pluginConfig) {
pluginConfig = {
- type: PluginType.Pipeline,
plugin: name,
name: name,
icon: PluginIcon,
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/store/connections/context.tsx
b/config-ui/src/store/connections/context.tsx
deleted file mode 100644
index 42e213dea..000000000
--- a/config-ui/src/store/connections/context.tsx
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * 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 React, { useState, useEffect, useMemo } from 'react';
-
-import { PageLoading } from '@/components';
-
-import API from '@/api';
-import type { PluginConfigType } from '@/plugins';
-import { PluginConfig, PluginType } from '@/plugins';
-
-import type { ConnectionItemType } from './types';
-import { ConnectionStatusEnum } from './types';
-
-export const ConnectionContext = React.createContext<{
- connections: ConnectionItemType[];
- onGet: (unique: string) => ConnectionItemType;
- onTest: (unique: string) => void;
- onRefresh: (plugin?: string) => void;
-}>(undefined!);
-
-interface Props {
- children?: React.ReactNode;
-}
-
-export const ConnectionContextProvider = ({ children, ...props }: Props) => {
- const [loading, setLoading] = useState(true);
- const [connections, setConnections] = useState<ConnectionItemType[]>([]);
-
- const plugins = useMemo(() => PluginConfig.filter((p) => p.type ===
PluginType.Connection), []);
-
- const queryConnection = async (plugin: string) => {
- try {
- const res = await API.connection.list(plugin);
- const { name, icon, isBeta, scopeConfig } = plugins.find((p) => p.plugin
=== plugin) as PluginConfigType;
-
- return res.map((connection) => ({
- ...connection,
- plugin,
- pluginName: name,
- icon,
- isBeta: isBeta ?? false,
- entities: scopeConfig?.entities ?? [],
- }));
- } catch {
- return [];
- }
- };
-
- const testConnection = async ({
- plugin,
- endpoint,
- proxy,
- token,
- username,
- password,
- authMethod,
- secretKey,
- appId,
- dbUrl,
- }: ConnectionItemType) => {
- try {
- const res = await API.connection.test(plugin, {
- endpoint,
- proxy,
- token,
- username,
- password,
- authMethod,
- secretKey,
- appId,
- dbUrl,
- });
- return res.success ? ConnectionStatusEnum.ONLINE :
ConnectionStatusEnum.OFFLINE;
- } catch {
- return ConnectionStatusEnum.OFFLINE;
- }
- };
-
- const transformConnection = (connections: Omit<ConnectionItemType, 'unique'
| 'status'>[]) => {
- return connections.map((it) => ({
- unique: `${it.plugin}-${it.id}`,
- plugin: it.plugin,
- pluginName: it.pluginName,
- id: it.id,
- name: it.name,
- status: ConnectionStatusEnum.NULL,
- icon: it.icon,
- isBeta: it.isBeta,
- entities: it.entities,
- endpoint: it.endpoint,
- proxy: it.proxy,
- token: it.token,
- username: it.username,
- password: it.password,
- authMethod: it.authMethod,
- secretKey: it.secretKey,
- appId: it.appId,
- dbUrl: it.dbUrl,
- }));
- };
-
- const handleGet = (unique: string) => {
- return connections.find((cs) => cs.unique === unique) as
ConnectionItemType;
- };
-
- const handleTest = async (unique: string) => {
- setConnections((connections) =>
- connections.map((cs) =>
- cs.unique === unique
- ? {
- ...cs,
- status: ConnectionStatusEnum.TESTING,
- }
- : cs,
- ),
- );
-
- const connection = handleGet(unique);
- const status = await testConnection(connection);
-
- setConnections((connections) =>
- connections.map((cs) =>
- cs.unique === unique
- ? {
- ...cs,
- status,
- }
- : cs,
- ),
- );
- };
-
- const handleRefresh = async (plugin?: string) => {
- if (plugin) {
- const res = await queryConnection(plugin);
- setConnections([...connections.filter((cs) => cs.plugin !== plugin),
...transformConnection(res)]);
- return;
- }
-
- const res = await Promise.all(plugins.map((cs) =>
queryConnection(cs.plugin)));
-
- setConnections(transformConnection(res.flat()));
- setLoading(false);
- };
-
- useEffect(() => {
- handleRefresh();
- }, []);
-
- if (loading) {
- return <PageLoading />;
- }
-
- return (
- <ConnectionContext.Provider
- value={{
- connections,
- onGet: handleGet,
- onTest: handleTest,
- onRefresh: handleRefresh,
- }}
- >
- {children}
- </ConnectionContext.Provider>
- );
-};
diff --git a/config-ui/src/store/index.ts b/config-ui/src/store/index.ts
index a247f9b21..378c0c717 100644
--- a/config-ui/src/store/index.ts
+++ b/config-ui/src/store/index.ts
@@ -16,5 +16,4 @@
*
*/
-export * from './connections';
export * from './tips';
diff --git a/config-ui/src/store/connections/types.ts
b/config-ui/src/types/connection.ts
similarity index 87%
rename from config-ui/src/store/connections/types.ts
rename to config-ui/src/types/connection.ts
index 07475889a..6225fd63b 100644
--- a/config-ui/src/store/connections/types.ts
+++ b/config-ui/src/types/connection.ts
@@ -16,30 +16,28 @@
*
*/
-export enum ConnectionStatusEnum {
+export enum IConnectionStatus {
+ IDLE = 'idle',
+ TESTING = 'testing',
ONLINE = 'online',
OFFLINE = 'offline',
- TESTING = 'testing',
- NULL = 'null',
}
-export type ConnectionItemType = {
+export interface IConnection {
unique: string;
plugin: string;
pluginName: string;
id: ID;
name: string;
- status: ConnectionStatusEnum;
+ status: IConnectionStatus;
icon: string;
isBeta: boolean;
- entities: string[];
endpoint: string;
proxy: string;
+ authMethod?: string;
token?: string;
username?: string;
password?: string;
- authMethod?: string;
appId?: string;
secretKey?: string;
- dbUrl?: string;
-};
+}
diff --git a/config-ui/src/plugins/register/teambition/index.ts
b/config-ui/src/types/index.ts
similarity index 96%
rename from config-ui/src/plugins/register/teambition/index.ts
rename to config-ui/src/types/index.ts
index de415db39..fcc98feb8 100644
--- a/config-ui/src/plugins/register/teambition/index.ts
+++ b/config-ui/src/types/index.ts
@@ -16,4 +16,4 @@
*
*/
-export * from './config';
+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"