You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by mi...@apache.org on 2023/05/26 01:09:05 UTC
[incubator-devlake] branch main updated: feat(config-ui): use new create bp and project design (#5284)
This is an automated email from the ASF dual-hosted git repository.
mintsweet 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 6813da56e feat(config-ui): use new create bp and project design (#5284)
6813da56e is described below
commit 6813da56e116438f49984de863989cb0da67f31f
Author: 青湛 <0x...@gmail.com>
AuthorDate: Fri May 26 09:09:00 2023 +0800
feat(config-ui): use new create bp and project design (#5284)
* feat(config-ui): export history in utils
* feat(config-ui): export login page in pages
* refactor(config-ui): don't load custom hook when table is loading
* feat(config-ui): use new create bp logic to replace old
* feat(config-ui): use new create project logic to replace old
* refactor(config-ui): remove the bp create page
---
config-ui/src/App.tsx | 16 +-
.../table/{table.tsx => components/content.tsx} | 32 +-
config-ui/src/components/table/components/index.ts | 1 +
config-ui/src/components/table/table.tsx | 78 +----
config-ui/src/pages/blueprint/create/api.ts | 30 --
.../pages/blueprint/create/components/step-1.tsx | 166 -----------
.../pages/blueprint/create/components/step-2.tsx | 40 ---
.../pages/blueprint/create/components/step-3.tsx | 33 ---
.../pages/blueprint/create/components/step-4.tsx | 72 -----
.../pages/blueprint/create/components/styled.ts | 101 -------
config-ui/src/pages/blueprint/create/context.tsx | 183 ------------
config-ui/src/pages/blueprint/create/index.tsx | 76 -----
config-ui/src/pages/blueprint/create/types.ts | 47 ---
config-ui/src/pages/blueprint/home/api.ts | 16 +-
config-ui/src/pages/blueprint/home/index.tsx | 323 ++++++++++++++-------
config-ui/src/pages/blueprint/home/styled.ts | 20 ++
config-ui/src/pages/blueprint/home/use-home.ts | 79 -----
config-ui/src/pages/blueprint/index.ts | 1 -
config-ui/src/pages/index.ts | 9 +-
.../create/components => login}/index.ts | 5 +-
config-ui/src/pages/project/home/api.ts | 16 +-
config-ui/src/pages/project/home/index.tsx | 196 ++++++++-----
config-ui/src/pages/project/home/styled.ts | 33 +--
config-ui/src/pages/project/home/use-project.ts | 92 ------
.../components/index.ts => project/utils.ts} | 7 +-
config-ui/src/utils/index.ts | 3 +-
26 files changed, 430 insertions(+), 1245 deletions(-)
diff --git a/config-ui/src/App.tsx b/config-ui/src/App.tsx
index 51b14cb60..3998e95c1 100644
--- a/config-ui/src/App.tsx
+++ b/config-ui/src/App.tsx
@@ -17,11 +17,9 @@
*/
import { Switch, Route, Redirect, Router } from 'react-router-dom';
-import { LoginPage } from './pages/login/login';
-import { history } from './utils/history';
import { ErrorLayout, BaseLayout } from '@/layouts';
-import { FromEnum } from '@/pages';
import {
+ LoginPage,
OfflinePage,
DBMigratePage,
ConnectionHomePage,
@@ -29,11 +27,11 @@ import {
ProjectHomePage,
ProjectDetailPage,
BlueprintHomePage,
- BlueprintCreatePage,
BlueprintDetailPage,
BlueprintConnectioAddPage,
BlueprintConnectionDetailPage,
} from '@/pages';
+import { history } from '@/utils';
function App() {
return (
@@ -77,17 +75,7 @@ function App() {
component={() => <BlueprintConnectioAddPage />}
/>
<Route exact path="/projects/:pname/:bid/:unique" component={() => <BlueprintConnectionDetailPage />} />
- <Route
- exact
- path="/projects/:pname/create-blueprint"
- component={() => <BlueprintCreatePage from={FromEnum.project} />}
- />
<Route exact path="/blueprints" component={() => <BlueprintHomePage />} />
- <Route
- exact
- path="/blueprints/create"
- component={() => <BlueprintCreatePage from={FromEnum.blueprint} />}
- />
<Route exact path="/blueprints/:id" component={() => <BlueprintDetailPage />} />
<Route exact path="/blueprints/:bid/connection-add" component={() => <BlueprintConnectioAddPage />} />
<Route exact path="/blueprints/:bid/:unique" component={() => <BlueprintConnectionDetailPage />} />
diff --git a/config-ui/src/components/table/table.tsx b/config-ui/src/components/table/components/content.tsx
similarity index 82%
copy from config-ui/src/components/table/table.tsx
copy to config-ui/src/components/table/components/content.tsx
index bace2e740..9b288827d 100644
--- a/config-ui/src/components/table/table.tsx
+++ b/config-ui/src/components/table/components/content.tsx
@@ -16,49 +16,31 @@
*
*/
-import React from 'react';
import { Checkbox, Radio } from '@blueprintjs/core';
import { TextTooltip } from '@/components';
-import { ColumnType } from './types';
-import { TableLoading, TableNoData } from './components';
-import { useRowSelection, UseRowSelectionProps } from './hooks';
-import * as S from './styled';
+import { ColumnType } from '../types';
+import { useRowSelection, UseRowSelectionProps } from '../hooks';
+import * as S from '../styled';
-interface Props<T> extends UseRowSelectionProps<T> {
- loading?: boolean;
+export interface TableContentProps<T> extends UseRowSelectionProps<T> {
columns: ColumnType<T>;
dataSource: T[];
- noData?: {
- text?: React.ReactNode;
- btnText?: string;
- onCreate?: () => void;
- };
noShadow?: boolean;
}
-export const Table = <T extends Record<string, any>>({
- loading,
+export const TableContent = <T extends Record<string, any>>({
columns,
dataSource,
- noData = {},
+ noShadow,
rowSelection,
- noShadow = false,
-}: Props<T>) => {
+}: TableContentProps<T>) => {
const { canSelection, selectionType, getCheckedAll, onCheckedAll, getChecked, onChecked } = useRowSelection<T>({
dataSource,
rowSelection,
});
- if (loading) {
- return <TableLoading />;
- }
-
- if (!dataSource.length) {
- return <TableNoData {...noData} />;
- }
-
return (
<S.Table
style={{
diff --git a/config-ui/src/components/table/components/index.ts b/config-ui/src/components/table/components/index.ts
index ef68e2564..df5950d7f 100644
--- a/config-ui/src/components/table/components/index.ts
+++ b/config-ui/src/components/table/components/index.ts
@@ -18,3 +18,4 @@
export * from './loading';
export * from './no-data';
+export * from './content';
diff --git a/config-ui/src/components/table/table.tsx b/config-ui/src/components/table/table.tsx
index bace2e740..f4188d5a3 100644
--- a/config-ui/src/components/table/table.tsx
+++ b/config-ui/src/components/table/table.tsx
@@ -17,40 +17,20 @@
*/
import React from 'react';
-import { Checkbox, Radio } from '@blueprintjs/core';
-import { TextTooltip } from '@/components';
+import type { TableContentProps } from './components';
+import { TableLoading, TableNoData, TableContent } from './components';
-import { ColumnType } from './types';
-import { TableLoading, TableNoData } from './components';
-import { useRowSelection, UseRowSelectionProps } from './hooks';
-import * as S from './styled';
-
-interface Props<T> extends UseRowSelectionProps<T> {
+interface Props<T> extends TableContentProps<T> {
loading?: boolean;
- columns: ColumnType<T>;
- dataSource: T[];
noData?: {
text?: React.ReactNode;
btnText?: string;
onCreate?: () => void;
};
- noShadow?: boolean;
}
-export const Table = <T extends Record<string, any>>({
- loading,
- columns,
- dataSource,
- noData = {},
- rowSelection,
- noShadow = false,
-}: Props<T>) => {
- const { canSelection, selectionType, getCheckedAll, onCheckedAll, getChecked, onChecked } = useRowSelection<T>({
- dataSource,
- rowSelection,
- });
-
+export const Table = <T extends Record<string, any>>({ loading, dataSource, noData = {}, ...props }: Props<T>) => {
if (loading) {
return <TableLoading />;
}
@@ -59,53 +39,5 @@ export const Table = <T extends Record<string, any>>({
return <TableNoData {...noData} />;
}
- return (
- <S.Table
- style={{
- boxShadow: noShadow ? 'none' : '0px 2.4px 4.8px -0.8px rgba(0, 0, 0, 0.1), 0px 1.6px 8px rgba(0, 0, 0, 0.07)',
- }}
- >
- <S.THeader>
- <S.TR>
- {canSelection && (
- <S.TH style={{ width: 40, textAlign: 'center' }}>
- {selectionType === 'checkbox' && <Checkbox checked={getCheckedAll()} onChange={() => onCheckedAll()} />}
- </S.TH>
- )}
- {columns.map(({ key, width, align = 'left', title }) => (
- <S.TH key={key} style={{ width, textAlign: align }}>
- {title}
- </S.TH>
- ))}
- </S.TR>
- </S.THeader>
- <S.TBody>
- {dataSource.map((data, i) => (
- <S.TR key={i}>
- {canSelection && (
- <S.TD style={{ width: 40, textAlign: 'center' }}>
- {selectionType === 'checkbox' && (
- <Checkbox checked={getChecked(data)} onChange={() => onChecked(data)} />
- )}
- {selectionType === 'radio' && <Radio checked={getChecked(data)} onChange={() => onChecked(data)} />}
- </S.TD>
- )}
- {columns.map(({ key, width, align = 'left', ellipsis, dataIndex, render }) => {
- const value = Array.isArray(dataIndex)
- ? dataIndex.reduce((acc, cur) => {
- acc[cur] = data[cur];
- return acc;
- }, {} as any)
- : data[dataIndex];
- return (
- <S.TD key={key} style={{ width, textAlign: align }}>
- {render ? render(value, data) : ellipsis ? <TextTooltip content={value}>{value}</TextTooltip> : value}
- </S.TD>
- );
- })}
- </S.TR>
- ))}
- </S.TBody>
- </S.Table>
- );
+ return <TableContent dataSource={dataSource} {...props} />;
};
diff --git a/config-ui/src/pages/blueprint/create/api.ts b/config-ui/src/pages/blueprint/create/api.ts
deleted file mode 100644
index c82ccaef8..000000000
--- a/config-ui/src/pages/blueprint/create/api.ts
+++ /dev/null
@@ -1,30 +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 { request } from '@/utils';
-
-export const createBlueprint = (payload: any) =>
- request('/blueprints', {
- method: 'post',
- data: payload,
- });
-
-export const runBlueprint = (id: ID) =>
- request(`/blueprints/${id}/trigger`, {
- method: 'post',
- });
diff --git a/config-ui/src/pages/blueprint/create/components/step-1.tsx b/config-ui/src/pages/blueprint/create/components/step-1.tsx
deleted file mode 100644
index a546dd44b..000000000
--- a/config-ui/src/pages/blueprint/create/components/step-1.tsx
+++ /dev/null
@@ -1,166 +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 { useMemo } from 'react';
-import { Link } from 'react-router-dom';
-import { InputGroup, Icon, Button, Intent } from '@blueprintjs/core';
-
-import { Card, Divider, MultiSelector, Loading } from '@/components';
-import { useConnections } from '@/hooks';
-import { getPluginConfig } from '@/plugins';
-import { ConnectionStatusEnum } from '@/store';
-
-import { ModeEnum, FromEnum } from '../../types';
-import { AdvancedEditor } from '../../components';
-import { validRawPlan } from '../../utils';
-
-import { useCreate } from '../context';
-
-import * as S from './styled';
-
-interface Props {
- from: FromEnum;
-}
-
-export const Step1 = ({ from }: Props) => {
- const { connections, onTest } = useConnections({ filterBeta: true, filterPlugin: ['webhook'] });
- const { mode, name, rawPlan, onChangeMode, onChangeName, onChangeConnections, onChangeRawPlan, onNext, ...props } =
- useCreate();
-
- const fromProject = useMemo(() => from === FromEnum.project, [from]);
- const uniqueList = useMemo(() => props.connections.map((sc) => sc.unique), [props.connections]);
-
- const error = useMemo(() => {
- switch (true) {
- case !name:
- return true;
- case name.length < 3:
- return true;
- case mode === ModeEnum.advanced && validRawPlan(rawPlan):
- return true;
- case mode === ModeEnum.normal && !uniqueList.length:
- return true;
- case mode === ModeEnum.normal &&
- !connections
- .filter((cs) => uniqueList.includes(cs.unique))
- .every((cs) => cs.status === ConnectionStatusEnum.ONLINE):
- return true;
- default:
- return false;
- }
- }, [mode, name, connections, props.connections, rawPlan]);
-
- return (
- <S.Wrapper>
- <Card className="card">
- <h2>Blueprint Name</h2>
- <Divider />
- <p>Give your Blueprint a unique name to help you identify it in the future.</p>
- <InputGroup placeholder="Enter Blueprint Name" value={name} onChange={(e) => onChangeName(e.target.value)} />
- </Card>
-
- {mode === ModeEnum.normal && (
- <>
- <Card className="card">
- <h2>Add Data Connections</h2>
- <Divider />
- <p>
- If you have not created any connections yet, please <Link to="/connections">create connections</Link>{' '}
- first.
- </p>
- <MultiSelector
- placeholder="Select Connections..."
- items={connections}
- getKey={(it) => it.unique}
- getName={(it) => it.name}
- getIcon={(it) => it.icon}
- selectedItems={connections.filter((cs) => uniqueList.includes(cs.unique))}
- onChangeItems={(selectedItems) => {
- const lastItem = selectedItems[selectedItems.length - 1];
- if (lastItem) {
- onTest(lastItem.unique);
- }
- onChangeConnections(
- selectedItems.map((sc) => {
- const config = getPluginConfig(sc.plugin);
- return {
- unique: sc.unique,
- plugin: sc.plugin,
- connectionId: sc.id,
- name: sc.name,
- icon: sc.icon,
- scope: [],
- origin: [],
- transformationType: config.transformationType,
- };
- }),
- );
- }}
- />
- <S.ConnectionList>
- {connections
- .filter((cs) => uniqueList.includes(cs.unique))
- .map((cs) => (
- <li key={cs.unique}>
- <span className="name">{cs.name}</span>
- <span className={`status ${cs.status}`}>
- {cs.status === ConnectionStatusEnum.TESTING && <Loading size={14} style={{ marginRight: 4 }} />}
- {cs.status === ConnectionStatusEnum.OFFLINE && (
- <Icon
- size={14}
- icon="repeat"
- style={{ marginRight: 4, cursor: 'pointer' }}
- onClick={() => onTest(cs.unique)}
- />
- )}
- {cs.status}
- </span>
- </li>
- ))}
- </S.ConnectionList>
- </Card>
- {!fromProject && (
- <S.Tips>
- <span>To customize how tasks are executed in the blueprint, please use </span>
- <span onClick={() => onChangeMode(ModeEnum.advanced)}>Advanced Mode.</span>
- </S.Tips>
- )}
- </>
- )}
-
- {mode === ModeEnum.advanced && !fromProject && (
- <>
- <Card className="card">
- <h2>JSON Configuration</h2>
- <Divider />
- <AdvancedEditor value={rawPlan} onChange={onChangeRawPlan} />
- </Card>
- <S.Tips>
- <span>To visually define blueprint tasks, please use </span>
- <span onClick={() => onChangeMode(ModeEnum.normal)}>Normal Mode.</span>
- </S.Tips>
- </>
- )}
-
- <S.Btns>
- <span></span>
- <Button intent={Intent.PRIMARY} disabled={error} text="Next Step" onClick={onNext} />
- </S.Btns>
- </S.Wrapper>
- );
-};
diff --git a/config-ui/src/pages/blueprint/create/components/step-2.tsx b/config-ui/src/pages/blueprint/create/components/step-2.tsx
deleted file mode 100644
index e3dbff814..000000000
--- a/config-ui/src/pages/blueprint/create/components/step-2.tsx
+++ /dev/null
@@ -1,40 +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 { DataScope } from '@/plugins';
-
-import { useCreate } from '../context';
-
-import * as S from './styled';
-
-export const Step2 = () => {
- const { connections, onChangeConnections, onPrev, onNext } = useCreate();
-
- return (
- <S.Wrapper>
- <DataScope
- connections={connections}
- cancelBtnProps={{ text: 'Previous Step' }}
- submitBtnProps={{ text: 'Next Step' }}
- onCancel={onPrev}
- onSubmit={onChangeConnections}
- onNext={onNext}
- />
- </S.Wrapper>
- );
-};
diff --git a/config-ui/src/pages/blueprint/create/components/step-3.tsx b/config-ui/src/pages/blueprint/create/components/step-3.tsx
deleted file mode 100644
index be178feb2..000000000
--- a/config-ui/src/pages/blueprint/create/components/step-3.tsx
+++ /dev/null
@@ -1,33 +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 { Transformation } from '@/plugins';
-
-import { useCreate } from '../context';
-
-import * as S from './styled';
-
-export const Step3 = () => {
- const { connections, onChangeConnections, onPrev, onNext } = useCreate();
-
- return (
- <S.Wrapper>
- <Transformation connections={connections} onCancel={onPrev} onSubmit={onChangeConnections} onNext={onNext} />
- </S.Wrapper>
- );
-};
diff --git a/config-ui/src/pages/blueprint/create/components/step-4.tsx b/config-ui/src/pages/blueprint/create/components/step-4.tsx
deleted file mode 100644
index 7108c9216..000000000
--- a/config-ui/src/pages/blueprint/create/components/step-4.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.
- *
- */
-
-import { Button, Intent } from '@blueprintjs/core';
-
-import { Card, Divider } from '@/components';
-
-import { ModeEnum } from '../../types';
-import { SyncPolicy } from '../../components';
-
-import { useCreate } from '../context';
-
-import * as S from './styled';
-
-export const Step4 = () => {
- const {
- mode,
- isManual,
- cronConfig,
- skipOnFail,
- timeAfter,
- onChangeIsManual,
- onChangeCronConfig,
- onChangeSkipOnFail,
- onChangeTimeAfter,
- onPrev,
- onSave,
- onSaveAndRun,
- } = useCreate();
-
- return (
- <S.Wrapper>
- <Card>
- <h2>Set Sync Policy</h2>
- <Divider />
- <SyncPolicy
- isManual={isManual}
- cronConfig={cronConfig}
- skipOnFail={skipOnFail}
- showTimeFilter={mode === ModeEnum.normal}
- timeAfter={timeAfter}
- onChangeIsManual={onChangeIsManual}
- onChangeCronConfig={onChangeCronConfig}
- onChangeSkipOnFail={onChangeSkipOnFail}
- onChangeTimeAfter={onChangeTimeAfter}
- />
- </Card>
- <S.Btns>
- <Button intent={Intent.PRIMARY} outlined text="Previous Step" onClick={onPrev} />
- <div>
- <Button intent={Intent.PRIMARY} outlined text="Save Blueprint" onClick={onSave} />
- <Button intent={Intent.PRIMARY} text="Save and Run Now" onClick={onSaveAndRun} />
- </div>
- </S.Btns>
- </S.Wrapper>
- );
-};
diff --git a/config-ui/src/pages/blueprint/create/components/styled.ts b/config-ui/src/pages/blueprint/create/components/styled.ts
deleted file mode 100644
index 67066091d..000000000
--- a/config-ui/src/pages/blueprint/create/components/styled.ts
+++ /dev/null
@@ -1,101 +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 styled from 'styled-components';
-import { Colors } from '@blueprintjs/core';
-
-export const Wrapper = styled.div`
- margin-top: 36px;
-
- .card + .card {
- margin-top: 16px;
- }
-`;
-
-export const ConnectionList = styled.ul`
- padding: 12px;
-
- li {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 4px 0;
- border-bottom: 1px solid #f0f0f0;
-
- .name {
- font-weight: 600;
- }
-
- .status {
- display: flex;
- align-items: center;
-
- &.online {
- color: ${Colors.GREEN3};
- }
-
- &.offline {
- color: ${Colors.RED3};
- }
- }
- }
-`;
-
-export const Tips = styled.p`
- margin: 24px 0 0;
-
- span:last-child {
- color: #7497f7;
- cursor: pointer;
- }
-`;
-
-export const ConnectionColumn = styled.div`
- display: flex;
- align-items: center;
-
- img {
- margin-right: 4px;
- width: 20px;
- }
-`;
-
-export const ScopeColumn = styled.ul``;
-
-export const ScopeItem = styled.li`
- margin-bottom: 4px;
-
- &:last-child {
- margin-bottom: 0;
- }
-
- .bp4-button {
- margin-left: 6px;
- }
-`;
-
-export const Btns = styled.div`
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-top: 36px;
-
- .bp4-button + .bp4-button {
- margin-left: 8px;
- }
-`;
diff --git a/config-ui/src/pages/blueprint/create/context.tsx b/config-ui/src/pages/blueprint/create/context.tsx
deleted file mode 100644
index f2af24ded..000000000
--- a/config-ui/src/pages/blueprint/create/context.tsx
+++ /dev/null
@@ -1,183 +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, useMemo, useContext } from 'react';
-import { useHistory } from 'react-router-dom';
-import dayjs from 'dayjs';
-
-import { operator, formatTime } from '@/utils';
-
-import { ModeEnum, FromEnum } from '../types';
-import { validRawPlan } from '../utils';
-
-import type { ContextType } from './types';
-import * as API from './api';
-
-export const Context = React.createContext<ContextType>({
- step: 1,
-
- name: 'MY BLUEPRINT',
- mode: ModeEnum.normal,
- connections: [],
- rawPlan: JSON.stringify([[]], null, ' '),
- cronConfig: '0 0 * * *',
- isManual: false,
- skipOnFail: false,
- timeAfter: null,
-
- onPrev: () => {},
- onNext: () => {},
- onSave: () => {},
- onSaveAndRun: () => {},
-
- onChangeMode: () => {},
- onChangeName: () => {},
- onChangeConnections: () => {},
- onChangeRawPlan: () => {},
- onChangeCronConfig: () => {},
- onChangeIsManual: () => {},
- onChangeSkipOnFail: () => {},
- onChangeTimeAfter: () => {},
-});
-
-interface Props {
- from: FromEnum;
- projectName: string;
- children: React.ReactNode;
-}
-
-export const ContextProvider = ({ from, projectName, children }: Props) => {
- const [step, setStep] = useState(1);
-
- const [name, setName] = useState(
- from === FromEnum.project ? `${window.decodeURIComponent(projectName)}-BLUEPRINT` : 'MY BLUEPRINT',
- );
- const [mode, setMode] = useState<ModeEnum>(ModeEnum.normal);
- const [connections, setConnections] = useState<MixConnection[]>([]);
- const [rawPlan, setRawPlan] = useState(JSON.stringify([[]], null, ' '));
- const [cronConfig, setCronConfig] = useState('0 0 * * *');
- const [isManual, setIsManual] = useState(false);
- const [skipOnFail, setSkipOnFail] = useState(true);
- const [timeAfter, setTimeAfter] = useState<string | null>(
- formatTime(dayjs().subtract(6, 'month').startOf('day').toDate(), 'YYYY-MM-DD[T]HH:mm:ssZ'),
- );
-
- const history = useHistory();
-
- const payload = useMemo(() => {
- const params: any = {
- name,
- projectName: projectName ? window.decodeURIComponent(projectName) : '',
- mode,
- enable: true,
- cronConfig,
- isManual,
- skipOnFail,
- };
-
- if (mode === ModeEnum.normal) {
- params.settings = {
- version: '2.0.0',
- timeAfter,
- connections: connections.map((cs) => {
- return {
- plugin: cs.plugin,
- connectionId: cs.connectionId,
- scopes: cs.scope.map((sc) => ({
- id: `${sc.id}`,
- entities: sc.entities,
- })),
- };
- }),
- };
- }
-
- if (mode === ModeEnum.advanced) {
- params.plan = !validRawPlan(rawPlan) ? JSON.parse(rawPlan) : JSON.stringify([[]], null, ' ');
- params.settings = null;
- }
-
- return params;
- }, [projectName, name, mode, connections, rawPlan, cronConfig, isManual, skipOnFail, timeAfter]);
-
- const handleSaveAfter = (id: ID) => {
- const path =
- from === FromEnum.blueprint ? `/blueprints/${id}` : `/projects/${window.encodeURIComponent(projectName)}`;
-
- history.push(path);
- };
-
- const handleSave = async () => {
- const [success, res] = await operator(() => API.createBlueprint(payload));
-
- if (success) {
- handleSaveAfter(res.id);
- }
- };
-
- const hanldeSaveAndRun = async () => {
- const [success, res] = await operator(async () => {
- const res = await API.createBlueprint(payload);
- return await API.runBlueprint(res.id);
- });
-
- if (success) {
- handleSaveAfter(res.blueprintId);
- }
- };
-
- const handlePrev = () => setStep(step - 1);
- const handleNext = () => setStep(step + 1);
-
- return (
- <Context.Provider
- value={{
- step,
-
- mode,
- name,
- connections,
- rawPlan,
- cronConfig,
- isManual,
- skipOnFail,
- timeAfter,
-
- onPrev: handlePrev,
- onNext: handleNext,
- onSave: handleSave,
- onSaveAndRun: hanldeSaveAndRun,
-
- onChangeMode: setMode,
- onChangeName: setName,
- onChangeConnections: setConnections,
- onChangeRawPlan: setRawPlan,
- onChangeCronConfig: setCronConfig,
- onChangeIsManual: setIsManual,
- onChangeSkipOnFail: setSkipOnFail,
- onChangeTimeAfter: setTimeAfter,
- }}
- >
- {children}
- </Context.Provider>
- );
-};
-
-export const useCreate = () => {
- return useContext(Context);
-};
diff --git a/config-ui/src/pages/blueprint/create/index.tsx b/config-ui/src/pages/blueprint/create/index.tsx
deleted file mode 100644
index f9f788e31..000000000
--- a/config-ui/src/pages/blueprint/create/index.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.
- *
- */
-
-import { useMemo } from 'react';
-import { useParams } from 'react-router-dom';
-
-import { PageHeader, Workflow } from '@/components';
-
-import { ModeEnum, FromEnum } from '../types';
-
-import { ContextProvider, Context } from './context';
-import { Step1, Step2, Step3, Step4 } from './components';
-
-interface Props {
- from: FromEnum;
-}
-
-export const BlueprintCreatePage = ({ from }: Props) => {
- const { pname } = useParams<{ pname: string }>();
-
- const breadcrumbs = useMemo(
- () =>
- from === FromEnum.project
- ? [
- { name: 'Projects', path: '/projects' },
- { name: window.decodeURIComponent(pname), path: `/projects/${pname}` },
- {
- name: 'Create a Blueprint',
- path: `/projects/${pname}/create-blueprint`,
- },
- ]
- : [
- { name: 'Blueprints', path: '/blueprints' },
- { name: 'Create a Blueprint', path: '/blueprints/create' },
- ],
- [from, pname],
- );
-
- return (
- <ContextProvider from={from} projectName={pname}>
- <Context.Consumer>
- {({ step, mode }) => (
- <PageHeader breadcrumbs={breadcrumbs}>
- <Workflow
- steps={
- mode === ModeEnum.normal
- ? ['Add Data Connections', 'Set Data Scope', 'Add Transformation (Optional)', 'Set Sync Policy']
- : ['Create Advanced Configuration', 'Set Sync Policy']
- }
- activeStep={step}
- />
- {step === 1 && <Step1 from={from} />}
- {mode === ModeEnum.normal && step === 2 && <Step2 />}
- {step === 3 && <Step3 />}
- {((mode === ModeEnum.normal && step === 4) || (mode === ModeEnum.advanced && step === 2)) && <Step4 />}
- </PageHeader>
- )}
- </Context.Consumer>
- </ContextProvider>
- );
-};
diff --git a/config-ui/src/pages/blueprint/create/types.ts b/config-ui/src/pages/blueprint/create/types.ts
deleted file mode 100644
index 9717371d3..000000000
--- a/config-ui/src/pages/blueprint/create/types.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 { ModeEnum } from '../types';
-
-export type ContextType = {
- step: number;
-
- mode: ModeEnum;
- name: string;
- connections: MixConnection[];
- rawPlan: string;
- cronConfig: string;
- isManual: boolean;
- skipOnFail: boolean;
- timeAfter: string | null;
-
- onPrev: () => void;
- onNext: () => void;
-
- onSave: () => void;
- onSaveAndRun: () => void;
-
- onChangeMode: (mode: ModeEnum) => void;
- onChangeName: React.Dispatch<React.SetStateAction<string>>;
- onChangeConnections: React.Dispatch<React.SetStateAction<MixConnection[]>>;
- onChangeRawPlan: React.Dispatch<React.SetStateAction<string>>;
- onChangeCronConfig: React.Dispatch<React.SetStateAction<string>>;
- onChangeIsManual: React.Dispatch<React.SetStateAction<boolean>>;
- onChangeSkipOnFail: React.Dispatch<React.SetStateAction<boolean>>;
- onChangeTimeAfter: React.Dispatch<React.SetStateAction<string | null>>;
-};
diff --git a/config-ui/src/pages/blueprint/home/api.ts b/config-ui/src/pages/blueprint/home/api.ts
index b31204b0e..62e7c80ed 100644
--- a/config-ui/src/pages/blueprint/home/api.ts
+++ b/config-ui/src/pages/blueprint/home/api.ts
@@ -18,9 +18,23 @@
import { request } from '@/utils';
+import { BlueprintType } from '../types';
+
type GetBlueprintsParams = {
page: number;
pageSize: number;
};
-export const getBlueprints = (params: GetBlueprintsParams) => request('/blueprints', { data: params });
+type GetBlueprintResponse = {
+ blueprints: Array<BlueprintType>;
+ count: number;
+};
+
+export const getBlueprints = (params: GetBlueprintsParams): Promise<GetBlueprintResponse> =>
+ request('/blueprints', { data: params });
+
+export const createBlueprint = (payload: any) =>
+ request('/blueprints', {
+ method: 'post',
+ data: payload,
+ });
diff --git a/config-ui/src/pages/blueprint/home/index.tsx b/config-ui/src/pages/blueprint/home/index.tsx
index c70d1c4dc..80815526d 100644
--- a/config-ui/src/pages/blueprint/home/index.tsx
+++ b/config-ui/src/pages/blueprint/home/index.tsx
@@ -16,144 +16,261 @@
*
*/
-import React, { useMemo } from 'react';
-import { Link, useHistory } from 'react-router-dom';
-import { ButtonGroup, Button, Tag, Intent } from '@blueprintjs/core';
+import { useState, useMemo } from 'react';
+import { Link } from 'react-router-dom';
+import { ButtonGroup, Button, Tag, Intent, FormGroup, InputGroup, RadioGroup, Radio } from '@blueprintjs/core';
+import dayjs from 'dayjs';
-import { PageLoading, PageHeader, Table, ColumnType, IconButton, TextTooltip } from '@/components';
-import { getCron, getCronOptions } from '@/config';
-import { formatTime } from '@/utils';
+import { PageHeader, Table, IconButton, TextTooltip, Dialog } from '@/components';
+import { getCronOptions, cronPresets, getCron } from '@/config';
+import { useConnections, useRefreshData } from '@/hooks';
+import { formatTime, operator } from '@/utils';
-import type { BlueprintType } from '../types';
import { ModeEnum } from '../types';
-import { useHome } from './use-home';
+import * as API from './api';
import * as S from './styled';
export const BlueprintHomePage = () => {
- const history = useHistory();
+ const [type, setType] = useState('all');
+ const [version, setVersion] = useState(1);
+ const [isOpen, setIsOpen] = useState(false);
+ const [name, setName] = useState('');
+ const [mode, setMode] = useState(ModeEnum.normal);
+ const [saving, setSaving] = useState(false);
- const { loading, dataSource, type, onChangeType } = useHome();
+ const { onGet } = useConnections();
+ const { ready, data } = useRefreshData(() => API.getBlueprints({ page: 1, pageSize: 200 }), [version]);
- const options = useMemo(() => getCronOptions(), []);
-
- const columns = useMemo(
+ const [options, presets] = useMemo(() => [getCronOptions(), cronPresets.map((preset) => preset.config)], []);
+ const dataSource = useMemo(
() =>
- [
- {
- title: 'Blueprint Name',
- dataIndex: 'name',
- key: 'name',
- ellipsis: true,
- },
- {
- title: 'Data Connections',
- key: 'connections',
- align: 'center',
- render: (_, row) => {
- if (row.mode === ModeEnum.advanced) {
- return 'Advanced Mode';
- }
- return row.settings.connections.map((cs) => cs.plugin).join(',');
- },
- },
- {
- title: 'Frequency',
- key: 'frequency',
- width: 100,
- align: 'center',
- render: (_, row) => {
- const cron = getCron(row.isManual, row.cronConfig);
- return cron.label;
- },
- },
- {
- title: 'Next Run Time',
- key: 'nextRunTime',
- width: 200,
- align: 'center',
- render: (_, row) => {
- const cron = getCron(row.isManual, row.cronConfig);
- return formatTime(cron.nextTime);
- },
- },
- {
- title: 'Project',
- dataIndex: 'projectName',
- key: 'project',
- align: 'center',
- render: (val) =>
- val ? (
- <Link to={`/projects/${window.encodeURIComponent(val)}`}>
- <TextTooltip content={val}>{val}</TextTooltip>
- </Link>
- ) : (
- val
- ),
- },
- {
- title: 'Status',
- dataIndex: 'enable',
- key: 'enable',
- align: 'center',
- width: 100,
- render: (val) => (
- <Tag minimal intent={val ? Intent.SUCCESS : Intent.DANGER}>
- {val ? 'Enabled' : 'Disabled'}
- </Tag>
- ),
- },
- {
- title: '',
- dataIndex: 'id',
- key: 'action',
- width: 100,
- align: 'center',
- render: (val) => (
- <IconButton icon="cog" tooltip="Detail" onClick={() => history.push(`/blueprints/${val}`)} />
- ),
- },
- ] as ColumnType<BlueprintType>,
- [],
+ (data?.blueprints ?? [])
+ .filter((it) => {
+ switch (type) {
+ case 'all':
+ return true;
+ case 'manual':
+ return it.isManual;
+ case 'custom':
+ return !presets.includes(it.cronConfig);
+ default:
+ return !it.isManual && it.cronConfig === type;
+ }
+ })
+ .map((it) => {
+ const connections = it.settings.connections
+ .filter((cs) => cs.plugin !== 'webhook')
+ .map((cs) => onGet(`${cs.plugin}-${cs.connectionId}`));
+ return {
+ ...it,
+ connections: connections.map((cs) => cs.name),
+ };
+ }),
+ [data, type],
);
- const handleCreate = () => history.push('/blueprints/create');
+ const handleShowDialog = () => setIsOpen(true);
+ const handleHideDialog = () => {
+ setName('');
+ setMode(ModeEnum.normal);
+ setIsOpen(false);
+ };
+
+ const handleCreate = async () => {
+ const payload: any = {
+ name,
+ mode,
+ enable: true,
+ cronConfig: presets[0],
+ isManual: false,
+ skipOnFail: true,
+ };
+
+ if (mode === ModeEnum.normal) {
+ payload.settings = {
+ version: '2.0.0',
+ timeAfter: formatTime(dayjs().subtract(6, 'month').startOf('day').toDate(), 'YYYY-MM-DD[T]HH:mm:ssZ'),
+ connections: [],
+ };
+ }
+
+ if (mode === ModeEnum.advanced) {
+ payload.settings = null;
+ payload.plan = [[]];
+ }
- if (loading) {
- return <PageLoading />;
- }
+ const [success] = await operator(() => API.createBlueprint(payload), {
+ setOperating: setSaving,
+ });
+
+ if (success) {
+ handleHideDialog();
+ setVersion((v) => v + 1);
+ }
+ };
return (
- <PageHeader breadcrumbs={[{ name: 'Blueprints', path: '/blueprints' }]}>
+ <PageHeader
+ breadcrumbs={[
+ { name: 'Advanced', path: '/blueprints' },
+ { name: 'Blueprints', path: '/blueprints' },
+ ]}
+ >
<S.Wrapper>
+ <p>This is a complete list of all Blueprints you have created, whether they belong to Projects or not.</p>
<div className="action">
<ButtonGroup>
- <Button
- intent={type === 'all' ? Intent.PRIMARY : Intent.NONE}
- text="All"
- onClick={() => onChangeType('all')}
- />
+ <Button intent={type === 'all' ? Intent.PRIMARY : Intent.NONE} text="All" onClick={() => setType('all')} />
{options.map(({ label, value }) => (
<Button
key={value}
intent={type === value ? Intent.PRIMARY : Intent.NONE}
text={label}
- onClick={() => onChangeType(value)}
+ onClick={() => setType(value)}
/>
))}
</ButtonGroup>
- <Button icon="plus" intent={Intent.PRIMARY} text="New Blueprint" onClick={handleCreate} />
+ <Button icon="plus" intent={Intent.PRIMARY} text="New Blueprint" onClick={handleShowDialog} />
</div>
<Table
- columns={columns}
+ loading={!ready}
+ columns={[
+ {
+ title: 'Blueprint Name',
+ dataIndex: 'name',
+ key: 'name',
+ ellipsis: true,
+ },
+ {
+ title: 'Data Connections',
+ dataIndex: ['mode', 'connections'],
+ key: 'connections',
+ align: 'center',
+ render: ({ mode, connections }) => {
+ if (mode === ModeEnum.advanced) {
+ return 'Advanced Mode';
+ }
+ return connections.join(',');
+ },
+ },
+ {
+ title: 'Frequency',
+ dataIndex: ['isManual', 'cronConfig'],
+ key: 'frequency',
+ width: 100,
+ align: 'center',
+ render: ({ isManual, cronConfig }) => {
+ const cron = getCron(isManual, cronConfig);
+ return cron.label;
+ },
+ },
+ {
+ title: 'Next Run Time',
+ dataIndex: ['isManual', 'cronConfig'],
+ key: 'nextRunTime',
+ width: 200,
+ align: 'center',
+ render: ({ isManual, cronConfig }) => {
+ const cron = getCron(isManual, cronConfig);
+ return formatTime(cron.nextTime);
+ },
+ },
+ {
+ title: 'Project',
+ dataIndex: 'projectName',
+ key: 'project',
+ align: 'center',
+ render: (val) =>
+ val ? (
+ <Link to={`/projects/${window.encodeURIComponent(val)}`}>
+ <TextTooltip content={val}>{val}</TextTooltip>
+ </Link>
+ ) : (
+ val
+ ),
+ },
+ {
+ title: 'Status',
+ dataIndex: 'enable',
+ key: 'enable',
+ align: 'center',
+ width: 100,
+ render: (val) => (
+ <Tag minimal intent={val ? Intent.SUCCESS : Intent.DANGER}>
+ {val ? 'Enabled' : 'Disabled'}
+ </Tag>
+ ),
+ },
+ {
+ title: '',
+ dataIndex: 'id',
+ key: 'action',
+ width: 100,
+ align: 'center',
+ render: (val) => (
+ <Link to={`/blueprints/${val}`}>
+ <IconButton icon="cog" tooltip="Detail" />
+ </Link>
+ ),
+ },
+ ]}
dataSource={dataSource}
noData={{
text: 'There is no Blueprint yet. Please add a new Blueprint here or from a Project.',
btnText: 'New Blueprint',
- onCreate: handleCreate,
+ onCreate: handleShowDialog,
}}
/>
</S.Wrapper>
+ <Dialog
+ style={{ width: 820 }}
+ isOpen={isOpen}
+ title="Create a New Blueprint"
+ okText="Save"
+ okDisabled={!name}
+ okLoading={saving}
+ onOk={handleCreate}
+ onCancel={handleHideDialog}
+ >
+ <S.DialogWrapper>
+ <FormGroup
+ label={<S.Label>Blueprint Name</S.Label>}
+ subLabel={
+ <S.LabelDescription>
+ Give your Blueprint a unique name to help you identify it in the future.
+ </S.LabelDescription>
+ }
+ labelInfo={<S.LabelInfo>*</S.LabelInfo>}
+ >
+ <InputGroup
+ style={{ width: 386 }}
+ placeholder="Your Blueprint Name"
+ value={name}
+ onChange={(e) => setName(e.target.value)}
+ />
+ </FormGroup>
+ <FormGroup
+ label={<S.Label>Blueprint Mode</S.Label>}
+ subLabel={
+ <S.LabelDescription>
+ Normal Mode is usually adequate for most usages. But if you need to customize how tasks are executed in
+ the Blueprint, please use Advanced Mode to create a Blueprint.
+ </S.LabelDescription>
+ }
+ labelInfo={<S.LabelInfo>*</S.LabelInfo>}
+ >
+ <RadioGroup
+ inline
+ selectedValue={mode}
+ onChange={(e) => setMode((e.target as HTMLInputElement).value as ModeEnum)}
+ >
+ <Radio value={ModeEnum.normal}>Normal Mode</Radio>
+ <Radio value={ModeEnum.advanced}>Advanced Mode</Radio>
+ </RadioGroup>
+ </FormGroup>
+ </S.DialogWrapper>
+ </Dialog>
</PageHeader>
);
};
diff --git a/config-ui/src/pages/blueprint/home/styled.ts b/config-ui/src/pages/blueprint/home/styled.ts
index 7406e280d..1cf236862 100644
--- a/config-ui/src/pages/blueprint/home/styled.ts
+++ b/config-ui/src/pages/blueprint/home/styled.ts
@@ -26,3 +26,23 @@ export const Wrapper = styled.div`
margin-bottom: 16px;
}
`;
+
+export const DialogWrapper = styled.div`
+ .bp4-form-group + .bp4-form-group {
+ margin-top: 40px;
+ margin-bottom: 0;
+ }
+`;
+
+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;
+`;
diff --git a/config-ui/src/pages/blueprint/home/use-home.ts b/config-ui/src/pages/blueprint/home/use-home.ts
deleted file mode 100644
index 0295666f8..000000000
--- a/config-ui/src/pages/blueprint/home/use-home.ts
+++ /dev/null
@@ -1,79 +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 { useState, useEffect, useMemo } from 'react';
-
-import { cronPresets } from '@/config';
-
-import type { BlueprintType } from '../types';
-
-import * as API from './api';
-
-export const useHome = () => {
- const [loading, setLoading] = useState(false);
- const [blueprints, setBlueprints] = useState<BlueprintType[]>([]);
- const [dataSource, setDataSource] = useState<BlueprintType[]>([]);
- const [type, setType] = useState('all');
-
- const presets = cronPresets.map((preset) => preset.config);
-
- const getBlueprints = async () => {
- setLoading(true);
- try {
- const res = await API.getBlueprints({
- page: 1,
- pageSize: 200,
- });
- setBlueprints(res.blueprints);
- } finally {
- setLoading(false);
- }
- };
-
- useEffect(() => {
- getBlueprints();
- }, []);
-
- useEffect(() => {
- setDataSource(
- blueprints.filter((bp) => {
- switch (type) {
- case 'all':
- return true;
- case 'manual':
- return bp.isManual;
- case 'custom':
- return !presets.includes(bp.cronConfig);
- default:
- return !bp.isManual && bp.cronConfig === type;
- }
- }),
- );
- }, [blueprints, type]);
-
- return useMemo(
- () => ({
- loading,
- blueprints,
- dataSource,
- type,
- onChangeType: setType,
- }),
- [loading, blueprints, dataSource, type],
- );
-};
diff --git a/config-ui/src/pages/blueprint/index.ts b/config-ui/src/pages/blueprint/index.ts
index cd8a30e1f..68aec9419 100644
--- a/config-ui/src/pages/blueprint/index.ts
+++ b/config-ui/src/pages/blueprint/index.ts
@@ -18,7 +18,6 @@
export * from './types';
export * from './home';
-export * from './create';
export * from './detail';
export * from './connection-add';
export * from './connection-detail';
diff --git a/config-ui/src/pages/index.ts b/config-ui/src/pages/index.ts
index bcf789c04..557b2860e 100644
--- a/config-ui/src/pages/index.ts
+++ b/config-ui/src/pages/index.ts
@@ -16,9 +16,10 @@
*
*/
-export * from './project';
-export * from './connection';
export * from './blueprint';
-export * from './pipeline';
-export * from './offline';
+export * from './connection';
export * from './db-migrate';
+export * from './login';
+export * from './offline';
+export * from './pipeline';
+export * from './project';
diff --git a/config-ui/src/pages/blueprint/create/components/index.ts b/config-ui/src/pages/login/index.ts
similarity index 88%
copy from config-ui/src/pages/blueprint/create/components/index.ts
copy to config-ui/src/pages/login/index.ts
index 6e5963478..a365b1ed2 100644
--- a/config-ui/src/pages/blueprint/create/components/index.ts
+++ b/config-ui/src/pages/login/index.ts
@@ -16,7 +16,4 @@
*
*/
-export * from './step-1';
-export * from './step-2';
-export * from './step-3';
-export * from './step-4';
+export * from './login';
diff --git a/config-ui/src/pages/project/home/api.ts b/config-ui/src/pages/project/home/api.ts
index 0cb21da63..2ac30a8a0 100644
--- a/config-ui/src/pages/project/home/api.ts
+++ b/config-ui/src/pages/project/home/api.ts
@@ -23,7 +23,15 @@ type GetProjectsParams = {
pageSize: number;
};
-export const getProjects = (params: GetProjectsParams) => request('/projects', { data: params });
+type GetProjectsResponse = {
+ projects: Array<{
+ name: string;
+ }>;
+ counts: number;
+};
+
+export const getProjects = (params: GetProjectsParams): Promise<GetProjectsResponse> =>
+ request('/projects', { data: params });
type CreateProjectPayload = {
name: string;
@@ -40,3 +48,9 @@ export const createProject = (payload: CreateProjectPayload) =>
method: 'post',
data: payload,
});
+
+export const createBlueprint = (payload: any) =>
+ request('/blueprints', {
+ method: 'post',
+ data: payload,
+ });
diff --git a/config-ui/src/pages/project/home/index.tsx b/config-ui/src/pages/project/home/index.tsx
index 844a05662..ffc287fd4 100644
--- a/config-ui/src/pages/project/home/index.tsx
+++ b/config-ui/src/pages/project/home/index.tsx
@@ -16,82 +16,118 @@
*
*/
-import React, { useMemo, useState } from 'react';
+import { useState, useMemo } from 'react';
import { Link, useHistory } from 'react-router-dom';
-import { Button, InputGroup, Checkbox, Intent } from '@blueprintjs/core';
+import { Button, InputGroup, Checkbox, Intent, FormGroup } from '@blueprintjs/core';
+import dayjs from 'dayjs';
-import { PageHeader, Table, ColumnType, Dialog, IconButton } from '@/components';
+import { PageHeader, Table, Dialog, IconButton, toast } from '@/components';
+import { cronPresets } from '@/config';
+import { useRefreshData } from '@/hooks';
+import { formatTime, operator } from '@/utils';
-import { useProject } from './use-project';
-import * as S from './styled';
+import { encodeName } from '../utils';
+import { ModeEnum } from '../../blueprint';
-type ProjectItem = {
- name: string;
-};
+import * as API from './api';
+import * as S from './styled';
export const ProjectHomePage = () => {
+ const [version, setVersion] = useState(1);
const [isOpen, setIsOpen] = useState(false);
const [name, setName] = useState('');
const [enableDora, setEnableDora] = useState(true);
+ const [saving, setSaving] = useState(false);
+
+ const { ready, data } = useRefreshData(() => API.getProjects({ page: 1, pageSize: 200 }), [version]);
const history = useHistory();
+ const dataSource = useMemo(() => data?.projects ?? [], [data]);
+ const presets = useMemo(() => cronPresets.map((preset) => preset.config), []);
+
const handleShowDialog = () => setIsOpen(true);
const handleHideDialog = () => {
setIsOpen(false);
setName('');
+ setEnableDora(true);
};
- const { loading, operating, projects, onSave } = useProject<ProjectItem>({
- name,
- enableDora,
- onHideDialog: handleHideDialog,
- });
+ const handleCreate = async () => {
+ if (!/^(\w|-|\/)+$/.test(name)) {
+ toast.error('Please enter alphanumeric or underscore');
+ return;
+ }
- const columns = useMemo(
- () =>
- [
- {
- title: 'Project Name',
- dataIndex: 'name',
- key: 'name',
- render: (name: string) => (
- <Link to={`/projects/${window.encodeURIComponent(name)}`} style={{ color: '#292b3f' }}>
- {name}
- </Link>
- ),
- },
- {
- title: '',
- dataIndex: 'name',
- key: 'action',
- width: 100,
- align: 'center',
- render: (name: any) => (
- <IconButton
- icon="cog"
- tooltip="Detail"
- onClick={() => history.push(`/projects/${window.encodeURIComponent(name)}`)}
- />
- ),
- },
- ] as ColumnType<ProjectItem>,
- [],
- );
+ const [success] = await operator(
+ async () => {
+ await API.createProject({
+ name: encodeName(name),
+ description: '',
+ metrics: [
+ {
+ pluginName: 'dora',
+ pluginOption: '',
+ enable: enableDora,
+ },
+ ],
+ });
+ return API.createBlueprint({
+ name: `${name}-Blueprint`,
+ projectName: name,
+ mode: ModeEnum.normal,
+ enable: true,
+ cronConfig: presets[0],
+ isManual: false,
+ skipOnFail: true,
+ settings: {
+ version: '2.0.0',
+ timeAfter: formatTime(dayjs().subtract(6, 'month').startOf('day').toDate(), 'YYYY-MM-DD[T]HH:mm:ssZ'),
+ connections: [],
+ },
+ });
+ },
+ {
+ setOperating: setSaving,
+ },
+ );
+
+ if (success) {
+ handleHideDialog();
+ setVersion((v) => v + 1);
+ }
+ };
return (
<PageHeader
breadcrumbs={[{ name: 'Projects', path: '/projects' }]}
- extra={
- projects.length ? (
- <Button intent={Intent.PRIMARY} icon="plus" text="New Project" onClick={handleShowDialog} />
- ) : null
- }
+ extra={<Button intent={Intent.PRIMARY} icon="plus" text="New Project" onClick={handleShowDialog} />}
>
<Table
- loading={loading}
- columns={columns}
- dataSource={projects}
+ loading={!ready}
+ columns={[
+ {
+ title: 'Project Name',
+ dataIndex: 'name',
+ key: 'name',
+ render: (name: string) => (
+ <Link to={`/projects/${encodeName(name)}`} style={{ color: '#292b3f' }}>
+ {name}
+ </Link>
+ ),
+ },
+ {
+ title: '',
+ dataIndex: 'name',
+ key: 'action',
+ width: 100,
+ align: 'center',
+ render: (name: any) => (
+ <IconButton icon="cog" tooltip="Detail" onClick={() => history.push(`/projects/${encodeName(name)}`)} />
+ ),
+ },
+ ]}
+ dataSource={dataSource}
noData={{
text: 'Add new projects to see engineering metrics based on projects.',
btnText: 'New Project',
@@ -101,41 +137,49 @@ export const ProjectHomePage = () => {
<Dialog
isOpen={isOpen}
title="Create a New Project"
- style={{
- top: -100,
- width: 820,
- }}
+ style={{ width: 820 }}
okText="Save"
okDisabled={!name}
- okLoading={operating}
+ okLoading={saving}
+ onOk={handleCreate}
onCancel={handleHideDialog}
- onOk={onSave}
>
- <S.DialogInner>
- <div className="block">
- <h3>Project Name *</h3>
- <p>Give your project a unique name with letters, numbers, -, _ or /</p>
- <InputGroup placeholder="Your Project Name" value={name} onChange={(e) => setName(e.target.value)} />
- </div>
- <div className="block">
- <h3>Project Settings</h3>
- <div className="checkbox">
- <Checkbox
- label="Enable DORA Metrics"
- checked={enableDora}
- onChange={(e) => setEnableDora((e.target as HTMLInputElement).checked)}
- />
- <p>
+ <S.DialogWrapper>
+ <FormGroup
+ label={<S.Label>Project Name</S.Label>}
+ subLabel={
+ <S.LabelDescription>Give your project a unique name with letters, numbers, -, _ or /</S.LabelDescription>
+ }
+ labelInfo={<S.LabelInfo>*</S.LabelInfo>}
+ >
+ <InputGroup
+ style={{ width: 386 }}
+ placeholder="Your Project Name"
+ value={name}
+ onChange={(e) => setName(e.target.value)}
+ />
+ </FormGroup>
+
+ <FormGroup
+ label={<S.Label>Project Settings</S.Label>}
+ subLabel={
+ <S.LabelDescription>
<a href="https://devlake.apache.org/docs/DORA/" rel="noreferrer" target="_blank">
DORA metrics
</a>
<span style={{ marginLeft: 4 }}>
are four widely-adopted metrics for measuring software delivery performance.
</span>
- </p>
- </div>
- </div>
- </S.DialogInner>
+ </S.LabelDescription>
+ }
+ >
+ <Checkbox
+ label="Enable DORA Metrics"
+ checked={enableDora}
+ onChange={(e) => setEnableDora((e.target as HTMLInputElement).checked)}
+ />
+ </FormGroup>
+ </S.DialogWrapper>
</Dialog>
</PageHeader>
);
diff --git a/config-ui/src/pages/project/home/styled.ts b/config-ui/src/pages/project/home/styled.ts
index 92f71638e..1449bc5ca 100644
--- a/config-ui/src/pages/project/home/styled.ts
+++ b/config-ui/src/pages/project/home/styled.ts
@@ -18,27 +18,22 @@
import styled from 'styled-components';
-export const DialogInner = styled.div`
- .block + .block {
- margin-top: 24px;
- }
-
- .bp4-input-group {
- width: 386px;
+export const DialogWrapper = styled.div`
+ .bp4-form-group + .bp4-form-group {
+ margin-top: 40px;
+ margin-bottom: 0;
}
+`;
- .checkbox {
- display: flex;
- margin-top: 8px;
+export const Label = styled.label`
+ font-size: 16px;
+ font-weight: 600;
+`;
- & > .bp4-control {
- margin: 0;
- }
+export const LabelInfo = styled.i`
+ color: #ff8b8b;
+`;
- & > p {
- display: flex;
- align-items: center;
- margin: 0 0 0 16px;
- }
- }
+export const LabelDescription = styled.p`
+ margin: 0;
`;
diff --git a/config-ui/src/pages/project/home/use-project.ts b/config-ui/src/pages/project/home/use-project.ts
deleted file mode 100644
index b241de813..000000000
--- a/config-ui/src/pages/project/home/use-project.ts
+++ /dev/null
@@ -1,92 +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 { useState, useEffect, useMemo } from 'react';
-
-import { toast } from '@/components';
-import { operator } from '@/utils';
-
-import * as API from './api';
-
-interface Props {
- name: string;
- enableDora: boolean;
- onHideDialog: () => void;
-}
-
-export const useProject = <T>({ name, enableDora, onHideDialog }: Props) => {
- const [loading, setLoading] = useState(false);
- const [operating, setOperating] = useState(false);
- const [projects, setProjects] = useState<T[]>([]);
-
- const getProjects = async () => {
- setLoading(true);
- try {
- const res = await API.getProjects({ page: 1, pageSize: 200 });
- setProjects(
- res.projects.map((it: any) => ({
- name: it.name,
- })),
- );
- } finally {
- setLoading(false);
- }
- };
-
- useEffect(() => {
- getProjects();
- }, []);
-
- const handleSave = async () => {
- if (!/^(\w|-|\/)+$/.test(name)) {
- toast.error('Please enter alphanumeric or underscore');
- return;
- }
-
- const payload = {
- name,
- description: '',
- metrics: [
- {
- pluginName: 'dora',
- pluginOption: '',
- enable: enableDora,
- },
- ],
- };
-
- const [success] = await operator(() => API.createProject(payload), {
- setOperating,
- });
-
- if (success) {
- onHideDialog();
- getProjects();
- }
- };
-
- return useMemo(
- () => ({
- loading,
- operating,
- projects,
- onSave: handleSave,
- }),
- [loading, operating, projects, name, enableDora],
- );
-};
diff --git a/config-ui/src/pages/blueprint/create/components/index.ts b/config-ui/src/pages/project/utils.ts
similarity index 83%
rename from config-ui/src/pages/blueprint/create/components/index.ts
rename to config-ui/src/pages/project/utils.ts
index 6e5963478..0dd911bd3 100644
--- a/config-ui/src/pages/blueprint/create/components/index.ts
+++ b/config-ui/src/pages/project/utils.ts
@@ -16,7 +16,6 @@
*
*/
-export * from './step-1';
-export * from './step-2';
-export * from './step-3';
-export * from './step-4';
+export const decodeName = (name: string) => window.decodeURIComponent(name);
+
+export const encodeName = (name: string) => window.encodeURIComponent(name);
diff --git a/config-ui/src/utils/index.ts b/config-ui/src/utils/index.ts
index 472050b64..21c45e9f0 100644
--- a/config-ui/src/utils/index.ts
+++ b/config-ui/src/utils/index.ts
@@ -16,6 +16,7 @@
*
*/
-export * from './request';
+export * from './history';
export * from './operator';
+export * from './request';
export * from './time';