diff --git a/config/routes.ts b/config/routes.ts index 75bee66351..272bb658a8 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -28,13 +28,19 @@ const routes = [ { path: '/ssl/list', name: 'list', - component: './ssl/List', + component: './SSL/List', hideInMenu: true, }, { name: 'create', path: '/ssl/create', - component: './ssl/Create', + component: './SSL/Create', + hideInMenu: true, + }, + { + name: 'edit', + path: '/ssl/:id/edit', + component: './SSL/Create', hideInMenu: true, }, ], @@ -52,18 +58,79 @@ const routes = [ path: '/routes/list', name: 'list', icon: 'BarsOutlined', - component: './Routes/List', + component: './Route/List', + hideInMenu: true, }, { path: '/routes/create', name: 'create', - component: './Routes/Create', + component: './Route/Create', hideInMenu: true, }, { path: '/routes/:rid/edit', name: 'edit', - component: './Routes/Create', + component: './Route/Create', + hideInMenu: true, + }, + ], + }, + { + name: 'consumer', + path: '/consumer', + icon: 'BarsOutlined', + routes: [ + { + path: '/consumer', + redirect: '/Consumer/list', + }, + { + path: '/consumer/list', + name: 'list', + icon: 'BarsOutlined', + component: './Consumer/List', + hideInMenu: true, + }, + { + path: '/consumer/create', + name: 'create', + component: './Consumer/Create', + hideInMenu: true, + }, + { + path: '/consumer/:id/edit', + name: 'edit', + component: './Consumer/Create', + hideInMenu: true, + }, + ], + }, + { + name: 'upstream', + path: '/upstream', + icon: 'BarsOutlined', + routes: [ + { + path: '/upstream', + redirect: '/Upstream/list', + }, + { + path: '/upstream/list', + name: 'list', + icon: 'BarsOutlined', + component: './Upstream/List', + hideInMenu: true, + }, + { + path: '/upstream/create', + name: 'create', + component: './Upstream/Create', + hideInMenu: true, + }, + { + path: '/upstream/:id/edit', + name: 'edit', + component: './Upstream/Create', hideInMenu: true, }, ], diff --git a/package.json b/package.json index 3a23a31270..329836e6ac 100644 --- a/package.json +++ b/package.json @@ -33,8 +33,7 @@ }, "husky": { "hooks": { - "pre-commit": "npm run lint-staged", - "commit-msg": "node scripts/verifyCommit.js" + "pre-commit": "npm run lint-staged" } }, "lint-staged": { @@ -48,8 +47,12 @@ "@ant-design/icons": "^4.2.1", "@ant-design/pro-layout": "6.0.0-2", "@ant-design/pro-table": "^2.3.3", + "@rjsf/antd": "^2.2.0", + "@rjsf/core": "^2.2.0", "antd": "^4.3.3", "classnames": "^2.2.6", + "dayjs": "^1.8.28", + "json-schema": "^0.2.5", "lodash": "^4.17.15", "moment": "^2.25.3", "nzh": "^1.0.3", diff --git a/src/pages/Routes/components/ActionBar/ActionBar.tsx b/src/components/ActionBar/ActionBar.tsx similarity index 73% rename from src/pages/Routes/components/ActionBar/ActionBar.tsx rename to src/components/ActionBar/ActionBar.tsx index 3abd3a2767..6ce7b5396a 100644 --- a/src/pages/Routes/components/ActionBar/ActionBar.tsx +++ b/src/components/ActionBar/ActionBar.tsx @@ -4,8 +4,9 @@ import { Row, Col, Button } from 'antd'; interface Props { step: number; + lastStep: number; onChange(nextStep: number): void; - redirect?: boolean; + withResultView?: boolean; } const style: CSSProperties = { @@ -19,8 +20,9 @@ const style: CSSProperties = { width: '100%', }; -const ActionBar: React.FC = ({ step, onChange, redirect }) => { - if (step > 3) { +const ActionBar: React.FC = ({ step, lastStep, onChange, withResultView }) => { + if (step > lastStep && !withResultView) { + onChange(lastStep); return null; } @@ -28,14 +30,13 @@ const ActionBar: React.FC = ({ step, onChange, redirect }) => {
- diff --git a/src/pages/Routes/components/ActionBar/index.ts b/src/components/ActionBar/index.ts similarity index 100% rename from src/pages/Routes/components/ActionBar/index.ts rename to src/components/ActionBar/index.ts diff --git a/src/pages/Routes/components/PanelSection/index.tsx b/src/components/PanelSection/index.tsx similarity index 100% rename from src/pages/Routes/components/PanelSection/index.tsx rename to src/components/PanelSection/index.tsx diff --git a/src/pages/Routes/components/CreateStep3/PluginCard.tsx b/src/components/PluginPage/PluginCard.tsx similarity index 100% rename from src/pages/Routes/components/CreateStep3/PluginCard.tsx rename to src/components/PluginPage/PluginCard.tsx diff --git a/src/pages/Routes/components/CreateStep3/PluginDrawer.tsx b/src/components/PluginPage/PluginDrawer.tsx similarity index 59% rename from src/pages/Routes/components/CreateStep3/PluginDrawer.tsx rename to src/components/PluginPage/PluginDrawer.tsx index b26d63f322..3a8d3ce540 100644 --- a/src/pages/Routes/components/CreateStep3/PluginDrawer.tsx +++ b/src/components/PluginPage/PluginDrawer.tsx @@ -1,32 +1,41 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import { Drawer, Button } from 'antd'; -import { useForm } from 'antd/es/form/util'; +import { withTheme, FormProps } from '@rjsf/core'; +import { Theme as AntDTheme } from '@rjsf/antd'; +import { JSONSchema7 } from 'json-schema'; -import PluginForm from '@/components/PluginForm'; - -interface Props extends Omit { +interface Props { + name?: string; + initialData: any; active?: boolean; disabled?: boolean; + schema: JSONSchema7; onActive(name: string): void; onInactive(name: string): void; onClose(): void; + onFinish(values: any): void; } const PluginDrawer: React.FC = ({ name, active, disabled, + schema, + initialData, onActive, onInactive, onClose, - ...rest + onFinish, }) => { - const [form] = useForm(); + const PluginForm = withTheme(AntDTheme); if (!name) { return null; } + // NOTE: 用于作为 PluginForm 的引用 + let form: any; + return ( = ({ @@ -65,7 +76,22 @@ const PluginDrawer: React.FC = ({ ) } > - + ) => { + form = _form; + }} + onSubmit={({ formData }) => { + onFinish(formData); + }} + > + {/* NOTE: 留空,用于隐藏 Submit 按钮 */} + + ); }; diff --git a/src/components/PluginPage/PluginPage.tsx b/src/components/PluginPage/PluginPage.tsx new file mode 100644 index 0000000000..c1c3f6842c --- /dev/null +++ b/src/components/PluginPage/PluginPage.tsx @@ -0,0 +1,125 @@ +import React, { useState, useEffect } from 'react'; +import { LinkOutlined, SettingOutlined } from '@ant-design/icons'; +import { omit } from 'lodash'; +import { JSONSchema7 } from 'json-schema'; + +import PanelSection from '@/components/PanelSection'; + +import PluginCard from './PluginCard'; +import PluginDrawer from './PluginDrawer'; +import { getList, fetchPluginSchema } from './service'; +import { PLUGIN_MAPPER_SOURCE } from './data'; + +type Props = { + disabled?: boolean; + data: PluginPage.PluginData; + onChange?(data: PluginPage.PluginData): void; +}; + +const PluginPage: React.FC = ({ data = {}, disabled, onChange }) => { + const [pluginName, setPluginName] = useState(); + const [activeList, setActiveList] = useState([]); + const [inactiveList, setInactiveList] = useState([]); + const [schema, setSchema] = useState(); + + const pluginList = [ + { + title: '已启用', + list: activeList, + }, + { + title: '未启用', + list: inactiveList, + }, + ]; + + useEffect(() => { + getList(data).then((props) => { + setActiveList(props.activeList); + setInactiveList(props.inactiveList); + }); + }, []); + + return ( + <> + {pluginList.map(({ list, title }) => { + if (disabled && title === '未启用') { + return null; + } + return ( + + {list.map(({ name }) => ( + { + fetchPluginSchema(name).then((schemaData) => { + setSchema(schemaData); + setTimeout(() => { + setPluginName(name); + }, 300); + }); + }} + />, + + window.open( + `https://github.com/apache/incubator-apisix/blob/master/doc/plugins/${name}.md`, + ) + } + />, + ]} + key={name} + /> + ))} + + ); + })} + + item.name === pluginName))} + schema={schema!} + disabled={disabled} + onActive={(name) => { + // TODO: 需测试诸如 普罗米修斯 此类只需通过 {} 即可启用的插件 + setActiveList(activeList.concat({ name, ...PLUGIN_MAPPER_SOURCE[name] })); + setInactiveList(inactiveList.filter((item) => item.name !== name)); + }} + onInactive={(name) => { + if (!onChange) { + throw new Error('请提供 onChange 方法'); + } + onChange(omit(Object.assign({}, data), name)); + setInactiveList(inactiveList.concat({ name, ...PLUGIN_MAPPER_SOURCE[name] })); + setActiveList(activeList.filter((item) => item.name !== name)); + setPluginName(undefined); + }} + onClose={() => setPluginName(undefined)} + onFinish={(value) => { + if (!pluginName) { + return; + } + if (!onChange) { + throw new Error('请提供 onChange 方法'); + } + onChange(Object.assign({}, data, { [pluginName]: value })); + setPluginName(undefined); + }} + /> + + ); +}; + +export default PluginPage; diff --git a/src/components/PluginPage/data.ts b/src/components/PluginPage/data.ts new file mode 100644 index 0000000000..e56149fad0 --- /dev/null +++ b/src/components/PluginPage/data.ts @@ -0,0 +1,97 @@ +export const PLUGIN_MAPPER_SOURCE: { [name: string]: PluginPage.PluginMapperItem } = { + 'limit-req': { + category: 'Limit', + }, + 'limit-count': { + category: 'Limit', + }, + 'limit-conn': { + category: 'Limit', + }, + 'key-auth': { + category: 'Security', + }, + 'basic-auth': { + category: 'Security', + }, + prometheus: { + category: 'Metric', + }, + 'node-status': { + category: 'Other', + }, + 'jwt-auth': { + category: 'Security', + }, + zipkin: { + category: 'Metric', + }, + 'ip-restriction': { + category: 'Security', + }, + 'grpc-transcode': { + category: 'Other', + hidden: true, + }, + 'serverless-pre-function': { + category: 'Other', + }, + 'serverless-post-function': { + category: 'Other', + }, + 'openid-connect': { + category: 'Security', + }, + 'proxy-rewrite': { + category: 'Other', + hidden: true, + }, + redirect: { + category: 'Other', + hidden: true, + }, + 'response-rewrite': { + category: 'Other', + }, + 'fault-injection': { + category: 'Security', + }, + 'udp-logger': { + category: 'Log', + }, + 'wolf-rbac': { + category: 'Other', + hidden: true, + }, + 'proxy-cache': { + category: 'Other', + }, + 'tcp-logger': { + category: 'Log', + }, + 'proxy-mirror': { + category: 'Other', + }, + 'kafka-logger': { + category: 'Log', + }, + cors: { + category: 'Security', + }, + heartbeat: { + category: 'Other', + hidden: true, + }, + 'batch-requests': { + category: 'Other', + }, + 'http-logger': { + category: 'Log', + }, + 'mqtt-proxy': { + category: 'Other', + }, + oauth: { + category: 'Security', + }, +}; diff --git a/src/components/PluginPage/index.ts b/src/components/PluginPage/index.ts new file mode 100644 index 0000000000..2a0db295c9 --- /dev/null +++ b/src/components/PluginPage/index.ts @@ -0,0 +1,6 @@ +export { default } from './PluginPage'; +export { default as PluginCard } from './PluginCard'; +export { default as PluginDrawer } from './PluginDrawer'; + +export { default as PluginZhCN } from './locales/zh-CN'; +export { default as PluginEnUS } from './locales/en-US'; diff --git a/src/components/PluginPage/locales/en-US.ts b/src/components/PluginPage/locales/en-US.ts new file mode 100644 index 0000000000..02de0db95f --- /dev/null +++ b/src/components/PluginPage/locales/en-US.ts @@ -0,0 +1,163 @@ +export default { + 'PluginForm.plugin.limit-conn.desc': '限制并发连接数', + 'PluginForm.plugin.limit-conn.property.conn': 'conn', + 'PluginForm.plugin.limit-conn.property.conn.extra': '最大并发连接数', + 'PluginForm.plugin.limit-conn.property.burst': 'burst', + 'PluginForm.plugin.limit-conn.property.burst.extra': + '并发连接数超过 conn,但是低于 conn + burst 时,请求将被延迟处理', + 'PluginForm.plugin.limit-conn.property.default_conn_delay': '延迟时间', + 'PluginForm.plugin.limit-conn.property.default_conn_delay.extra': + '被延迟处理的请求,需要等待多少秒', + 'PluginForm.plugin.limit-conn.property.key': 'key', + 'PluginForm.plugin.limit-conn.property.key.extra': '用来做限制的依据', + 'PluginForm.plugin.limit-conn.property.rejected_code': '拒绝状态码', + 'PluginForm.plugin.limit-conn.property.rejected_code.extra': + '当并发连接数超过 conn + burst 的限制时,返回给终端的 HTTP 状态码', + + 'PluginForm.plugin.limit-count.desc': '在指定的时间范围内,限制总的请求次数', + 'PluginForm.plugin.limit-count.property.count': '总请求次数', + 'PluginForm.plugin.limit-count.property.count.extra': '指定时间窗口内的请求数量阈值', + 'PluginForm.plugin.limit-count.property.time_window': '时间窗口', + 'PluginForm.plugin.limit-count.property.time_window.extra': + '时间窗口的大小(以秒为单位),超过这个时间,总请求次数就会重置', + 'PluginForm.plugin.limit-count.property.key': 'key', + 'PluginForm.plugin.limit-count.property.key.extra': '用来做请求计数的依据', + 'PluginForm.plugin.limit-count.property.rejected_code': '拒绝状态码', + 'PluginForm.plugin.limit-count.property.rejected_code.extra': + '当请求超过阈值时,返回给终端的 HTTP 状态码', + 'PluginForm.plugin.limit-count.property.policy': '策略', + 'PluginForm.plugin.limit-count.property.redis_host': '地址', + 'PluginForm.plugin.limit-count.property.redis_host.extra': '用于集群限流的 Redis 节点地址', + 'PluginForm.plugin.limit-count.property.redis_port': '端口', + 'PluginForm.plugin.limit-count.property.redis_password': '密码', + 'PluginForm.plugin.limit-count.property.redis_timeout': '超时时间(毫秒)', + + 'PluginForm.plugin.limit-req.desc': '限制请求速度的插件,基于漏桶算法', + 'PluginForm.plugin.limit-req.property.rate': 'rate', + 'PluginForm.plugin.limit-req.property.rate.extra': '每秒请求速率', + 'PluginForm.plugin.limit-req.property.burst': 'burst', + 'PluginForm.plugin.limit-req.property.burst.extra': + '每秒请求速率超过 rate,但是低于 rate + burst 时,请求将被延迟处理', + 'PluginForm.plugin.limit-req.property.key': 'key', + 'PluginForm.plugin.limit-req.property.key.extra': '用来做请求计数的依据', + 'PluginForm.plugin.limit-req.property.rejected_code': '拒绝状态码', + 'PluginForm.plugin.limit-req.property.rejected_code.extra': + '速率超过 rate + burst 的限制时,返回给终端的 HTTP 状态码', + + 'PluginForm.plugin.cors.desc': 'CORS 插件可以为服务端启用 CORS 的返回头', + 'PluginForm.plugin.cors.property.allow_origins': '允许跨域访问的 Origin', + 'PluginForm.plugin.cors.property.allow_origins.extra': '比如 https://somehost.com:8081', + 'PluginForm.plugin.cors.property.allow_methods': '允许跨域访问的 Method', + + 'PluginForm.plugin.fault-injection.desc': '故障注入插件,用来模拟各种后端故障和高延迟', + 'PluginForm.plugin.fault-injection.property.http_status': 'HTTP 状态码', + 'PluginForm.plugin.fault-injection.property.body': '响应体', + 'PluginForm.plugin.fault-injection.property.duration': '延迟时间(秒)', + + 'PluginForm.plugin.http-logger.desc': 'http-logger 可以将日志数据请求推送到 HTTP/HTTPS 服务器', + 'PluginForm.plugin.http-logger.property.uri': '日志服务器地址', + 'PluginForm.plugin.http-logger.property.uri.extra': '比如 127.0.0.1:80/postendpoint?param=1', + + 'PluginForm.plugin.ip-restriction.desc': + 'ip-restriction 可以把一批 IP 地址列入白名单或黑名单(二选一),时间复杂度是O(1),并支持用 CIDR 来表示 IP 范围', + 'PluginForm.plugin.ip-restriction.property.whitelist': '白名单', + 'PluginForm.plugin.ip-restriction.property.blacklist': '黑名单', + + 'PluginForm.plugin.kafka-logger.desc': '把接口请求日志以 JSON 的形式推送给外部 Kafka 集群', + 'PluginForm.plugin.kafka-logger.property.broker_list': 'broker', + 'PluginForm.plugin.kafka-logger.property.kafka_topic': 'topic', + + 'PluginForm.plugin.prometheus.desc': '提供符合 prometheus 数据格式的 metrics 数据', + + 'PluginForm.plugin.proxy-cache.desc': '代理缓存插件,缓存后端服务的响应数据', + 'PluginForm.plugin.proxy-cache.property.cache_zone': '缓存区域名', + 'PluginForm.plugin.proxy-cache.property.cache_zone.extra': + ' 本地目录为 /tmp/区域名,修改默认区域名必须同时修改 config.yaml', + 'PluginForm.plugin.proxy-cache.property.cache_key': '缓存 key', + 'PluginForm.plugin.proxy-cache.property.cache_key.extra': + '可以使用 Nginx 变量,例如:$host, $uri', + 'PluginForm.plugin.proxy-cache.property.cache_bypass': '跳过缓存检索', + 'PluginForm.plugin.proxy-cache.property.cache_bypass.extra': + '这里可以使用 Nginx 变量,当此参数的值不为空或非0时将会跳过缓存的检索', + 'PluginForm.plugin.proxy-cache.property.cache_method': '缓存 Method', + 'PluginForm.plugin.proxy-cache.property.cache_http_status': '缓存响应状态码', + 'PluginForm.plugin.proxy-cache.property.hide_cache_headers': '隐藏缓存头', + 'PluginForm.plugin.proxy-cache.property.hide_cache_headers.extra': + '是否将 Expires 和 Cache-Control 响应头返回给客户端', + 'PluginForm.plugin.proxy-cache.property.no_cache': '不缓存的数据', + 'PluginForm.plugin.proxy-cache.property.no_cache.extra': + '这里可以使用 Nginx 变量, 当此参数的值不为空或非0时将不会缓存数据', + + 'PluginForm.plugin.proxy-mirror.desc': 'proxy mirror 代理镜像插件,提供了镜像客户端请求的能力', + 'PluginForm.plugin.proxy-mirror.property.host': '镜像服务地址', + 'PluginForm.plugin.proxy-mirror.property.host.extra': + '例如:http://127.0.0.1:9797。地址中需要包含 http 或 https,不能包含 URI 部分', + + 'PluginForm.plugin.response-rewrite.desc': '该插件支持修改上游服务返回的 body 和 header 信息', + 'PluginForm.plugin.response-rewrite.property.status_code': '状态码', + 'PluginForm.plugin.response-rewrite.property.body': '响应体', + 'PluginForm.plugin.response-rewrite.property.body_base64': '响应体是否需要 base64 解码', + 'PluginForm.plugin.response-rewrite.property.headers': 'HTTP 头', + + 'PluginForm.plugin.syslog.desc': '对接 syslog 日志服务器', + 'PluginForm.plugin.syslog.property.host': '日志服务器地址', + 'PluginForm.plugin.syslog.property.port': '日志服务器端口', + 'PluginForm.plugin.syslog.property.timeout': '超时时间', + 'PluginForm.plugin.syslog.property.tls': '开启 SSL', + 'PluginForm.plugin.syslog.property.flush_limit': '缓存区大小', + 'PluginForm.plugin.syslog.property.sock_type': '协议类型', + 'PluginForm.plugin.syslog.property.max_retry_times': '重试次数', + 'PluginForm.plugin.syslog.property.retry_interval': '重试间隔时间(毫秒)', + 'PluginForm.plugin.syslog.property.pool_size': '连接池大小', + + 'PluginForm.plugin.tcp-logger.desc': '对接 TCP 日志服务器', + 'PluginForm.plugin.tcp-logger.property.host': '日志服务器地址', + 'PluginForm.plugin.tcp-logger.property.port': '日志服务器地址', + 'PluginForm.plugin.tcp-logger.property.timeout': '超时时间', + 'PluginForm.plugin.tcp-logger.property.tls': '开启 SSL', + 'PluginForm.plugin.tcp-logger.property.tls_options': 'TLS 选型', + + 'PluginForm.plugin.udp-logger.desc': '对接 UDP 日志服务器', + 'PluginForm.plugin.udp-logger.property.host': '日志服务器地址', + 'PluginForm.plugin.udp-logger.property.port': '日志服务器地址', + 'PluginForm.plugin.udp-logger.property.timeout': '超时时间', + + 'PluginForm.plugin.zipkin.desc': '对接 zipkin', + 'PluginForm.plugin.zipkin.property.endpoint': 'endpoint', + 'PluginForm.plugin.zipkin.property.endpoint.extra': '例如 http://127.0.0.1:9411/api/v2/spans', + 'PluginForm.plugin.zipkin.property.sample_ratio': '采样率', + 'PluginForm.plugin.zipkin.property.service_name': '服务名', + 'PluginForm.plugin.zipkin.property.server_addr': '网关实例 IP', + 'PluginForm.plugin.zipkin.property.server_addr.extra': '默认值是 Nginx 内置变量 server_addr', + + 'PluginForm.plugin.skywalking.desc': '对接 Apache Skywalking', + 'PluginForm.plugin.skywalking.property.endpoint': 'endpoint', + 'PluginForm.plugin.skywalking.property.endpoint.extra': '例如 http://127.0.0.1:12800', + 'PluginForm.plugin.skywalking.property.sample_ratio': '采样率', + 'PluginForm.plugin.skywalking.property.service_name': '服务名', + + 'PluginForm.plugin.serverless-pre-function.desc': '在指定阶段最开始的位置,运行指定的 Lua 函数', + 'PluginForm.plugin.serverless-pre-function.property.phase': '运行阶段', + 'PluginForm.plugin.serverless-pre-function.property.functions': '运行的函数集', + + 'PluginForm.plugin.serverless-post-function.desc': '在指定阶段最后的位置,运行指定的 Lua 函数', + 'PluginForm.plugin.serverless-post-function.property.phase': '运行阶段', + 'PluginForm.plugin.serverless-post-function.property.functions': '运行的函数集', + + 'PluginForm.plugin.basic-auth.desc': 'basic auth 插件', + 'PluginForm.plugin.jwt-auth.desc': 'JWT 认证插件', + 'PluginForm.plugin.key-auth.desc': 'key auth 插件', + 'PluginForm.plugin.wolf-rbac.desc': '对接 wolf RBAC 服务', + 'PluginForm.plugin.openid-connect.desc': 'Open ID Connect(OIDC) 插件提供对接外部认证服务的能力', + + 'PluginForm.plugin.redirect.desc': '重定向插件', + 'PluginForm.plugin.proxy-rewrite.desc': 'proxy rewrite 代理改写插件,可以改写客户端请求', + 'PluginForm.plugin.mqtt-proxy.desc': + 'mqtt-proxy 插件可以帮助你根据 MQTT 的 client_id 实现动态负载均衡', + 'PluginForm.plugin.grpc-transcoding.desc': + 'gRPC 转换插件,实现 HTTP(s) -> APISIX -> gRPC server 的转换', + 'PluginForm.plugin.batch-requests.desc': + 'batch-requests 插件可以一次接受多个请求并以 http pipeline 的方式在网关发起多个 http 请求,合并结果后再返回客户端,这在客户端需要访问多个接口时可以显著地提升请求性能', + + 'PluginForm.plugin.node-status.desc': 'node-status 暂无描述', +}; diff --git a/src/components/PluginPage/locales/zh-CN.ts b/src/components/PluginPage/locales/zh-CN.ts new file mode 100644 index 0000000000..02de0db95f --- /dev/null +++ b/src/components/PluginPage/locales/zh-CN.ts @@ -0,0 +1,163 @@ +export default { + 'PluginForm.plugin.limit-conn.desc': '限制并发连接数', + 'PluginForm.plugin.limit-conn.property.conn': 'conn', + 'PluginForm.plugin.limit-conn.property.conn.extra': '最大并发连接数', + 'PluginForm.plugin.limit-conn.property.burst': 'burst', + 'PluginForm.plugin.limit-conn.property.burst.extra': + '并发连接数超过 conn,但是低于 conn + burst 时,请求将被延迟处理', + 'PluginForm.plugin.limit-conn.property.default_conn_delay': '延迟时间', + 'PluginForm.plugin.limit-conn.property.default_conn_delay.extra': + '被延迟处理的请求,需要等待多少秒', + 'PluginForm.plugin.limit-conn.property.key': 'key', + 'PluginForm.plugin.limit-conn.property.key.extra': '用来做限制的依据', + 'PluginForm.plugin.limit-conn.property.rejected_code': '拒绝状态码', + 'PluginForm.plugin.limit-conn.property.rejected_code.extra': + '当并发连接数超过 conn + burst 的限制时,返回给终端的 HTTP 状态码', + + 'PluginForm.plugin.limit-count.desc': '在指定的时间范围内,限制总的请求次数', + 'PluginForm.plugin.limit-count.property.count': '总请求次数', + 'PluginForm.plugin.limit-count.property.count.extra': '指定时间窗口内的请求数量阈值', + 'PluginForm.plugin.limit-count.property.time_window': '时间窗口', + 'PluginForm.plugin.limit-count.property.time_window.extra': + '时间窗口的大小(以秒为单位),超过这个时间,总请求次数就会重置', + 'PluginForm.plugin.limit-count.property.key': 'key', + 'PluginForm.plugin.limit-count.property.key.extra': '用来做请求计数的依据', + 'PluginForm.plugin.limit-count.property.rejected_code': '拒绝状态码', + 'PluginForm.plugin.limit-count.property.rejected_code.extra': + '当请求超过阈值时,返回给终端的 HTTP 状态码', + 'PluginForm.plugin.limit-count.property.policy': '策略', + 'PluginForm.plugin.limit-count.property.redis_host': '地址', + 'PluginForm.plugin.limit-count.property.redis_host.extra': '用于集群限流的 Redis 节点地址', + 'PluginForm.plugin.limit-count.property.redis_port': '端口', + 'PluginForm.plugin.limit-count.property.redis_password': '密码', + 'PluginForm.plugin.limit-count.property.redis_timeout': '超时时间(毫秒)', + + 'PluginForm.plugin.limit-req.desc': '限制请求速度的插件,基于漏桶算法', + 'PluginForm.plugin.limit-req.property.rate': 'rate', + 'PluginForm.plugin.limit-req.property.rate.extra': '每秒请求速率', + 'PluginForm.plugin.limit-req.property.burst': 'burst', + 'PluginForm.plugin.limit-req.property.burst.extra': + '每秒请求速率超过 rate,但是低于 rate + burst 时,请求将被延迟处理', + 'PluginForm.plugin.limit-req.property.key': 'key', + 'PluginForm.plugin.limit-req.property.key.extra': '用来做请求计数的依据', + 'PluginForm.plugin.limit-req.property.rejected_code': '拒绝状态码', + 'PluginForm.plugin.limit-req.property.rejected_code.extra': + '速率超过 rate + burst 的限制时,返回给终端的 HTTP 状态码', + + 'PluginForm.plugin.cors.desc': 'CORS 插件可以为服务端启用 CORS 的返回头', + 'PluginForm.plugin.cors.property.allow_origins': '允许跨域访问的 Origin', + 'PluginForm.plugin.cors.property.allow_origins.extra': '比如 https://somehost.com:8081', + 'PluginForm.plugin.cors.property.allow_methods': '允许跨域访问的 Method', + + 'PluginForm.plugin.fault-injection.desc': '故障注入插件,用来模拟各种后端故障和高延迟', + 'PluginForm.plugin.fault-injection.property.http_status': 'HTTP 状态码', + 'PluginForm.plugin.fault-injection.property.body': '响应体', + 'PluginForm.plugin.fault-injection.property.duration': '延迟时间(秒)', + + 'PluginForm.plugin.http-logger.desc': 'http-logger 可以将日志数据请求推送到 HTTP/HTTPS 服务器', + 'PluginForm.plugin.http-logger.property.uri': '日志服务器地址', + 'PluginForm.plugin.http-logger.property.uri.extra': '比如 127.0.0.1:80/postendpoint?param=1', + + 'PluginForm.plugin.ip-restriction.desc': + 'ip-restriction 可以把一批 IP 地址列入白名单或黑名单(二选一),时间复杂度是O(1),并支持用 CIDR 来表示 IP 范围', + 'PluginForm.plugin.ip-restriction.property.whitelist': '白名单', + 'PluginForm.plugin.ip-restriction.property.blacklist': '黑名单', + + 'PluginForm.plugin.kafka-logger.desc': '把接口请求日志以 JSON 的形式推送给外部 Kafka 集群', + 'PluginForm.plugin.kafka-logger.property.broker_list': 'broker', + 'PluginForm.plugin.kafka-logger.property.kafka_topic': 'topic', + + 'PluginForm.plugin.prometheus.desc': '提供符合 prometheus 数据格式的 metrics 数据', + + 'PluginForm.plugin.proxy-cache.desc': '代理缓存插件,缓存后端服务的响应数据', + 'PluginForm.plugin.proxy-cache.property.cache_zone': '缓存区域名', + 'PluginForm.plugin.proxy-cache.property.cache_zone.extra': + ' 本地目录为 /tmp/区域名,修改默认区域名必须同时修改 config.yaml', + 'PluginForm.plugin.proxy-cache.property.cache_key': '缓存 key', + 'PluginForm.plugin.proxy-cache.property.cache_key.extra': + '可以使用 Nginx 变量,例如:$host, $uri', + 'PluginForm.plugin.proxy-cache.property.cache_bypass': '跳过缓存检索', + 'PluginForm.plugin.proxy-cache.property.cache_bypass.extra': + '这里可以使用 Nginx 变量,当此参数的值不为空或非0时将会跳过缓存的检索', + 'PluginForm.plugin.proxy-cache.property.cache_method': '缓存 Method', + 'PluginForm.plugin.proxy-cache.property.cache_http_status': '缓存响应状态码', + 'PluginForm.plugin.proxy-cache.property.hide_cache_headers': '隐藏缓存头', + 'PluginForm.plugin.proxy-cache.property.hide_cache_headers.extra': + '是否将 Expires 和 Cache-Control 响应头返回给客户端', + 'PluginForm.plugin.proxy-cache.property.no_cache': '不缓存的数据', + 'PluginForm.plugin.proxy-cache.property.no_cache.extra': + '这里可以使用 Nginx 变量, 当此参数的值不为空或非0时将不会缓存数据', + + 'PluginForm.plugin.proxy-mirror.desc': 'proxy mirror 代理镜像插件,提供了镜像客户端请求的能力', + 'PluginForm.plugin.proxy-mirror.property.host': '镜像服务地址', + 'PluginForm.plugin.proxy-mirror.property.host.extra': + '例如:http://127.0.0.1:9797。地址中需要包含 http 或 https,不能包含 URI 部分', + + 'PluginForm.plugin.response-rewrite.desc': '该插件支持修改上游服务返回的 body 和 header 信息', + 'PluginForm.plugin.response-rewrite.property.status_code': '状态码', + 'PluginForm.plugin.response-rewrite.property.body': '响应体', + 'PluginForm.plugin.response-rewrite.property.body_base64': '响应体是否需要 base64 解码', + 'PluginForm.plugin.response-rewrite.property.headers': 'HTTP 头', + + 'PluginForm.plugin.syslog.desc': '对接 syslog 日志服务器', + 'PluginForm.plugin.syslog.property.host': '日志服务器地址', + 'PluginForm.plugin.syslog.property.port': '日志服务器端口', + 'PluginForm.plugin.syslog.property.timeout': '超时时间', + 'PluginForm.plugin.syslog.property.tls': '开启 SSL', + 'PluginForm.plugin.syslog.property.flush_limit': '缓存区大小', + 'PluginForm.plugin.syslog.property.sock_type': '协议类型', + 'PluginForm.plugin.syslog.property.max_retry_times': '重试次数', + 'PluginForm.plugin.syslog.property.retry_interval': '重试间隔时间(毫秒)', + 'PluginForm.plugin.syslog.property.pool_size': '连接池大小', + + 'PluginForm.plugin.tcp-logger.desc': '对接 TCP 日志服务器', + 'PluginForm.plugin.tcp-logger.property.host': '日志服务器地址', + 'PluginForm.plugin.tcp-logger.property.port': '日志服务器地址', + 'PluginForm.plugin.tcp-logger.property.timeout': '超时时间', + 'PluginForm.plugin.tcp-logger.property.tls': '开启 SSL', + 'PluginForm.plugin.tcp-logger.property.tls_options': 'TLS 选型', + + 'PluginForm.plugin.udp-logger.desc': '对接 UDP 日志服务器', + 'PluginForm.plugin.udp-logger.property.host': '日志服务器地址', + 'PluginForm.plugin.udp-logger.property.port': '日志服务器地址', + 'PluginForm.plugin.udp-logger.property.timeout': '超时时间', + + 'PluginForm.plugin.zipkin.desc': '对接 zipkin', + 'PluginForm.plugin.zipkin.property.endpoint': 'endpoint', + 'PluginForm.plugin.zipkin.property.endpoint.extra': '例如 http://127.0.0.1:9411/api/v2/spans', + 'PluginForm.plugin.zipkin.property.sample_ratio': '采样率', + 'PluginForm.plugin.zipkin.property.service_name': '服务名', + 'PluginForm.plugin.zipkin.property.server_addr': '网关实例 IP', + 'PluginForm.plugin.zipkin.property.server_addr.extra': '默认值是 Nginx 内置变量 server_addr', + + 'PluginForm.plugin.skywalking.desc': '对接 Apache Skywalking', + 'PluginForm.plugin.skywalking.property.endpoint': 'endpoint', + 'PluginForm.plugin.skywalking.property.endpoint.extra': '例如 http://127.0.0.1:12800', + 'PluginForm.plugin.skywalking.property.sample_ratio': '采样率', + 'PluginForm.plugin.skywalking.property.service_name': '服务名', + + 'PluginForm.plugin.serverless-pre-function.desc': '在指定阶段最开始的位置,运行指定的 Lua 函数', + 'PluginForm.plugin.serverless-pre-function.property.phase': '运行阶段', + 'PluginForm.plugin.serverless-pre-function.property.functions': '运行的函数集', + + 'PluginForm.plugin.serverless-post-function.desc': '在指定阶段最后的位置,运行指定的 Lua 函数', + 'PluginForm.plugin.serverless-post-function.property.phase': '运行阶段', + 'PluginForm.plugin.serverless-post-function.property.functions': '运行的函数集', + + 'PluginForm.plugin.basic-auth.desc': 'basic auth 插件', + 'PluginForm.plugin.jwt-auth.desc': 'JWT 认证插件', + 'PluginForm.plugin.key-auth.desc': 'key auth 插件', + 'PluginForm.plugin.wolf-rbac.desc': '对接 wolf RBAC 服务', + 'PluginForm.plugin.openid-connect.desc': 'Open ID Connect(OIDC) 插件提供对接外部认证服务的能力', + + 'PluginForm.plugin.redirect.desc': '重定向插件', + 'PluginForm.plugin.proxy-rewrite.desc': 'proxy rewrite 代理改写插件,可以改写客户端请求', + 'PluginForm.plugin.mqtt-proxy.desc': + 'mqtt-proxy 插件可以帮助你根据 MQTT 的 client_id 实现动态负载均衡', + 'PluginForm.plugin.grpc-transcoding.desc': + 'gRPC 转换插件,实现 HTTP(s) -> APISIX -> gRPC server 的转换', + 'PluginForm.plugin.batch-requests.desc': + 'batch-requests 插件可以一次接受多个请求并以 http pipeline 的方式在网关发起多个 http 请求,合并结果后再返回客户端,这在客户端需要访问多个接口时可以显著地提升请求性能', + + 'PluginForm.plugin.node-status.desc': 'node-status 暂无描述', +}; diff --git a/src/components/PluginPage/service.ts b/src/components/PluginPage/service.ts new file mode 100644 index 0000000000..e644ba259d --- /dev/null +++ b/src/components/PluginPage/service.ts @@ -0,0 +1,29 @@ +import { request } from 'umi'; + +import { JSONSchema7 } from 'json-schema'; + +import { PLUGIN_MAPPER_SOURCE } from './data'; + +export const fetchPluginList = () => request('/plugins'); + +export const getList = (plugins: PluginPage.PluginData) => { + const PLUGIN_BLOCK_LIST = Object.entries(PLUGIN_MAPPER_SOURCE) + .filter(([, value]) => value.hidden) + .flat() + .filter((item) => typeof item === 'string'); + + return fetchPluginList().then((data) => { + const names = data.filter((name) => !PLUGIN_BLOCK_LIST.includes(name)); + + const activeNameList = Object.keys(plugins); + const inactiveNameList = names.filter((name) => !activeNameList.includes(name)); + + return { + activeList: activeNameList.map((name) => ({ name, ...PLUGIN_MAPPER_SOURCE[name] })), + inactiveList: inactiveNameList.map((name) => ({ name, ...PLUGIN_MAPPER_SOURCE[name] })), + }; + }); +}; + +export const fetchPluginSchema = (name: string): Promise => + request(`/schema/plugins/${name}`); diff --git a/src/components/PluginPage/typing.d.ts b/src/components/PluginPage/typing.d.ts new file mode 100644 index 0000000000..c48ecf0d24 --- /dev/null +++ b/src/components/PluginPage/typing.d.ts @@ -0,0 +1,14 @@ +declare namespace PluginPage { + type PluginCategory = 'Security' | 'Limit' | 'Log' | 'Metric' | 'Other'; + + type PluginMapperItem = { + category: PluginCategory; + hidden?: boolean; + }; + + interface PluginProps extends PluginMapperItem { + name: string; + } + + type PluginData = { [name: string]: any }; +} diff --git a/src/iconfont.ts b/src/iconfont.ts new file mode 100644 index 0000000000..a1942c2e76 --- /dev/null +++ b/src/iconfont.ts @@ -0,0 +1,8 @@ +import { createFromIconfontCN } from '@ant-design/icons'; + +// NOTE: 增加新图标时,请访问 https://www.iconfont.cn/manage/index 进行图标管理 +const IconFont = createFromIconfontCN({ + scriptUrl: '//at.alicdn.com/t/font_1918158_alfpv3n06l6.js', +}); + +export default IconFont; diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 6b136f7e09..3ff90f6c1c 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -1,5 +1,8 @@ import { PluginFormEnUS } from '@/components/PluginForm'; +import { ConsumerEnUS } from '@/pages/Consumer'; +import { RouteEnUS } from '@/pages/Route'; + import component from './en-US/component'; import globalHeader from './en-US/globalHeader'; import menu from './en-US/menu'; @@ -20,4 +23,6 @@ export default { ...pwa, ...component, ...PluginFormEnUS, + ...ConsumerEnUS, + ...RouteEnUS, }; diff --git a/src/locales/en-US/menu.ts b/src/locales/en-US/menu.ts index 90b066fb8d..8d99bc440a 100644 --- a/src/locales/en-US/menu.ts +++ b/src/locales/en-US/menu.ts @@ -53,8 +53,4 @@ export default { 'menu.ssl.edit': 'Edit', 'menu.ssl.create': 'Create', 'menu.setting': 'Settings', - 'menu.routes': 'Route', - 'menu.routes.list': 'Route List', - 'menu.routes.create': 'Create a Route', - 'menu.routes.edit': 'Edit the Route', }; diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 67b4086431..c021df8679 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -1,5 +1,7 @@ import { PluginFormZhCN } from '@/components/PluginForm'; +import { ConsumerZhCN } from '@/pages/Consumer'; +import { RouteZhCN } from '@/pages/Route'; import component from './zh-CN/component'; import globalHeader from './zh-CN/globalHeader'; import menu from './zh-CN/menu'; @@ -20,4 +22,6 @@ export default { ...pwa, ...component, ...PluginFormZhCN, + ...ConsumerZhCN, + ...RouteZhCN, }; diff --git a/src/locales/zh-CN/menu.ts b/src/locales/zh-CN/menu.ts index 6821b81592..31ad32b588 100644 --- a/src/locales/zh-CN/menu.ts +++ b/src/locales/zh-CN/menu.ts @@ -54,8 +54,4 @@ export default { 'menu.ssl.create': '创建', 'menu.setting': '设置', 'menu.metrics': '监控', - 'menu.routes': '路由', - 'menu.routes.list': '列表', - 'menu.routes.create': '创建', - 'menu.routes.edit': '编辑', }; diff --git a/src/pages/Consumer/Create.tsx b/src/pages/Consumer/Create.tsx new file mode 100644 index 0000000000..769dcedd8a --- /dev/null +++ b/src/pages/Consumer/Create.tsx @@ -0,0 +1,86 @@ +import React, { useState, useEffect } from 'react'; +import { PageContainer } from '@ant-design/pro-layout'; +import { Card, Steps, notification } from 'antd'; +import { useForm } from 'antd/es/form/util'; + +import ActionBar from '@/components/ActionBar'; +import PluginPage from '@/components/PluginPage/PluginPage'; +import { history } from 'umi'; +import { PLUGIN_MAPPER_SOURCE } from '@/components/PluginPage/data'; + +import Step1 from './components/Step1'; +import Preview from './components/Preview'; +import { fetchItem, create, update } from './service'; + +const Page: React.FC = (props) => { + const [step, setStep] = useState(1); + const [plugins, setPlugins] = useState({}); + const [form1] = useForm(); + + useEffect(() => { + const { id } = (props as any).match.params; + if (id) { + fetchItem(id).then(({ data }) => { + const { username, desc, ...rest } = data; + form1.setFieldsValue({ username, desc }); + setPlugins(rest.plugins); + }); + } + }, []); + + const onSubmit = () => { + const data = Object.assign({}, form1.getFieldsValue(), { plugins }) as ConsumerModule.Entity; + const { id } = (props as any).match.params; + (id ? update(id, data) : create(data)) + .then(() => { + notification.success({ message: `${id ? '更新' : '创建'} Consumer 成功` }); + history.push('/consumer/list'); + }) + .catch(() => { + setStep(3); + }); + }; + + const onStepChange = (nextStep: number) => { + if (step === 1) { + form1.validateFields().then(() => { + setStep(nextStep); + }); + } else if (nextStep === 3) { + const authPluginNames = Object.entries(PLUGIN_MAPPER_SOURCE) + .filter(([name, value]) => name.includes('auth') && value.category === 'Security') + .map((item) => item[0]); + const isValid = Object.keys(plugins).some((name) => authPluginNames.includes(name)); + if (!isValid) { + notification.warning({ message: '请启用至少一种身份认证类插件' }); + return; + } + setStep(3); + } else if (nextStep === 4) { + onSubmit(); + } else { + setStep(nextStep); + } + }; + + return ( + <> + + + + + + + + + {step === 1 && } + {step === 2 && } + {step === 3 && } + + + + + ); +}; + +export default Page; diff --git a/src/pages/Consumer/List.tsx b/src/pages/Consumer/List.tsx new file mode 100644 index 0000000000..2da7cb3a77 --- /dev/null +++ b/src/pages/Consumer/List.tsx @@ -0,0 +1,89 @@ +import React, { useRef, useState } from 'react'; +import { PageContainer } from '@ant-design/pro-layout'; +import ProTable, { ProColumns, ActionType } from '@ant-design/pro-table'; +import { PlusOutlined } from '@ant-design/icons'; +import { Popconfirm, Button, notification, Input } from 'antd'; +import moment from 'moment'; + +import { history } from 'umi'; +import { fetchList, remove } from './service'; + +const Page: React.FC = () => { + const ref = useRef(); + const [search, setSearch] = useState(''); + + const columns: ProColumns[] = [ + { + title: '用户名', + dataIndex: 'username', + }, + { + title: '描述', + dataIndex: 'desc', + }, + { + title: '更新时间', + dataIndex: 'update_time', + render: (text) => `${moment.unix(Number(text)).format('YYYY-MM-DD HH:mm:ss')}`, + }, + { + title: '操作', + valueType: 'option', + render: (_, record) => ( + <> + + { + remove(record.id).then(() => { + notification.success({ message: '删除记录成功' }); + /* eslint-disable no-unused-expressions */ + ref.current?.reload(); + }); + }} + > + + + + ), + }, + ]; + + return ( + + + actionRef={ref} + columns={columns} + rowKey="id" + search={false} + request={(params) => fetchList(params, search)} + toolBarRender={(action) => [ + { + setSearch(value); + action.setPageInfo({ page: 1 }); + action.reload(); + }} + />, + , + ]} + /> + + ); +}; + +export default Page; diff --git a/src/pages/Consumer/components/Preview.tsx b/src/pages/Consumer/components/Preview.tsx new file mode 100644 index 0000000000..db930f224e --- /dev/null +++ b/src/pages/Consumer/components/Preview.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { FormInstance } from 'antd/lib/form'; + +import PluginPage from '@/components/PluginPage'; + +import Step1 from './Step1'; + +type Props = { + form1: FormInstance; + plugins: PluginPage.PluginData; +}; + +const Page: React.FC = ({ form1, plugins }) => { + return ( + <> + + + + ); +}; + +export default Page; diff --git a/src/pages/Consumer/components/Step1.tsx b/src/pages/Consumer/components/Step1.tsx new file mode 100644 index 0000000000..ba66115ad1 --- /dev/null +++ b/src/pages/Consumer/components/Step1.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { Form, Input } from 'antd'; +import { FormInstance } from 'antd/lib/form'; + +const FORM_LAYOUT = { + labelCol: { + span: 2, + }, + wrapperCol: { + span: 8, + }, +}; + +type Props = { + form: FormInstance; + disabled?: boolean; +}; + +const Step1: React.FC = ({ form, disabled }) => { + return ( +
+ + + + + + +
+ ); +}; + +export default Step1; diff --git a/src/pages/Consumer/index.ts b/src/pages/Consumer/index.ts new file mode 100644 index 0000000000..4d9cf305bc --- /dev/null +++ b/src/pages/Consumer/index.ts @@ -0,0 +1,2 @@ +export { default as ConsumerZhCN } from './locales/zh-CN'; +export { default as ConsumerEnUS } from './locales/en-US'; diff --git a/src/pages/Consumer/locales/en-US.ts b/src/pages/Consumer/locales/en-US.ts new file mode 100644 index 0000000000..bf969f1a80 --- /dev/null +++ b/src/pages/Consumer/locales/en-US.ts @@ -0,0 +1,6 @@ +export default { + 'menu.consumer': 'Consumer', + 'menu.consumer.list': 'Consumer', + 'menu.consumer.create': 'Create Consumer', + 'menu.consumer.edit': 'Edit Consumer', +}; diff --git a/src/pages/Consumer/locales/zh-CN.ts b/src/pages/Consumer/locales/zh-CN.ts new file mode 100644 index 0000000000..0ac1270c1d --- /dev/null +++ b/src/pages/Consumer/locales/zh-CN.ts @@ -0,0 +1,6 @@ +export default { + 'menu.consumer': 'Consumer', + 'menu.consumer.list': 'Consumer', + 'menu.consumer.create': '创建 Consumer', + 'menu.consumer.edit': '编辑 Consumer', +}; diff --git a/src/pages/Consumer/service.ts b/src/pages/Consumer/service.ts new file mode 100644 index 0000000000..d924a95eab --- /dev/null +++ b/src/pages/Consumer/service.ts @@ -0,0 +1,30 @@ +import { request } from 'umi'; + +export const fetchList = ({ current = 1, pageSize = 10 }, search: string) => + request('/consumers', { + params: { + page: current, + size: pageSize, + search, + }, + }).then(({ list, count }) => ({ + data: list, + total: count, + })); + +export const fetchItem = (id: string) => + request<{ data: ConsumerModule.ResEntity }>(`/consumers/${id}`); + +export const create = (data: ConsumerModule.Entity) => + request('/consumers', { + method: 'POST', + data, + }); + +export const update = (id: string, data: ConsumerModule.Entity) => + request(`/consumers/${id}`, { + method: 'PUT', + data, + }); + +export const remove = (id: string) => request(`/consumers/${id}`, { method: 'DELETE' }); diff --git a/src/pages/Consumer/typing.d.ts b/src/pages/Consumer/typing.d.ts new file mode 100644 index 0000000000..093bf227d4 --- /dev/null +++ b/src/pages/Consumer/typing.d.ts @@ -0,0 +1,15 @@ +declare namespace ConsumerModule { + type Entity = { + username: string; + desc: string; + plugins: { + // TODO: 完善类型 + [name: string]: any; + }; + }; + + type ResEntity = Entity & { + id: string; + update_time: string; + }; +} diff --git a/src/pages/Routes/Create.less b/src/pages/Route/Create.less similarity index 100% rename from src/pages/Routes/Create.less rename to src/pages/Route/Create.less diff --git a/src/pages/Routes/Create.tsx b/src/pages/Route/Create.tsx similarity index 62% rename from src/pages/Routes/Create.tsx rename to src/pages/Route/Create.tsx index 1015358c36..ee612caa00 100644 --- a/src/pages/Routes/Create.tsx +++ b/src/pages/Route/Create.tsx @@ -2,11 +2,19 @@ import React, { useState, useEffect } from 'react'; import { Card, Steps, Form } from 'antd'; import { PageHeaderWrapper } from '@ant-design/pro-layout'; -import { PLUGIN_MAPPER_SOURCE } from '@/components/PluginForm/data'; -import { createRoute, fetchRoute, updateRoute, fetchPluginList } from './service'; +import ActionBar from '@/components/ActionBar'; +import PluginPage from '@/components/PluginPage'; + +import { + create, + fetchItem, + update, + checkUniqueName, + fetchUpstreamItem, + checkHostWithSSL, +} from './service'; import Step1 from './components/Step1'; import Step2 from './components/Step2'; -import CreateStep3 from './components/CreateStep3'; import CreateStep4 from './components/CreateStep4'; import { DEFAULT_STEP_1_DATA, @@ -16,7 +24,6 @@ import { STEP_HEADER_4, } from './constants'; import ResultView from './components/ResultView'; -import ActionBar from './components/ActionBar'; import styles from './Create.less'; const { Step } = Steps; @@ -27,7 +34,7 @@ type Props = { match: any; }; -const Create: React.FC = (props) => { +const Page: React.FC = (props) => { const [step1Data, setStep1Data] = useState(DEFAULT_STEP_1_DATA); const [step2Data, setStep2Data] = useState(DEFAULT_STEP_2_DATA); const [step3Data, setStep3Data] = useState(DEFAULT_STEP_3_DATA); @@ -37,7 +44,7 @@ const Create: React.FC = (props) => { const [form1] = Form.useForm(); const [form2] = Form.useForm(); - const [step, setStep] = useState(0); + const [step, setStep] = useState(1); const [stepHeader, setStepHeader] = useState(STEP_HEADER_4); const routeData = { @@ -46,34 +53,8 @@ const Create: React.FC = (props) => { step3Data, }; - const setupPlugin = () => { - const PLUGIN_BLOCK_LIST = Object.entries(PLUGIN_MAPPER_SOURCE) - .filter(([, value]) => value.hidden) - .flat() - .filter((item) => typeof item === 'string'); - - fetchPluginList().then((data: string[]) => { - const names = data.filter((name) => !PLUGIN_BLOCK_LIST.includes(name)); - - const enabledNames = Object.keys(step3Data.plugins); - const disabledNames = names.filter((name) => !enabledNames.includes(name)); - - setStep3Data({ - plugins: step3Data.plugins, - _disabledPluginList: disabledNames.map((name) => ({ - name, - ...PLUGIN_MAPPER_SOURCE[name], - })), - _enabledPluginList: enabledNames.map((name) => ({ - name, - ...PLUGIN_MAPPER_SOURCE[name], - })), - }); - }); - }; - const setupRoute = (rid: number) => - fetchRoute(rid).then((data) => { + fetchItem(rid).then((data) => { form1.setFieldsValue(data.step1Data); setStep1Data(data.step1Data as RouteModule.Step1Data); @@ -84,10 +65,8 @@ const Create: React.FC = (props) => { }); useEffect(() => { - if (props.route.name === 'edit') { - setupRoute(props.match.params.rid).then(() => setupPlugin()); - } else { - setupPlugin(); + if (props.route.path.indexOf('edit') !== -1) { + setupRoute(props.match.params.rid); } }, []); @@ -103,18 +82,17 @@ const Create: React.FC = (props) => { setStepHeader(STEP_HEADER_4); }, [step1Data]); - // FIXME const onReset = () => { setStep1Data(DEFAULT_STEP_1_DATA); setStep2Data(DEFAULT_STEP_2_DATA); setStep3Data(DEFAULT_STEP_3_DATA); form1.resetFields(); form2.resetFields(); - setStep(0); + setStep(1); }; const renderStep = () => { - if (step === 0) { + if (step === 1) { return ( = (props) => { ); } - if (step === 1) { + if (step === 2) { if (redirect) { return ( {}} redirect /> @@ -137,20 +115,35 @@ const Create: React.FC = (props) => { setStep2Data({ ...step2Data, ...params })} + onChange={(params: RouteModule.Step2Data) => { + if (params.upstream_id) { + fetchUpstreamItem(params.upstream_id).then((data) => { + form2.setFieldsValue({ + ...form2.getFieldsValue(), + ...data, + }); + }); + } + setStep2Data({ ...form2.getFieldsValue(), ...params } as RouteModule.Step2Data); + }} /> ); } - if (step === 2) { - return ; + if (step === 3) { + return ( + setStep3Data({ plugins: data })} + /> + ); } - if (step === 3) { + if (step === 4) { return {}} />; } - if (step === 4) { + if (step === 5) { return ; } @@ -159,30 +152,40 @@ const Create: React.FC = (props) => { const onStepChange = (nextStep: number) => { const onUpdateOrCreate = () => { - if ((props as any).route.name === 'edit') { - updateRoute((props as any).match.params.rid, { data: routeData }).then(() => { - setStep(4); + if (props.route.path.indexOf('edit') !== -1) { + update((props as any).match.params.rid, { data: routeData }).then(() => { + setStep(5); }); } else { - createRoute({ data: routeData }).then(() => { - setStep(4); + create({ data: routeData }).then(() => { + setStep(5); }); } }; - if (nextStep === 0) { + if (nextStep === 1) { setStep(nextStep); } - if (nextStep === 1) { - form1.validateFields().then((value) => { - setStep1Data({ ...step1Data, ...value }); + if (nextStep === 2) { + if (step === 1) { + form1.validateFields().then((value) => { + const { redirectOption, hosts } = value; + Promise.all([ + redirectOption === 'forceHttps' ? checkHostWithSSL(hosts) : Promise.resolve(), + checkUniqueName(value.name, (props as any).match.params.rid || ''), + ]).then(() => { + setStep1Data({ ...step1Data, ...value }); + setStep(nextStep); + }); + }); + } else { setStep(nextStep); - }); + } return; } - if (nextStep === 2) { + if (nextStep === 3) { if (redirect) { onUpdateOrCreate(); return; @@ -194,20 +197,20 @@ const Create: React.FC = (props) => { return; } - if (nextStep === 3) { + if (nextStep === 4) { setStep(nextStep); } - if (nextStep === 4) { + if (nextStep === 5) { onUpdateOrCreate(); } }; return ( <> - + - + {stepHeader.map((item) => ( ))} @@ -215,9 +218,9 @@ const Create: React.FC = (props) => { {renderStep()} - + ); }; -export default Create; +export default Page; diff --git a/src/pages/Route/List.tsx b/src/pages/Route/List.tsx new file mode 100644 index 0000000000..b91cdfe84a --- /dev/null +++ b/src/pages/Route/List.tsx @@ -0,0 +1,113 @@ +import React, { useRef, useState } from 'react'; +import { PageHeaderWrapper } from '@ant-design/pro-layout'; +import ProTable, { ProColumns, ActionType } from '@ant-design/pro-table'; +import { Button, Popconfirm, notification, Tag, Input } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; +import moment from 'moment'; +import { history } from 'umi'; + +import { fetchList, remove } from './service'; + +const Page: React.FC = () => { + const ref = useRef(); + const [search, setSearch] = useState(''); + + const columns: ProColumns[] = [ + { + title: '名称', + dataIndex: 'name', + }, + { + title: '域名', + dataIndex: 'hosts', + render: (_, record) => + record.hosts.map((host) => ( + + {host} + + )), + }, + { + title: '路径', + dataIndex: 'uri', + render: (_, record) => + record.uris.map((uri) => ( + + {uri} + + )), + }, + // { + // title: '优先级', + // dataIndex: 'priority', + // }, + { + title: '描述', + dataIndex: 'description', + }, + { + title: '更新时间', + dataIndex: 'update_time', + render: (text) => `${moment.unix(Number(text)).format('YYYY-MM-DD HH:mm:ss')}`, + }, + { + title: '操作', + valueType: 'option', + render: (_, record) => ( + <> + + { + remove(record.id!).then(() => { + notification.success({ message: '删除记录成功' }); + /* eslint-disable no-unused-expressions */ + ref.current?.reload(); + }); + }} + okText="确定" + cancelText="取消" + > + + + + ), + }, + ]; + + return ( + + + actionRef={ref} + rowKey="name" + columns={columns} + search={false} + request={(params) => fetchList(params, search)} + toolBarRender={(action) => [ + { + setSearch(value); + action.setPageInfo({ page: 1 }); + action.reload(); + }} + />, + , + ]} + /> + + ); +}; + +export default Page; diff --git a/src/pages/Routes/components/CreateStep4/CreateStep4.tsx b/src/pages/Route/components/CreateStep4/CreateStep4.tsx similarity index 85% rename from src/pages/Routes/components/CreateStep4/CreateStep4.tsx rename to src/pages/Route/components/CreateStep4/CreateStep4.tsx index a539b9f629..522995abfe 100644 --- a/src/pages/Routes/components/CreateStep4/CreateStep4.tsx +++ b/src/pages/Route/components/CreateStep4/CreateStep4.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { FormInstance } from 'antd/lib/form'; +import PluginPage from '@/components/PluginPage'; + import Step1 from '../Step1'; import Step2 from '../Step2'; -import CreateStep3 from '../CreateStep3'; interface Props extends RouteModule.Data { form1: FormInstance; @@ -25,7 +26,7 @@ const CreateStep4: React.FC = ({ form1, form2, redirect, ...rest }) => {

定义 API 后端服务

插件配置

- + )} diff --git a/src/pages/Routes/components/CreateStep4/index.ts b/src/pages/Route/components/CreateStep4/index.ts similarity index 100% rename from src/pages/Routes/components/CreateStep4/index.ts rename to src/pages/Route/components/CreateStep4/index.ts diff --git a/src/pages/Routes/components/ResultView/ResultView.tsx b/src/pages/Route/components/ResultView/ResultView.tsx similarity index 94% rename from src/pages/Routes/components/ResultView/ResultView.tsx rename to src/pages/Route/components/ResultView/ResultView.tsx index f4505b25f4..1283799fb7 100644 --- a/src/pages/Routes/components/ResultView/ResultView.tsx +++ b/src/pages/Route/components/ResultView/ResultView.tsx @@ -11,7 +11,7 @@ const ResultView: React.FC = () => ( status="success" title="提交成功" extra={[ - , - { - removeRoute(record.id!).then(() => { - notification.success({ message: '删除路由成功' }); - /* eslint-disable no-unused-expressions */ - ref.current?.reload(); - }); - }} - > - - - - ), - }, - ]; - - return ( - - > - actionRef={ref} - rowKey="name" - request={() => fetchRouteList()} - columns={columns} - search={false} - toolBarRender={() => [ - , - ]} - /> - - ); -}; - -export default RouteList; diff --git a/src/pages/Routes/components/CreateStep3/CreateStep3.tsx b/src/pages/Routes/components/CreateStep3/CreateStep3.tsx deleted file mode 100644 index 59e08c2e2e..0000000000 --- a/src/pages/Routes/components/CreateStep3/CreateStep3.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { useState } from 'react'; -import { SettingOutlined, LinkOutlined } from '@ant-design/icons'; -import { omit, merge } from 'lodash'; - -import { PLUGIN_MAPPER_SOURCE } from '@/components/PluginForm/data'; -import PanelSection from '../PanelSection'; -import PluginDrawer from './PluginDrawer'; -import PluginCard from './PluginCard'; - -interface Props extends RouteModule.Data {} - -const sectionStyle = { - display: 'grid', - gridTemplateColumns: 'repeat(3, 33.333%)', - gridRowGap: 10, - gridColumnGap: 10, -}; - -const CreateStep3: React.FC = ({ data, disabled, onChange }) => { - const [currentPlugin, setCurrentPlugin] = useState(); - const { _disabledPluginList = [], _enabledPluginList = [] } = data.step3Data; - - return ( - <> - - {_enabledPluginList.map(({ name }) => ( - setCurrentPlugin(name)} />, - - window.open( - `https://github.com/apache/incubator-apisix/blob/master/doc/plugins/${name}.md`, - ) - } - />, - ]} - key={name} - /> - ))} - - {!disabled && ( - - {_disabledPluginList.map(({ name }) => ( - setCurrentPlugin(name)} />, - - window.open( - `https://github.com/apache/incubator-apisix/blob/master/doc/plugins/${name}.md`, - ) - } - />, - ]} - key={name} - /> - ))} - - )} - item.name === currentPlugin))} - onActive={(name: string) => { - onChange({ - ...data.step3Data, - _disabledPluginList: _disabledPluginList.filter((item) => item.name !== name), - _enabledPluginList: _enabledPluginList.concat({ name, ...PLUGIN_MAPPER_SOURCE[name] }), - }); - }} - onInactive={(name: string) => { - onChange({ - ...omit({ ...data.step3Data }, `plugins.${currentPlugin}`), - _disabledPluginList: _disabledPluginList.concat({ - name, - ...PLUGIN_MAPPER_SOURCE[name], - }), - _enabledPluginList: _enabledPluginList.filter((item) => item.name !== name), - }); - setCurrentPlugin(undefined); - }} - onClose={() => setCurrentPlugin(undefined)} - onFinish={(value) => { - onChange(merge(data.step3Data, { plugins: { [currentPlugin as string]: value } })); - setCurrentPlugin(undefined); - }} - /> - - ); -}; - -export default CreateStep3; diff --git a/src/pages/Routes/components/CreateStep3/index.ts b/src/pages/Routes/components/CreateStep3/index.ts deleted file mode 100644 index bb56257da6..0000000000 --- a/src/pages/Routes/components/CreateStep3/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './CreateStep3'; diff --git a/src/pages/Routes/service.ts b/src/pages/Routes/service.ts deleted file mode 100644 index 38150c4919..0000000000 --- a/src/pages/Routes/service.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { request } from 'umi'; - -import { transformStepData, transformRouteData } from './transform'; - -export const createRoute = (data: Pick) => - request(`/routes`, { - method: 'POST', - data: transformStepData(data), - }); - -export const updateRoute = (rid: number, data: Pick) => - request(`/routes/${rid}`, { - method: 'PUT', - data: transformStepData(data), - }); - -export const fetchRoute = (rid: number) => - request(`/routes/${rid}`).then((data) => transformRouteData(data)); - -export const fetchRouteList = () => request(`/routes?page=1&size=100000`); - -export const removeRoute = (rid: number) => request(`/routes/${rid}`, { method: 'DELETE' }); - -export const fetchPluginList = () => request('/plugins'); diff --git a/src/pages/ssl/Create.less b/src/pages/SSL/Create.less similarity index 100% rename from src/pages/ssl/Create.less rename to src/pages/SSL/Create.less diff --git a/src/pages/SSL/Create.tsx b/src/pages/SSL/Create.tsx new file mode 100644 index 0000000000..12538b89c6 --- /dev/null +++ b/src/pages/SSL/Create.tsx @@ -0,0 +1,81 @@ +import React, { useState } from 'react'; +import { Card, Steps, notification } from 'antd'; +import { PageHeaderWrapper } from '@ant-design/pro-layout'; +import { useForm } from 'antd/es/form/util'; +import moment from 'moment'; + +import ActionBar from '@/components/ActionBar'; +import { history } from 'umi'; +import Step1 from '@/pages/SSL/components/Step1'; +import Step2 from '@/pages/SSL/components/Step2'; +import { verifyKeyPaire, create, update } from '@/pages/SSL/service'; +import styles from '@/pages/SSL/style.less'; + +const Page: React.FC = (props) => { + const [step, setStep] = useState(1); + const [form] = useForm(); + const { id } = (props as any).match.params; + + const onValidateForm = () => { + let keyPaire = { cert: '', key: '' }; + form + .validateFields() + .then((value) => { + keyPaire = { cert: value.cert, key: value.key }; + return verifyKeyPaire(value.cert, value.key); + }) + .then((_data) => { + const { snis, validity_end } = _data.data; + form.setFieldsValue( + Object.assign({}, form.getFieldsValue(), keyPaire, { + snis, + expireTime: moment.unix(Number(validity_end)).format('YYYY-MM-DD HH:mm:ss'), + }), + ); + setStep(2); + }) + .catch(() => { + notification.warning({ message: '请检查证书内容' }); + }); + }; + + const submit = () => { + const data = form.getFieldsValue(); + const sslData = { + sni: data.snis, + cert: data.cert!, + key: data.key!, + }; + (id ? update(id, sslData) : create(sslData)).then(() => { + history.replace('/ssl/list'); + }); + }; + + const handleStepChange = (nextStep: number) => { + if (nextStep === 2) { + onValidateForm(); + } + if (nextStep === 3) { + submit(); + } + }; + + return ( + <> + + + + {['完善证书信息', '预览'].map((item) => ( + + ))} + + {Boolean(step === 1) && } + {Boolean(step === 2) && } + + + + + ); +}; + +export default Page; diff --git a/src/pages/SSL/List.tsx b/src/pages/SSL/List.tsx new file mode 100644 index 0000000000..44ae420b09 --- /dev/null +++ b/src/pages/SSL/List.tsx @@ -0,0 +1,135 @@ +import React, { useRef, useState } from 'react'; +import { PageHeaderWrapper } from '@ant-design/pro-layout'; +import ProTable, { ProColumns, ActionType } from '@ant-design/pro-table'; +import { Button, Switch, Popconfirm, notification, Tag, Input } from 'antd'; +import { useIntl, history } from 'umi'; +import { PlusOutlined } from '@ant-design/icons'; +import moment from 'moment'; + +import { fetchList as fetchSSLList, remove as removeSSL, switchEnable } from '@/pages/SSL/service'; + +const Page: React.FC = () => { + const [search, setSearch] = useState(''); + + const tableRef = useRef(); + const { formatMessage } = useIntl(); + + const onEnableChange = (id: string, checked: boolean) => { + switchEnable(id, checked) + .then(() => { + notification.success({ message: '更新证书启用状态成功' }); + }) + .catch(() => { + /* eslint-disable no-unused-expressions */ + tableRef.current?.reload(); + }); + }; + + const columns: ProColumns[] = [ + { + title: 'SNI', + dataIndex: 'sni', + render: (_, record) => { + return record.snis.map((sni) => ( + + {sni} + + )); + }, + }, + { + title: '过期时间', + dataIndex: 'validity_end', + hideInSearch: true, + render: (text) => `${moment.unix(Number(text)).format('YYYY-MM-DD HH:mm:ss')}`, + }, + { + title: '是否启用', + dataIndex: 'status', + hideInSearch: true, + render: (text, record) => ( + { + onEnableChange(record.id, checked); + }} + /> + ), + }, + { + title: '更新时间', + dataIndex: 'update_time', + hideInSearch: true, + render: (text) => `${moment.unix(Number(text)).format('YYYY-MM-DD HH:mm:ss')}`, + }, + { + title: formatMessage({ id: 'component.global.action' }), + valueType: 'option', + render: (_, record) => ( + <> + + + removeSSL(record.id).then(() => { + notification.success({ + message: formatMessage({ id: 'component.ssl.removeSSLSuccess' }), + }); + /* eslint-disable no-unused-expressions */ + requestAnimationFrame(() => tableRef.current?.reload()); + }) + } + cancelText="取消" + okText="确定" + > + + + + ), + }, + { + title: '有效期', + dataIndex: 'expire_range', + hideInTable: true, + hideInSearch: true, + }, + ]; + + return ( + + + search={false} + rowKey="id" + columns={columns} + actionRef={tableRef} + request={(params) => fetchSSLList(params, search)} + toolBarRender={(action) => [ + { + setSearch(value); + action.setPageInfo({ page: 1 }); + action.reload(); + }} + />, + , + ]} + /> + + ); +}; + +export default Page; diff --git a/src/pages/ssl/components/CertificateForm/index.tsx b/src/pages/SSL/components/CertificateForm/index.tsx similarity index 77% rename from src/pages/ssl/components/CertificateForm/index.tsx rename to src/pages/SSL/components/CertificateForm/index.tsx index 854bdcece4..d0178d0541 100644 --- a/src/pages/ssl/components/CertificateForm/index.tsx +++ b/src/pages/SSL/components/CertificateForm/index.tsx @@ -1,28 +1,24 @@ import React from 'react'; -import { Form, Input } from 'antd'; +import { Form, Input, Tag } from 'antd'; import { useIntl } from 'umi'; import { FormInstance } from 'antd/lib/form'; -import { FormData } from '../../Create'; interface CertificateFormProps { mode: 'EDIT' | 'VIEW'; - data: FormData; form: FormInstance; } -const CertificateForm: React.FC = ({ mode, data, form }) => { +const CertificateForm: React.FC = ({ mode, form }) => { const { formatMessage } = useIntl(); const renderSNI = () => { if (mode === 'VIEW') { return ( - - + + {(form.getFieldValue('snis') || []).map((item: string) => ( + + {item} + + ))} ); } @@ -33,7 +29,7 @@ const CertificateForm: React.FC = ({ mode, data, form }) = if (mode === 'VIEW') { return ( @@ -45,7 +41,7 @@ const CertificateForm: React.FC = ({ mode, data, form }) = }; return ( -
+ {renderSNI()} = ({ onSuccess, onRemove, dat fileReader.onload = function (event) { const { result } = event.currentTarget as any; if (type === 'PUBLIC_KEY') { - const uploadPublicData: UploadPublicSuccessData = { + const uploadPublicData: SSLModule.UploadPublicSuccessData = { cert: result, publicKeyList: [genUploadFile(fileName)], }; onSuccess(uploadPublicData); } else { - const uploadprivateData: UploadPrivateSuccessData = { + const uploadprivateData: SSLModule.UploadPrivateSuccessData = { key: result, privateKeyList: [genUploadFile(fileName)], }; @@ -94,4 +86,4 @@ const CertificateUploader: React.FC = ({ onSuccess, onRemove, dat ); }; -export { CertificateUploader }; +export default CertificateUploader; diff --git a/src/pages/SSL/components/Step1/index.tsx b/src/pages/SSL/components/Step1/index.tsx new file mode 100644 index 0000000000..37138056e8 --- /dev/null +++ b/src/pages/SSL/components/Step1/index.tsx @@ -0,0 +1,86 @@ +import React, { useState } from 'react'; +import { Form, Select } from 'antd'; +import { UploadFile } from 'antd/lib/upload/interface'; +import { FormInstance } from 'antd/es/form'; + +import CertificateForm from '@/pages/SSL/components/CertificateForm'; +import CertificateUploader, { UploadType } from '@/pages/SSL/components/CertificateUploader'; + +type CreateType = 'Upload' | 'Input'; + +type Props = { + form: FormInstance; +}; + +const Step: React.FC = ({ form }) => { + const [publicKeyList, setPublicKeyList] = useState([]); + const [privateKeyList, setPrivateKeyList] = useState([]); + + const [createType, setCreateType] = useState('Input'); + + const onRemove = (type: UploadType) => { + if (type === 'PUBLIC_KEY') { + form.setFieldsValue({ + cert: '', + sni: '', + expireTime: undefined, + }); + setPublicKeyList([]); + } else { + form.setFieldsValue({ key: '' }); + setPrivateKeyList([]); + } + }; + return ( + <> + + + +
+ +
+ {Boolean(createType === 'Upload') && ( + { + if (cert) { + setPublicKeyList(rest.publicKeyList); + form.setFieldsValue({ cert }); + } else { + form.setFieldsValue({ key }); + setPrivateKeyList(rest.privateKeyList); + } + }} + onRemove={onRemove} + data={{ publicKeyList, privateKeyList }} + /> + )} + + ); +}; +export default Step; diff --git a/src/pages/SSL/components/Step2/index.tsx b/src/pages/SSL/components/Step2/index.tsx new file mode 100644 index 0000000000..d19308541a --- /dev/null +++ b/src/pages/SSL/components/Step2/index.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +import { FormInstance } from 'antd/es/form'; +import CertificateForm from '@/pages/SSL/components/CertificateForm'; + +type Props = { + form: FormInstance; +}; + +const Step: React.FC = ({ form }) => { + return ( +
+ +
+ ); +}; +export default Step; diff --git a/src/pages/ssl/service.ts b/src/pages/SSL/service.ts similarity index 54% rename from src/pages/ssl/service.ts rename to src/pages/SSL/service.ts index b46667c357..a7081a95f5 100644 --- a/src/pages/ssl/service.ts +++ b/src/pages/SSL/service.ts @@ -2,28 +2,19 @@ import { request } from 'umi'; import querystring from 'querystring'; import { identity, pickBy, omit } from 'lodash'; -import { transformFetchItemData } from '@/transforms/global'; - -type FetchListParams = { - current?: number; - pageSize?: number; - sni?: string; - expire_range?: string; - expire_start?: number; - expire_end?: number; - status?: 0; -}; - -export const fetchList = ({ current = 1, pageSize = 10, ...props }: FetchListParams) => { +export const fetchList = ( + { current = 1, pageSize = 10, ...props }: SSLModule.FetchListParams, + search: string, +) => { const [expire_start, expire_end] = (props.expire_range || '').split(':'); let queryObj = omit(props, 'expire_range', '_timestamp'); - queryObj = pickBy(Object.assign({}, queryObj, { expire_start, expire_end }), identity); + queryObj = pickBy(Object.assign({}, queryObj, { expire_start, expire_end, search }), identity); const query = querystring.encode(queryObj); return request<{ count: number; list: SSLModule.ResSSL[] }>( `/ssls?page=${current}&size=${pageSize}&${query}`, ).then((data) => { return { - count: data.count, + total: data.count, data: data.list.map((item) => ({ ...item, sni: item.snis.join(';'), @@ -32,9 +23,6 @@ export const fetchList = ({ current = 1, pageSize = 10, ...props }: FetchListPar }); }; -export const fetchItem = (id: string) => - request(`/ssls/${id}`).then((data) => transformFetchItemData(data)); - export const remove = (id: string) => request(`/ssls/${id}`, { method: 'DELETE', @@ -46,33 +34,26 @@ export const create = (data: SSLModule.SSL) => data, }); -type VerifyKeyPaireProps = { - code: string; - msg: string; - data: { - id: string; - create_time: number; - update_time: number; - validity_start: number; - validity_end: number; - snis: string[]; - status: number; - }; -}; - /** * 1. 校验证书是否匹配 * 2. 解析公钥内容 * */ -export const verifyKeyPaire = (cert = '', key = ''): Promise => +export const verifyKeyPaire = (cert = '', key = ''): Promise => request('/check_ssl_cert', { method: 'POST', data: { cert, key }, }); -export const update = (id: string, checked: boolean) => +export const switchEnable = (id: string, checked: boolean) => request(`/ssls/${id}`, { + method: 'PATCH', data: { status: Number(checked), }, }); + +export const update = (id: string, data: SSLModule.SSL) => + request(`/ssls/${id}`, { + method: 'PUT', + data, + }); diff --git a/src/pages/SSL/style.less b/src/pages/SSL/style.less new file mode 100644 index 0000000000..3f6f0d46f9 --- /dev/null +++ b/src/pages/SSL/style.less @@ -0,0 +1,101 @@ +@import '~antd/es/style/themes/default.less'; + +.card { + margin-bottom: 24px; +} + +.heading { + margin: 0 0 16px 0; + font-size: 14px; + line-height: 22px; +} + +.steps:global(.ant-steps) { + max-width: 750px; + margin: 16px auto; +} + +.errorIcon { + margin-right: 24px; + color: @error-color; + cursor: pointer; + + span.anticon { + margin-right: 4px; + } +} + +.errorPopover { + :global { + .ant-popover-inner-content { + min-width: 256px; + max-height: 290px; + padding: 0; + overflow: auto; + } + } +} + +.errorListItem { + padding: 8px 16px; + list-style: none; + border-bottom: 1px solid @border-color-split; + cursor: pointer; + transition: all 0.3s; + + &:hover { + background: @item-active-bg; + } + + &:last-child { + border: 0; + } + + .errorIcon { + float: left; + margin-top: 4px; + margin-right: 12px; + padding-bottom: 22px; + color: @error-color; + } + + .errorField { + margin-top: 2px; + color: @text-color-secondary; + font-size: 12px; + } +} + +.editable { + td { + padding-top: 13px !important; + padding-bottom: 12.5px !important; + } +} + +// custom footer for fixed footer toolbar +.advancedForm + div { + padding-bottom: 64px; +} + +.advancedForm { + :global { + .ant-form .ant-row:last-child .ant-form-item { + margin-bottom: 24px; + } + + .ant-table td { + transition: none !important; + } + } +} + +.optional { + color: @text-color-secondary; + font-style: normal; +} + +.button-area { + display: flex; + justify-content: center; +} diff --git a/src/pages/SSL/typing.d.ts b/src/pages/SSL/typing.d.ts new file mode 100644 index 0000000000..e8c8d91acb --- /dev/null +++ b/src/pages/SSL/typing.d.ts @@ -0,0 +1,52 @@ +declare namespace SSLModule { + type SSL = { + sni: string[]; + cert: string; + key: string; + }; + + type ResSSL = { + id: string; + create_time: number; + update_time: number; + validity_start: number; + validity_end: number; + status: number; + snis: string[]; + public_key: string; + }; + + type UploadPublicSuccessData = { + cert: string; + publicKeyList: UploadFile[]; + }; + + type UploadPrivateSuccessData = { + key: string; + privateKeyList: UploadFile[]; + }; + + type VerifyKeyPaireProps = { + code: string; + msg: string; + data: { + id: string; + create_time: number; + update_time: number; + validity_start: number; + validity_end: number; + snis: string[]; + status: number; + }; + }; + + type FetchListParams = { + current?: number; + pageSize?: number; + sni?: string; + expire_range?: string; + expire_start?: number; + expire_end?: number; + status?: 0; + }; +} diff --git a/src/pages/Upstream/Create.tsx b/src/pages/Upstream/Create.tsx new file mode 100644 index 0000000000..507bbc3575 --- /dev/null +++ b/src/pages/Upstream/Create.tsx @@ -0,0 +1,67 @@ +import React, { useState, useEffect } from 'react'; +import { PageContainer } from '@ant-design/pro-layout'; +import { Card, Steps, notification } from 'antd'; +import { useForm } from 'antd/es/form/util'; + +import ActionBar from '@/components/ActionBar'; +import { history } from 'umi'; + +import Step1 from './components/Step1'; +import Preview from './components/Preview'; +import { fetchOne, create, update } from './service'; +import { transformCreate, transformFetch } from './transform'; + +const Page: React.FC = (props) => { + const [step, setStep] = useState(1); + const [form1] = useForm(); + + useEffect(() => { + const { id } = (props as any).match.params; + + if (id) { + fetchOne(id).then((data) => { + form1.setFieldsValue(transformFetch(data)); + }); + } + }, []); + + const onSubmit = () => { + const data = transformCreate(Object.assign({}, form1.getFieldsValue()) as UpstreamModule.Body); + const { id } = (props as any).match.params; + (id ? update(id, data) : create(data)).then(() => { + notification.success({ message: `${id ? '更新' : '创建'} 上游成功` }); + history.replace('/upstream/list'); + }); + }; + + const onStepChange = (nextStep: number) => { + if (step === 1) { + form1.validateFields().then(() => { + setStep(nextStep); + }); + } else if (nextStep === 3) { + onSubmit(); + } else { + setStep(nextStep); + } + }; + + return ( + <> + + + + + + + + {step === 1 && } + {step === 2 && } + + + + + ); +}; + +export default Page; diff --git a/src/pages/Upstream/List.tsx b/src/pages/Upstream/List.tsx new file mode 100644 index 0000000000..9b75899210 --- /dev/null +++ b/src/pages/Upstream/List.tsx @@ -0,0 +1,96 @@ +import React, { useRef, useState } from 'react'; +import { PageContainer } from '@ant-design/pro-layout'; +import ProTable, { ProColumns, ActionType } from '@ant-design/pro-table'; +import { PlusOutlined } from '@ant-design/icons'; +import { Popconfirm, Button, notification, Input } from 'antd'; +import { history } from 'umi'; +import moment from 'moment'; + +import { fetchList, remove } from './service'; + +const Page: React.FC = () => { + const ref = useRef(); + + const [search, setSearch] = useState(''); + + const columns: ProColumns[] = [ + { + title: '名称', + dataIndex: 'name', + }, + { + title: '类型', + dataIndex: 'type', + }, + { + title: '描述', + dataIndex: 'description', + }, + { + title: '更新时间', + dataIndex: 'update_time', + render: (text) => `${moment.unix(Number(text)).format('YYYY-MM-DD HH:mm:ss')}`, + }, + { + title: '操作', + valueType: 'option', + render: (_, record) => ( + <> + + { + remove(record.id!).then(() => { + notification.success({ + message: '删除记录成功', + }); + /* eslint-disable no-unused-expressions */ + ref.current?.reload(); + }); + }} + > + + + + ), + }, + ]; + + return ( + + + actionRef={ref} + columns={columns} + rowKey="id" + search={false} + request={(params) => fetchList(params, search)} + toolBarRender={(action) => [ + { + setSearch(value); + action.setPageInfo({ page: 1 }); + action.reload(); + }} + />, + , + ]} + /> + + ); +}; + +export default Page; diff --git a/src/pages/Upstream/components/Preview.tsx b/src/pages/Upstream/components/Preview.tsx new file mode 100644 index 0000000000..5ddc7e2a8f --- /dev/null +++ b/src/pages/Upstream/components/Preview.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { FormInstance } from 'antd/lib/form'; + +import Step1 from './Step1'; + +type Props = { + form1: FormInstance; +}; + +const Page: React.FC = ({ form1 }) => ; + +export default Page; diff --git a/src/pages/Upstream/components/Step1.tsx b/src/pages/Upstream/components/Step1.tsx new file mode 100644 index 0000000000..2c70b8bae8 --- /dev/null +++ b/src/pages/Upstream/components/Step1.tsx @@ -0,0 +1,168 @@ +import React from 'react'; +import { Form, Input, Row, Col, InputNumber, Select } from 'antd'; +import { FormInstance } from 'antd/lib/form'; + +import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; +import Button from 'antd/es/button'; +import { FORM_ITEM_WITHOUT_LABEL, FORM_ITEM_LAYOUT } from '@/pages/Upstream/constants'; + +type Props = { + form: FormInstance; + disabled?: boolean; +}; + +const initialValues = { + name: '', + description: '', + type: 'roundrobin', + upstreamHostList: [{} as UpstreamModule.UpstreamHost], + timeout: { + connect: 6000, + send: 6000, + read: 6000, + }, +}; + +const Step1: React.FC = ({ form, disabled }) => { + const renderUpstreamMeta = () => ( + + {(fields, { add, remove }) => ( + <> + {fields.map((field, index) => ( + + + + + + + + + + + + + + + + + + + {!disabled && + (fields.length > 1 ? ( + { + remove(field.name); + }} + /> + ) : null)} + + + + ))} + {!disabled && ( + + + + )} + + )} + + ); + + const renderTimeUnit = () => ms; + + return ( +
+ + + + + + + + + + {renderUpstreamMeta()} + + + + + {renderTimeUnit()} + + + + + + {renderTimeUnit()} + + + + + + {renderTimeUnit()} + +
+ ); +}; + +export default Step1; diff --git a/src/pages/Upstream/constants.ts b/src/pages/Upstream/constants.ts new file mode 100644 index 0000000000..59d9683a28 --- /dev/null +++ b/src/pages/Upstream/constants.ts @@ -0,0 +1,15 @@ +export const FORM_ITEM_LAYOUT = { + labelCol: { + span: 6, + }, + wrapperCol: { + span: 18, + }, +}; + +export const FORM_ITEM_WITHOUT_LABEL = { + wrapperCol: { + xs: { span: 24, offset: 0 }, + sm: { span: 20, offset: 6 }, + }, +}; diff --git a/src/pages/Upstream/index.ts b/src/pages/Upstream/index.ts new file mode 100644 index 0000000000..e01fb702be --- /dev/null +++ b/src/pages/Upstream/index.ts @@ -0,0 +1,2 @@ +export { default as UpstreamZhCN } from './locales/zh-CN'; +export { default as UpstreamEnUS } from './locales/en-US'; diff --git a/src/pages/Upstream/locales/en-US.ts b/src/pages/Upstream/locales/en-US.ts new file mode 100644 index 0000000000..06a5301c2b --- /dev/null +++ b/src/pages/Upstream/locales/en-US.ts @@ -0,0 +1,6 @@ +export default { + 'menu.upstream': 'Upstream', + 'menu.upstream.list': 'Upstream List', + 'menu.upstream.create': 'Create a Upstream', + 'menu.upstream.edit': 'Edit the Upstream', +}; diff --git a/src/pages/Upstream/locales/zh-CN.ts b/src/pages/Upstream/locales/zh-CN.ts new file mode 100644 index 0000000000..853e63de66 --- /dev/null +++ b/src/pages/Upstream/locales/zh-CN.ts @@ -0,0 +1,6 @@ +export default { + 'menu.upstream': '上游', + 'menu.upstream.list': '上游', + 'menu.upstream.create': '创建上游', + 'menu.upstream.edit': '编辑上游', +}; diff --git a/src/pages/Upstream/service.ts b/src/pages/Upstream/service.ts new file mode 100644 index 0000000000..d2c4b9bd17 --- /dev/null +++ b/src/pages/Upstream/service.ts @@ -0,0 +1,29 @@ +import { request } from 'umi'; + +export const fetchList = ({ current = 1, pageSize = 10 }, search: string) => + request('/upstreams', { + params: { + page: current, + size: pageSize, + search, + }, + }).then(({ data, count }) => ({ + data, + total: count, + })); + +export const fetchOne = (id: string) => request(`/upstreams/${id}`); + +export const create = (data: UpstreamModule.Entity) => + request('/upstreams', { + method: 'POST', + data, + }); + +export const update = (id: string, data: UpstreamModule.Entity) => + request(`/upstreams/${id}`, { + method: 'PUT', + data, + }); + +export const remove = (id: string) => request(`/upstreams/${id}`, { method: 'DELETE' }); diff --git a/src/pages/Upstream/transform.ts b/src/pages/Upstream/transform.ts new file mode 100644 index 0000000000..351ff1df66 --- /dev/null +++ b/src/pages/Upstream/transform.ts @@ -0,0 +1,35 @@ +import { omit } from 'lodash'; + +const transformUpstreamNodes = ( + nodes: { [key: string]: number } = {}, +): RouteModule.UpstreamHost[] => { + const data: RouteModule.UpstreamHost[] = []; + Object.entries(nodes).forEach(([k, v]) => { + const [host, port] = k.split(':'); + data.push({ host, port: Number(port), weight: Number(v) }); + }); + if (data.length === 0) { + data.push({} as RouteModule.UpstreamHost); + } + return data; +}; + +export const transformCreate = (props: UpstreamModule.Body): UpstreamModule.Entity => { + const nodes = {}; + props.upstreamHostList.forEach((node) => { + nodes[`${node.host}:${node.port}`] = node.weight; + }); + + return { + ...omit(props, 'upstreamHostList'), + nodes, + }; +}; + +export const transformFetch = (props: UpstreamModule.Entity) => { + const upstreamHostList = transformUpstreamNodes(props.nodes); + return { + ...omit(props, 'nodes'), + upstreamHostList, + }; +}; diff --git a/src/pages/Upstream/typing.d.ts b/src/pages/Upstream/typing.d.ts new file mode 100644 index 0000000000..3580d26155 --- /dev/null +++ b/src/pages/Upstream/typing.d.ts @@ -0,0 +1,33 @@ +declare namespace UpstreamModule { + type UpstreamHost = { + host: string; + port: number; + weight: number; + }; + + type Base = { + name: string; + timeout: { + connect: number; + read: number; + send: number; + }; + type: 'roundrobin' | 'chash'; + description: string; + }; + + type Entity = Base & { + nodes: { + [ipWithPort: string]: number; + }; + }; + + type Body = Base & { + upstreamHostList: UpstreamHost[]; + }; + + type ResEntity = Entity & { + id: string; + update_time: string; + }; +} diff --git a/src/pages/ssl/Create.tsx b/src/pages/ssl/Create.tsx deleted file mode 100644 index 69ff3fb942..0000000000 --- a/src/pages/ssl/Create.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { useState } from 'react'; -import { Card, Steps } from 'antd'; -import { PageHeaderWrapper } from '@ant-design/pro-layout'; -import Step1 from './components/Step1'; -import Step2 from './components/Step2'; -import Step3 from './components/Step3'; -import styles from './Create.less'; - -const { Step } = Steps; - -export interface FormData { - sni?: string; - cert?: string; - key?: string; - expireTime?: Date | string; -} - -export interface StepProps { - data: FormData; - onStepChange(step: number): void; - onFormChange(data: FormData, reset?: boolean): void; -} - -const Create: React.FC = () => { - const [currentStep, setCurrentStep] = useState(0); - const [formData, setFormData] = useState>({}); - const setpProps = { - data: formData, - onStepChange: setCurrentStep, - onFormChange: (params: FormData, reset?: boolean) => { - if (reset) { - setFormData({}); - } else { - setFormData({ ...formData, ...params }); - } - }, - }; - - const renderStep = () => { - return ( - <> - {Boolean(currentStep === 0) && } - {Boolean(currentStep === 1) && } - {Boolean(currentStep === 2) && } - - ); - }; - - return ( - - - <> - - {['完善证书信息', '预览', '完成'].map((item) => ( - - ))} - - {renderStep()} - - - - ); -}; - -export default Create; diff --git a/src/pages/ssl/List.tsx b/src/pages/ssl/List.tsx deleted file mode 100644 index 88b1927946..0000000000 --- a/src/pages/ssl/List.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { useRef } from 'react'; -import { PageHeaderWrapper } from '@ant-design/pro-layout'; -import ProTable, { ProColumns, ActionType } from '@ant-design/pro-table'; -import { Button, Switch, Popconfirm, notification, DatePicker } from 'antd'; -import { history, useIntl } from 'umi'; -import { PlusOutlined } from '@ant-design/icons'; - -import { fetchList as fetchSSLList, remove as removeSSL, update as updateSSL } from './service'; - -const List: React.FC = () => { - const tableRef = useRef(); - const { formatMessage } = useIntl(); - - const onEnableChange = (id: string, checked: boolean) => { - updateSSL(id, checked) - .then(() => { - notification.success({ message: '更新证书启用状态成功' }); - }) - .catch(() => { - notification.error({ message: '更新证书启用状态失败' }); - /* eslint-disable no-unused-expressions */ - tableRef.current?.reload(); - }); - }; - - const columns: ProColumns[] = [ - { - title: 'SNI', - dataIndex: 'sni', - }, - { - title: '过期时间', - dataIndex: 'validity_end', - hideInSearch: true, - render: (text) => `${new Date(Number(text) * 1000).toLocaleString()}`, - }, - { - title: '是否启用', - dataIndex: 'status', - render: (text, record) => ( - { - onEnableChange(record.id, checked); - }} - /> - ), - renderFormItem: (_, props) => ( - props.onChange && props.onChange(Number(checked))} /> - ), - }, - { - title: formatMessage({ id: 'component.global.action' }), - valueType: 'option', - render: (_, record) => ( - - removeSSL(record.id).then(() => { - notification.success({ - message: formatMessage({ id: 'component.ssl.removeSSLSuccess' }), - }); - /* eslint-disable no-unused-expressions */ - requestAnimationFrame(() => tableRef.current?.reload()); - }) - } - > - - - ), - }, - { - title: '有效期', - dataIndex: 'expire_range', - hideInTable: true, - renderFormItem: (_, props) => ( - { - const from = range?.[0]?.unix(); - const to = range?.[1]?.unix(); - props.onChange && props.onChange(`${from}:${to}`); - }} - /> - ), - }, - ]; - - return ( - - - request={(params) => fetchSSLList(params)} - search - rowKey="id" - columns={columns} - actionRef={tableRef} - toolBarRender={() => [ - , - ]} - /> - - ); -}; - -export default List; diff --git a/src/pages/ssl/components/Step1/index.tsx b/src/pages/ssl/components/Step1/index.tsx deleted file mode 100644 index a450f224d0..0000000000 --- a/src/pages/ssl/components/Step1/index.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React, { useState } from 'react'; -import { Form, Button, Select } from 'antd'; -import { UploadFile } from 'antd/lib/upload/interface'; - -import CertificateForm from '../CertificateForm'; -import { CertificateUploader, UploadType } from '../CertificateUploader'; -import { StepProps } from '../../Create'; -import { verifyKeyPaire } from '../../service'; - -type CreateType = 'Upload' | 'Input'; - -const Step: React.FC = ({ onStepChange, onFormChange, data }) => { - const [form] = Form.useForm(); - const { validateFields } = form; - - const [publicKeyList, setPublicKeyList] = useState([]); - const [privateKeyList, setPrivateKeyList] = useState([]); - - const [createType, setCreateType] = useState('Input'); - - const onValidateForm = async () => { - let keyPaire = { cert: '', key: '' }; - validateFields() - .then((value) => { - keyPaire = { cert: value.cert, key: value.key }; - return verifyKeyPaire(value.cert, value.key); - }) - .then((_data) => { - const { snis, validity_end } = _data.data; - onFormChange({ - ...keyPaire, - sni: snis.join(';'), - expireTime: new Date(validity_end * 1000).toLocaleString(), - }); - onStepChange(1); - }); - }; - - const onRemove = (type: UploadType) => { - if (type === 'PUBLIC_KEY') { - onFormChange({ - cert: '', - sni: '', - expireTime: undefined, - }); - setPublicKeyList([]); - } else { - onFormChange({ - key: '', - }); - setPrivateKeyList([]); - } - }; - return ( - <> - - - -
- -
- {Boolean(createType === 'Upload') && ( - { - form.setFieldsValue(_data); - if (_data.cert) { - setPublicKeyList(_data.publicKeyList); - } else { - setPrivateKeyList(_data.privateKeyList); - } - }} - onRemove={onRemove} - data={{ publicKeyList, privateKeyList }} - /> - )} -
- - -
- - ); -}; -export default Step; diff --git a/src/pages/ssl/components/Step2/index.tsx b/src/pages/ssl/components/Step2/index.tsx deleted file mode 100644 index f8cef19515..0000000000 --- a/src/pages/ssl/components/Step2/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { Form, Button } from 'antd'; -import { create as createSSL } from '@/pages/ssl/service'; -import CertificateForm from '../CertificateForm'; -import { StepProps } from '../../Create'; - -const Step: React.FC = ({ data, onStepChange }) => { - const [form] = Form.useForm(); - const submit = () => { - createSSL({ - sni: data.sni!.split(';'), - cert: data.cert!, - key: data.key!, - }).then(() => { - onStepChange(2); - }); - }; - return ( -
- -
- - -
-
- ); -}; -export default Step; diff --git a/src/pages/ssl/components/Step3/index.tsx b/src/pages/ssl/components/Step3/index.tsx deleted file mode 100644 index ecb6c221b7..0000000000 --- a/src/pages/ssl/components/Step3/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { Result, Button } from 'antd'; -import { history } from 'umi'; -import { StepProps } from '../../Create'; - -const Step: React.FC = ({ onStepChange, onFormChange }) => { - return ( - { - history.replace('/ssl'); - }} - > - 回到列表页 - , - , - ]} - /> - ); -}; -export default Step; diff --git a/src/pages/ssl/typing.d.ts b/src/pages/ssl/typing.d.ts deleted file mode 100644 index 498defdc58..0000000000 --- a/src/pages/ssl/typing.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -declare namespace SSLModule { - type SSL = { - sni: string[]; - cert: string; - key: string; - }; - - type ResSSL = { - id: string; - create_time: number; - update_time: number; - validity_start: number; - validity_end: number; - status: number; - snis: string[]; - public_key: string; - }; -} diff --git a/yarn.lock b/yarn.lock index 458d0584f3..8348bc7b6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1427,6 +1427,14 @@ pirates "^4.0.0" source-map-support "^0.5.16" +"@babel/runtime-corejs2@^7.8.7": + version "7.10.4" + resolved "https://registry.npm.taobao.org/@babel/runtime-corejs2/download/@babel/runtime-corejs2-7.10.4.tgz?cache=0&sync_timestamp=1593521247063&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fruntime-corejs2%2Fdownload%2F%40babel%2Fruntime-corejs2-7.10.4.tgz#5d48ee239624d511c88208da86c27a161ee01cf7" + integrity sha1-XUjuI5Yk1RHIggjahsJ6Fh7gHPc= + dependencies: + core-js "^2.6.5" + regenerator-runtime "^0.13.4" + "@babel/runtime-corejs3@^7.8.3": version "7.9.6" resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.9.6.tgz#67aded13fffbbc2cb93247388cf84d77a4be9a71" @@ -2043,6 +2051,28 @@ resolved "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a" integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg== +"@rjsf/antd@^2.2.0": + version "2.2.1" + resolved "https://registry.npm.taobao.org/@rjsf/antd/download/@rjsf/antd-2.2.1.tgz#2f0b67e7543aa166933e972792c0e4fd7d721fe7" + integrity sha1-Lwtn51Q6oWaTPpcnksDk/X1yH+c= + +"@rjsf/core@^2.2.0": + version "2.2.1" + resolved "https://registry.npm.taobao.org/@rjsf/core/download/@rjsf/core-2.2.1.tgz#ce6d6b0bd8f16f4c9d8ecad8a2c3023e680eccf8" + integrity sha1-zm1rC9jxb0ydjsrYosMCPmgOzPg= + dependencies: + "@babel/runtime-corejs2" "^7.8.7" + "@types/json-schema" "^7.0.4" + ajv "^6.7.0" + core-js "^2.5.7" + json-schema-merge-allof "^0.6.0" + jsonpointer "^4.0.1" + lodash "^4.17.15" + prop-types "^15.7.2" + react-app-polyfill "^1.0.4" + react-is "^16.9.0" + shortid "^2.2.14" + "@samverschueren/stream-to-observable@^0.3.0": version "0.3.0" resolved "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f" @@ -3843,6 +3873,16 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.5.5, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.7.0: + version "6.12.3" + resolved "https://registry.npm.taobao.org/ajv/download/ajv-6.12.3.tgz?cache=0&sync_timestamp=1593876862902&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fajv%2Fdownload%2Fajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706" + integrity sha1-GMWvOKER3etPJpe9eNaKvByr1wY= + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + alphanum-sort@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" @@ -4226,9 +4266,9 @@ asap@~1.0.0: resolved "https://registry.npmjs.org/asap/-/asap-1.0.0.tgz#b2a45da5fdfa20b0496fc3768cc27c12fa916a7d" integrity sha1-sqRdpf36ILBJb8N2jMJ8EvqRan0= -asap@~2.0.3: +asap@~2.0.3, asap@~2.0.6: version "2.0.6" - resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + resolved "https://registry.npm.taobao.org/asap/download/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= asn1.js@^4.0.0: @@ -5595,6 +5635,25 @@ compression@1.7.4: safe-buffer "5.1.2" vary "~1.1.2" +compute-gcd@^1.2.0: + version "1.2.0" + resolved "https://registry.npm.taobao.org/compute-gcd/download/compute-gcd-1.2.0.tgz#fc1ede5b65001e950226502f46543863e4fea10e" + integrity sha1-/B7eW2UAHpUCJlAvRlQ4Y+T+oQ4= + dependencies: + validate.io-array "^1.0.3" + validate.io-function "^1.0.2" + validate.io-integer-array "^1.0.0" + +compute-lcm@^1.1.0: + version "1.1.0" + resolved "https://registry.npm.taobao.org/compute-lcm/download/compute-lcm-1.1.0.tgz#abd96d040b41b0a166f89944b5c8b7c511e21ad5" + integrity sha1-q9ltBAtBsKFm+JlEtci3xRHiGtU= + dependencies: + compute-gcd "^1.2.0" + validate.io-array "^1.0.3" + validate.io-function "^1.0.2" + validate.io-integer-array "^1.0.0" + compute-scroll-into-view@^1.0.13: version "1.0.13" resolved "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.13.tgz#be1b1663b0e3f56cd5f7713082549f562a3477e2" @@ -5755,17 +5814,17 @@ core-js@3.1.4: resolved "https://registry.npmjs.org/core-js/-/core-js-3.1.4.tgz#3a2837fc48e582e1ae25907afcd6cf03b0cc7a07" integrity sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ== -core-js@3.6.5, core-js@^3.6.5: +core-js@3.6.5, core-js@^3.5.0, core-js@^3.6.5: version "3.6.5" - resolved "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a" - integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA== + resolved "https://registry.npm.taobao.org/core-js/download/core-js-3.6.5.tgz?cache=0&sync_timestamp=1586450269267&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcore-js%2Fdownload%2Fcore-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a" + integrity sha1-c5XcJzrzf7LlDpvT2f6EEoUjHRo= core-js@^1.0.0: version "1.2.7" resolved "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= -core-js@^2.4.0: +core-js@^2.4.0, core-js@^2.5.7, core-js@^2.6.5: version "2.6.11" resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== @@ -6205,6 +6264,11 @@ date-format@^0.0.0: resolved "https://registry.npmjs.org/date-format/-/date-format-0.0.0.tgz#09206863ab070eb459acea5542cbd856b11966b3" integrity sha1-CSBoY6sHDrRZrOpVQsvYVrEZZrM= +dayjs@^1.8.28: + version "1.8.29" + resolved "https://registry.npm.taobao.org/dayjs/download/dayjs-1.8.29.tgz?cache=0&sync_timestamp=1593704767514&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fdayjs%2Fdownload%2Fdayjs-1.8.29.tgz#5d23e341de6bfbd206c01136d2fb0f01877820f5" + integrity sha1-XSPjQd5r+9IGwBE20vsPAYd4IPU= + debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -10622,6 +10686,22 @@ json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== +json-schema-compare@^0.2.2: + version "0.2.2" + resolved "https://registry.npm.taobao.org/json-schema-compare/download/json-schema-compare-0.2.2.tgz#dd601508335a90c7f4cfadb6b2e397225c908e56" + integrity sha1-3WAVCDNakMf0z622suOXIlyQjlY= + dependencies: + lodash "^4.17.4" + +json-schema-merge-allof@^0.6.0: + version "0.6.0" + resolved "https://registry.npm.taobao.org/json-schema-merge-allof/download/json-schema-merge-allof-0.6.0.tgz#64d48820fec26b228db837475ce3338936bf59a5" + integrity sha1-ZNSIIP7CayKNuDdHXOMziTa/WaU= + dependencies: + compute-lcm "^1.1.0" + json-schema-compare "^0.2.2" + lodash "^4.17.4" + json-schema-ref-parser@^6.1.0: version "6.1.0" resolved "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-6.1.0.tgz#30af34aeab5bee0431da805dac0eb21b574bf63d" @@ -10662,6 +10742,11 @@ json-schema@0.2.3: resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= +json-schema@^0.2.5: + version "0.2.5" + resolved "https://registry.npm.taobao.org/json-schema/download/json-schema-0.2.5.tgz#97997f50972dd0500214e208c407efa4b5d7063b" + integrity sha1-l5l/UJct0FACFOIIxAfvpLXXBjs= + json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" @@ -10715,6 +10800,11 @@ jsonify@~0.0.0: resolved "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= +jsonpointer@^4.0.1: + version "4.1.0" + resolved "https://registry.npm.taobao.org/jsonpointer/download/jsonpointer-4.1.0.tgz#501fb89986a2389765ba09e6053299ceb4f2c2cc" + integrity sha1-UB+4mYaiOJdlugnmBTKZzrTywsw= + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -11930,6 +12020,11 @@ nan@^2.12.1, nan@^2.14.0: resolved "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== +nanoid@^2.1.0: + version "2.1.11" + resolved "https://registry.npm.taobao.org/nanoid/download/nanoid-2.1.11.tgz?cache=0&sync_timestamp=1592015786161&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fnanoid%2Fdownload%2Fnanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280" + integrity sha1-7CS4p1jVkVYVMbQXagHjq08PAoA= + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -13881,6 +13976,13 @@ promise@^7.1.1: dependencies: asap "~2.0.3" +promise@^8.0.3: + version "8.1.0" + resolved "https://registry.npm.taobao.org/promise/download/promise-8.1.0.tgz#697c25c3dfe7435dd79fcd58c38a135888eaf05e" + integrity sha1-aXwlw9/nQ13Xn81Yw4oTWIjq8F4= + dependencies: + asap "~2.0.6" + prompts@^2.0.1: version "2.3.2" resolved "https://registry.npmjs.org/prompts/-/prompts-2.3.2.tgz#480572d89ecf39566d2bd3fe2c9fccb7c4c0b068" @@ -14868,6 +14970,18 @@ rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-app-polyfill@^1.0.4: + version "1.0.6" + resolved "https://registry.npm.taobao.org/react-app-polyfill/download/react-app-polyfill-1.0.6.tgz#890f8d7f2842ce6073f030b117de9130a5f385f0" + integrity sha1-iQ+NfyhCzmBz8DCxF96RMKXzhfA= + dependencies: + core-js "^3.5.0" + object-assign "^4.1.1" + promise "^8.0.3" + raf "^3.4.1" + regenerator-runtime "^0.13.3" + whatwg-fetch "^3.0.0" + react-copy-to-clipboard@^5.0.1: version "5.0.2" resolved "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz#d82a437e081e68dfca3761fbd57dbf2abdda1316" @@ -15325,7 +15439,7 @@ regenerator-runtime@0.13.2: resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447" integrity sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA== -regenerator-runtime@0.13.5, regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.5: +regenerator-runtime@0.13.5, regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.5: version "0.13.5" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== @@ -16265,6 +16379,13 @@ shellwords@^0.1.1: resolved "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== +shortid@^2.2.14: + version "2.2.15" + resolved "https://registry.npm.taobao.org/shortid/download/shortid-2.2.15.tgz#2b902eaa93a69b11120373cd42a1f1fe4437c122" + integrity sha1-K5AuqpOmmxESA3PNQqHx/kQ3wSI= + dependencies: + nanoid "^2.1.0" + side-channel@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947" @@ -18318,6 +18439,36 @@ validate-npm-package-name@^3.0.0: dependencies: builtins "^1.0.3" +validate.io-array@^1.0.3: + version "1.0.6" + resolved "https://registry.npm.taobao.org/validate.io-array/download/validate.io-array-1.0.6.tgz#5b5a2cafd8f8b85abb2f886ba153f2d93a27774d" + integrity sha1-W1osr9j4uFq7L4hroVPy2Tond00= + +validate.io-function@^1.0.2: + version "1.0.2" + resolved "https://registry.npm.taobao.org/validate.io-function/download/validate.io-function-1.0.2.tgz#343a19802ed3b1968269c780e558e93411c0bad7" + integrity sha1-NDoZgC7TsZaCaceA5VjpNBHAutc= + +validate.io-integer-array@^1.0.0: + version "1.0.0" + resolved "https://registry.npm.taobao.org/validate.io-integer-array/download/validate.io-integer-array-1.0.0.tgz#2cabde033293a6bcbe063feafe91eaf46b13a089" + integrity sha1-LKveAzKTpry+Bj/q/pHq9GsToIk= + dependencies: + validate.io-array "^1.0.3" + validate.io-integer "^1.0.4" + +validate.io-integer@^1.0.4: + version "1.0.5" + resolved "https://registry.npm.taobao.org/validate.io-integer/download/validate.io-integer-1.0.5.tgz#168496480b95be2247ec443f2233de4f89878068" + integrity sha1-FoSWSAuVviJH7EQ/IjPeT4mHgGg= + dependencies: + validate.io-number "^1.0.3" + +validate.io-number@^1.0.3: + version "1.0.3" + resolved "https://registry.npm.taobao.org/validate.io-number/download/validate.io-number-1.0.3.tgz#f63ffeda248bf28a67a8d48e0e3b461a1665baf8" + integrity sha1-9j/+2iSL8opnqNSODjtGGhZluvg= + value-equal@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"