From ef64449a831d6f60b8d13162706add85df389fe7 Mon Sep 17 00:00:00 2001 From: tingfuyeh Date: Thu, 2 Dec 2021 10:26:47 +0800 Subject: [PATCH] feat: namespace create support --- web/.prettierrc | 12 +- web/src/app.tsx | 56 ++----- web/src/assets/img/namespace.svg | 1 + web/src/menu.ts | 9 +- web/src/polaris/namespace/Page.tsx | 63 +++++++ web/src/polaris/namespace/PageDuck.ts | 157 ++++++++++++++++++ web/src/polaris/namespace/getColumns.tsx | 50 ++++++ web/src/polaris/namespace/model.ts | 70 ++++++++ .../polaris/namespace/operation/Create.tsx | 55 ++++++ .../polaris/namespace/operation/CreateDuck.ts | 105 ++++++++++++ web/src/polaris/service/model.ts | 10 +- web/src/polaris/service/types.ts | 2 + 12 files changed, 530 insertions(+), 60 deletions(-) create mode 100644 web/src/assets/img/namespace.svg create mode 100644 web/src/polaris/namespace/Page.tsx create mode 100644 web/src/polaris/namespace/PageDuck.ts create mode 100644 web/src/polaris/namespace/getColumns.tsx create mode 100644 web/src/polaris/namespace/model.ts create mode 100644 web/src/polaris/namespace/operation/Create.tsx create mode 100644 web/src/polaris/namespace/operation/CreateDuck.ts diff --git a/web/.prettierrc b/web/.prettierrc index 471a02aa..f208a235 100644 --- a/web/.prettierrc +++ b/web/.prettierrc @@ -1,7 +1,7 @@ { - "semi": false, - "singleQuote": true, - "printWidth": 120, - "trailingComma": "all", - "jsxSingleQuote": true -} \ No newline at end of file + "semi": true, + "singleQuote": false, + "printWidth": 120, + "trailingComma": "all", + "jsxSingleQuote": false +} diff --git a/web/src/app.tsx b/web/src/app.tsx index 376b7f1f..863ab985 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -6,6 +6,10 @@ import ServicePage from "@src/polaris/service/Page"; import ServicePageDuck from "@src/polaris/service/PageDuck"; const Service = connectWithDuck(ServicePage, ServicePageDuck); +import NamespacePage from "@src/polaris/namespace/Page"; +import NamespacePageDuck from "@src/polaris/namespace/PageDuck"; +const Namespace = connectWithDuck(NamespacePage, NamespacePageDuck); + import ServiceDetailPage from "@src/polaris/service/detail/Page"; import ServiceDetailDuck from "@src/polaris/service/detail/PageDuck"; const ServiceDetail = connectWithDuck(ServiceDetailPage, ServiceDetailDuck); @@ -24,23 +28,14 @@ import { MenuConfig } from "./menu"; import { connectWithDuck } from "./polaris/common/helpers"; import insertCSS from "./polaris/common/helpers/insertCSS"; import MonitorPage from "@src/polaris/monitor/Page"; -import { - CircuitBreakerMonitorDuck, - RouteMonitorDuck, - RatelimitMonitorDuck, -} from "@src/polaris/monitor/PageDuck"; -const CircuitBreakerMonitor = connectWithDuck( - MonitorPage, - CircuitBreakerMonitorDuck -); +import { CircuitBreakerMonitorDuck, RouteMonitorDuck, RatelimitMonitorDuck } from "@src/polaris/monitor/PageDuck"; +const CircuitBreakerMonitor = connectWithDuck(MonitorPage, CircuitBreakerMonitorDuck); const RouteMonitor = connectWithDuck(MonitorPage, RouteMonitorDuck); const RatelimitMonitor = connectWithDuck(MonitorPage, RatelimitMonitorDuck); export default function root() { const history = useHistory(); - const [selected, setSelected] = React.useState( - history.location.pathname.match(/^\/(\w+)/)?.[1] - ); + const [selected, setSelected] = React.useState(history.location.pathname.match(/^\/(\w+)/)?.[1]); const getMenuItemProps = (id) => ({ selected: selected === id, onClick: () => { @@ -55,11 +50,7 @@ export default function root() { left={ <> - logo + logo @@ -83,12 +74,7 @@ export default function root() { return null; } - return ( - - ); + return ; })} ); @@ -96,36 +82,22 @@ export default function root() { if (typeof MenuConfig[item] !== "object") { return; } - return ( - - ); + return ; })} + - - + + - - + diff --git a/web/src/assets/img/namespace.svg b/web/src/assets/img/namespace.svg new file mode 100644 index 00000000..5f42874a --- /dev/null +++ b/web/src/assets/img/namespace.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/menu.ts b/web/src/menu.ts index c9a6ac95..c9e5f906 100644 --- a/web/src/menu.ts +++ b/web/src/menu.ts @@ -7,6 +7,10 @@ export const MenuConfig = { title: "服务列表", icon: ["/static/img/service.svg", "/static/img/service.svg"], }, + namespace: { + title: "命名空间", + icon: ["/static/img/namespace.svg", "/static/img/namespace.svg"], + }, }, observability: { isGroup: true, @@ -17,10 +21,7 @@ export const MenuConfig = { }, "circuitBreaker-monitor": { title: "熔断监控", - icon: [ - "/static/img/circuit-monitor.svg", - "/static/img/circuit-monitor.svg", - ], + icon: ["/static/img/circuit-monitor.svg", "/static/img/circuit-monitor.svg"], }, "ratelimit-monitor": { title: "限流监控", diff --git a/web/src/polaris/namespace/Page.tsx b/web/src/polaris/namespace/Page.tsx new file mode 100644 index 00000000..8770360f --- /dev/null +++ b/web/src/polaris/namespace/Page.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { DuckCmpProps, memorize } from "saga-duck"; +import NamespaceDuck from "./PageDuck"; +import getColumns from "./getColumns"; +import insertCSS from "../common/helpers/insertCSS"; +import { Justify, H3, Table, Button, SearchBox, Card } from "tea-component"; +import GridPageGrid from "../common/duckComponents/GridPageGrid"; +import GridPagePagination from "../common/duckComponents/GridPagePagination"; +import BasicLayout from "../common/components/BaseLayout"; + +insertCSS( + "service", + ` +.justify-search{ + margin-right:20px +} +.justify-button{ + vertical-align: bottom +} +`, +); +const getHandlers = memorize(({ creators }: NamespaceDuck, dispatch) => ({ + inputKeyword: (keyword) => dispatch(creators.inputKeyword(keyword)), + search: (keyword) => dispatch(creators.search(keyword)), + clearKeyword: () => dispatch(creators.inputKeyword("")), + reload: () => dispatch(creators.reload()), + create: () => dispatch(creators.create()), +})); +export default function ServicePage(props: DuckCmpProps) { + const { duck, store, dispatch } = props; + const { selectors } = duck; + const columns = React.useMemo(() => getColumns(props), []); + const handlers = getHandlers(props); + return ( + }> + + + {"新建"} + + } + right={ + <> + + + + } + /> + + + + + + + ); +} diff --git a/web/src/polaris/namespace/PageDuck.ts b/web/src/polaris/namespace/PageDuck.ts new file mode 100644 index 00000000..736907ad --- /dev/null +++ b/web/src/polaris/namespace/PageDuck.ts @@ -0,0 +1,157 @@ +import { createToPayload, reduceFromPayload } from "saga-duck"; +import { takeLatest } from "redux-saga-catch"; +import Create from "./operation/Create"; +import CreateDuck from "./operation/CreateDuck"; +import { put, select } from "redux-saga/effects"; +import GridPageDuck, { Filter as BaseFilter } from "../common/ducks/GridPage"; +import { ComposedId } from "../service/detail/types"; +import { resolvePromise } from "saga-duck/build/helper"; +import { showDialog } from "../common/helpers/showDialog"; +import { Modal, notification } from "tea-component"; +import { Namespace } from "../service/types"; +import { deleteNamespace, describeComplicatedNamespaces } from "./model"; + +export interface NamespaceItem extends Namespace { + id: string; +} +export default class ServicePageDuck extends GridPageDuck { + Filter: BaseFilter; + Item: NamespaceItem; + get baseUrl() { + return ""; + } + get quickTypes() { + enum Types { + EDIT, + REMOVE, + CREATE, + LOAD, + SET_COMPOSE_ID, + } + return { + ...super.quickTypes, + ...Types, + }; + } + get initialFetch() { + return true; + } + get recordKey() { + return "id"; + } + get watchTypes() { + return [...super.watchTypes, this.types.SEARCH, this.types.SET_COMPOSE_ID]; + } + get params() { + return [...super.params]; + } + get quickDucks() { + return { + ...super.quickDucks, + }; + } + get reducers() { + const { types } = this; + return { + ...super.reducers, + composedId: reduceFromPayload(types.SET_COMPOSE_ID, {} as ComposedId), + }; + } + get creators() { + const { types } = this; + return { + ...super.creators, + edit: createToPayload(types.EDIT), + remove: createToPayload(types.REMOVE), + create: createToPayload(types.CREATE), + load: (composedId, data) => ({ + type: types.LOAD, + payload: { composedId, data }, + }), + }; + } + get rawSelectors() { + type State = this["State"]; + return { + ...super.rawSelectors, + filter: (state: State) => ({ + page: state.page, + count: state.count, + keyword: state.keyword, + }), + }; + } + + *saga() { + const { types, creators, selectors } = this; + yield* super.saga(); + yield takeLatest(types.LOAD, function* (action) { + const { composedId } = action.payload; + yield put({ type: types.SET_COMPOSE_ID, payload: composedId }); + }); + yield takeLatest(types.CREATE, function* () { + const res = yield* resolvePromise( + new Promise((resolve) => { + showDialog(Create, CreateDuck, function* (duck: CreateDuck) { + try { + resolve(yield* duck.execute({}, { isModify: false })); + } finally { + resolve(false); + } + }); + }), + ); + if (res) { + yield put(creators.reload()); + } + }); + yield takeLatest(types.EDIT, function* (action) { + const data = action.payload; + const res = yield* resolvePromise( + new Promise((resolve) => { + showDialog(Create, CreateDuck, function* (duck: CreateDuck) { + try { + resolve(yield* duck.execute(data, { isModify: true })); + } finally { + resolve(false); + } + }); + }), + ); + if (res) { + yield put(creators.reload()); + } + }); + yield takeLatest(types.REMOVE, function* (action) { + const { name } = action.payload; + + const confirm = yield Modal.confirm({ + message: `确认删除命名空间`, + description: "删除后,无法恢复", + okText: "删除", + }); + if (confirm) { + const res = yield deleteNamespace([{ name, token: "polaris@12345678" }]); + if (res) notification.success({ description: "删除成功" }); + yield put(creators.reload()); + } + }); + } + + async getData(filters: this["Filter"]) { + const { page, count, keyword } = filters; + const result = await describeComplicatedNamespaces({ + limit: count, + offset: (page - 1) * count, + name: keyword || undefined, + }); + return { + totalCount: result.amount, + list: + result.namespaces?.map((item) => ({ + ...item, + id: item.name, + })) || [], + }; + } +} diff --git a/web/src/polaris/namespace/getColumns.tsx b/web/src/polaris/namespace/getColumns.tsx new file mode 100644 index 00000000..a4984ddd --- /dev/null +++ b/web/src/polaris/namespace/getColumns.tsx @@ -0,0 +1,50 @@ +import * as React from 'react' +import { DuckCmpProps } from 'saga-duck' +import NamespacePageDuck, { NamespaceItem } from './PageDuck' +import { Text } from 'tea-component' +import { Column } from '../common/ducks/GridPage' +import Action from '../common/duckComponents/grid/Action' + +export default ({ duck: { creators } }: DuckCmpProps): Column[] => [ + { + key: 'name', + header: '名称', + render: x => {x.name}, + }, + { + key: 'commnet', + header: '描述', + render: x => {x.comment || '-'}, + }, + { + key: 'owners', + header: '负责人', + render: x => {x.owners ?? '-'}, + }, + { + key: 'ctime', + header: '创建时间', + render: x => {x.ctime}, + }, + { + key: 'mtime', + header: '修改时间', + render: x => {x.mtime}, + }, + { + key: 'action', + header: '操作', + render: x => { + return ( + + dispatch(creators.edit(x))} tip={'编辑'}> + {'编辑'} + + dispatch(creators.remove(x))} tip={'删除'}> + {'删除'} + + + ) + }, + }, +] diff --git a/web/src/polaris/namespace/model.ts b/web/src/polaris/namespace/model.ts new file mode 100644 index 00000000..678c3410 --- /dev/null +++ b/web/src/polaris/namespace/model.ts @@ -0,0 +1,70 @@ +import { getApiRequest, apiRequest, putApiRequest } from "../common/util/apiRequest"; +import { DescribeNamespacesResult } from "../service/model"; +import { Namespace } from "../service/types"; + +export interface DescribeNamespaceParams { + limit: number; + offset: number; + name?: string; + owners?: string; +} + +export interface CreateNamespaceParams { + name: string; + comment: string; + owners: string; +} +export interface CreateNamespaceResult { + namespace: Namespace; +} +export interface ModifyNamespaceParams { + name: string; + comment?: string; + owners?: string; + token: string; +} +export interface ModifyNamespaceResult { + size: number; +} +export interface DeleteNamespaceParams { + name: string; + token: string; +} +export interface DeleteNamespaceResult { + size: number; +} +export async function describeComplicatedNamespaces(params: DescribeNamespaceParams) { + const res = await getApiRequest({ + action: "naming/v1/namespaces", + data: params, + }); + + return res; +} + +export async function createNamespace(params: CreateNamespaceParams[]) { + const res = await apiRequest({ + action: "naming/v1/namespaces", + data: params, + }); + + return res; +} + +export async function modifyNamespace(params: ModifyNamespaceParams[]) { + const res = await putApiRequest({ + action: "naming/v1/namespaces", + data: params, + }); + + return res; +} + +export async function deleteNamespace(params: DeleteNamespaceParams[]) { + const res = await apiRequest({ + action: "naming/v1/namespaces/delete", + data: params, + }); + + return res; +} diff --git a/web/src/polaris/namespace/operation/Create.tsx b/web/src/polaris/namespace/operation/Create.tsx new file mode 100644 index 00000000..84c7cab4 --- /dev/null +++ b/web/src/polaris/namespace/operation/Create.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { DuckCmpProps, purify } from "saga-duck"; +import Duck from "./CreateDuck"; +import { Form } from "tea-component"; +import Dialog from "@src/polaris/common/duckComponents/Dialog"; +import FormField from "@src/polaris/common/duckComponents/form/Field"; +import Input from "@src/polaris/common/duckComponents/form/Input"; + +export default function Create(props: DuckCmpProps) { + const { duck, store, dispatch } = props; + const { selectors } = duck; + const visible = selectors.visible(store); + if (!visible) { + return