You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ju...@apache.org on 2020/05/21 06:47:56 UTC
[incubator-apisix-dashboard] branch next updated: Feat layout (#204)
This is an automated email from the ASF dual-hosted git repository.
juzhiyuan pushed a commit to branch next
in repository https://gitbox.apache.org/repos/asf/incubator-apisix-dashboard.git
The following commit(s) were added to refs/heads/next by this push:
new 62f0658 Feat layout (#204)
62f0658 is described below
commit 62f065857ddaf6d32bf069b45fa3aa54be0a2c45
Author: 琚致远 <ju...@apache.org>
AuthorDate: Thu May 21 14:47:46 2020 +0800
Feat layout (#204)
* feat: remove unused files
* feat: added initial state
* feat: update global
* feat: update layout
* feat: update typings
* fix: routes
* fix: ssl create
* feat: remove unused files
* feat: update login
* codes clean
---
README.md | 2 +-
config/config.ts | 74 ++++-----
config/defaultSettings.ts | 22 +--
src/app.tsx | 44 ++++-
src/components/Authorized/Authorized.tsx | 35 ----
src/components/Authorized/AuthorizedRoute.tsx | 33 ----
src/components/Authorized/CheckPermissions.tsx | 83 ----------
src/components/Authorized/PromiseRender.tsx | 93 -----------
src/components/Authorized/Secured.tsx | 66 --------
src/components/Authorized/index.tsx | 11 --
src/components/Authorized/renderAuthorize.ts | 30 ----
src/components/Footer/index.tsx | 17 ++
src/components/GlobalHeader/AvatarDropdown.tsx | 86 ----------
src/components/GlobalHeader/NoticeIconView.tsx | 171 -------------------
src/components/GlobalHeader/RightContent.tsx | 56 -------
src/components/HeaderSearch/index.less | 30 ----
src/components/HeaderSearch/index.tsx | 105 ------------
src/components/RightContent/AvatarDropdown.tsx | 99 +++++++++++
.../{GlobalHeader => RightContent}/index.less | 35 +---
src/components/RightContent/index.tsx | 51 ++++++
src/components/SelectLang/index.less | 24 ---
src/components/SelectLang/index.tsx | 50 ------
src/global.tsx | 24 +--
src/layouts/BasicLayout.tsx | 155 ------------------
src/layouts/BlankLayout.tsx | 5 -
src/layouts/SecurityLayout.tsx | 57 -------
src/layouts/UserLayout.less | 71 --------
src/layouts/UserLayout.tsx | 77 ---------
src/models/connect.d.ts | 31 ----
src/models/global.ts | 137 ----------------
src/models/login.ts | 87 ----------
src/models/setting.ts | 37 -----
src/models/user.ts | 85 ----------
src/pages/Authorized.tsx | 36 ----
src/pages/document.ejs | 2 +-
src/pages/ssl/components/Step3/index.tsx | 3 +-
.../user/login/components/Login/LoginItem.tsx | 72 +-------
src/pages/user/login/components/Login/LoginTab.tsx | 6 +-
src/pages/user/login/components/Login/index.tsx | 21 +--
src/pages/user/login/index.tsx | 182 +++++++++++++--------
src/pages/user/login/style.less | 80 +++++++--
src/services/API.d.ts | 7 -
src/services/login.ts | 21 ++-
src/services/user.ts | 3 +
src/utils/Authorized.ts | 19 ---
src/utils/authority.ts | 32 ----
46 files changed, 484 insertions(+), 1983 deletions(-)
diff --git a/README.md b/README.md
index 4e0151a..7495abf 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# APISIX Dashboard
+# Apache APISIX Dashboard
This project is initialized with [Ant Design Pro](https://pro.ant.design). Follow is the quick guide for how to use.
diff --git a/config/config.ts b/config/config.ts
index 7524365..8247273 100644
--- a/config/config.ts
+++ b/config/config.ts
@@ -21,12 +21,17 @@ export default defineConfig({
targets: {
ie: 11,
},
+ layout: {
+ name: 'APISIX Dashboard',
+ locale: true,
+ logo: '/favicon.png',
+ },
base: '/dashboard/',
publicPath: '/',
routes: [
{
path: '/user',
- component: '../layouts/UserLayout',
+ layout: false,
routes: [
{
name: 'login',
@@ -37,53 +42,34 @@ export default defineConfig({
},
{
path: '/',
- component: '../layouts/SecurityLayout',
+ redirect: '/settings',
+ },
+ {
+ name: 'settings',
+ path: '/settings',
+ icon: 'setting',
+ component: './Settings',
+ },
+ {
+ name: 'ssl',
+ path: '/ssl',
+ icon: 'BarsOutlined',
routes: [
{
- path: '/',
- component: '../layouts/BasicLayout',
- authority: ['admin', 'user'],
- routes: [
- {
- path: '/',
- redirect: '/settings',
- },
- {
- name: 'settings',
- path: '/settings',
- icon: 'setting',
- component: './Settings',
- },
- {
- name: 'ssl',
- path: '/ssl',
- icon: 'BarsOutlined',
- routes: [
- {
- path: '/ssl',
- redirect: '/ssl/list',
- },
- {
- path: '/ssl/list',
- name: 'list',
- component: './ssl/List',
- hideInMenu: true,
- },
- {
- name: 'create',
- path: '/ssl/create',
- component: './ssl/Create',
- hideInMenu: true,
- },
- ],
- },
- {
- component: './404',
- },
- ],
+ path: '/ssl',
+ redirect: '/ssl/list',
+ },
+ {
+ path: '/ssl/list',
+ name: 'list',
+ component: './ssl/List',
+ hideInMenu: true,
},
{
- component: './404',
+ name: 'create',
+ path: '/ssl/create',
+ component: './ssl/Create',
+ hideInMenu: true,
},
],
},
diff --git a/config/defaultSettings.ts b/config/defaultSettings.ts
index a98401c..0245a76 100644
--- a/config/defaultSettings.ts
+++ b/config/defaultSettings.ts
@@ -1,26 +1,20 @@
-import { Settings as ProSettings } from '@ant-design/pro-layout';
+import { Settings as LayoutSettings } from '@ant-design/pro-layout';
-type DefaultSettings = ProSettings & {
- pwa: boolean;
-};
-
-const defaultSettings: DefaultSettings = {
- navTheme: 'dark',
- // 拂晓蓝
+export default {
+ navTheme: 'light',
primaryColor: '#1890ff',
- layout: 'side',
+ layout: 'mix',
contentWidth: 'Fluid',
fixedHeader: false,
+ autoHideHeader: false,
fixSiderbar: false,
colorWeak: false,
menu: {
locale: true,
},
- title: 'APISIX',
+ title: 'APISIX Dashboard',
pwa: false,
iconfontUrl: '',
+} as LayoutSettings & {
+ pwa: boolean;
};
-
-export { DefaultSettings };
-
-export default defaultSettings;
diff --git a/src/app.tsx b/src/app.tsx
index 1d66ae6..e94290a 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -1,6 +1,48 @@
+import React from 'react';
import { notification } from 'antd';
-import { RequestConfig } from 'umi';
+import { RequestConfig, history } from 'umi';
+import { BasicLayoutProps, Settings as LayoutSettings } from '@ant-design/pro-layout';
+
import { getAdminAPIConfig } from '@/utils/setting';
+import RightContent from '@/components/RightContent';
+import Footer from '@/components/Footer';
+import { queryCurrent } from '@/services/user';
+import defaultSettings from '../config/defaultSettings';
+
+export async function getInitialState(): Promise<{
+ currentUser?: API.CurrentUser;
+ settings?: LayoutSettings;
+}> {
+ // 如果是登录页面,不执行
+ if (history.location.pathname !== '/user/login') {
+ try {
+ const currentUser = await queryCurrent();
+ return {
+ currentUser,
+ settings: defaultSettings,
+ };
+ } catch (error) {
+ history.push('/user/login');
+ }
+ }
+ return {
+ settings: defaultSettings,
+ };
+}
+
+export const layout = ({
+ initialState,
+}: {
+ initialState: { settings?: LayoutSettings };
+}): BasicLayoutProps => {
+ return {
+ rightContentRender: () => <RightContent />,
+ disableContentMargin: false,
+ footerRender: () => <Footer />,
+ menuHeaderRender: undefined,
+ ...initialState?.settings,
+ };
+};
const codeMessage = {
200: '服务器成功返回请求的数据。',
diff --git a/src/components/Authorized/Authorized.tsx b/src/components/Authorized/Authorized.tsx
deleted file mode 100644
index 36af4c6..0000000
--- a/src/components/Authorized/Authorized.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import React from 'react';
-import { Result } from 'antd';
-import check, { IAuthorityType } from './CheckPermissions';
-
-import AuthorizedRoute from './AuthorizedRoute';
-import Secured from './Secured';
-
-interface AuthorizedProps {
- authority: IAuthorityType;
- noMatch?: React.ReactNode;
-}
-
-type IAuthorizedType = React.FunctionComponent<AuthorizedProps> & {
- Secured: typeof Secured;
- check: typeof check;
- AuthorizedRoute: typeof AuthorizedRoute;
-};
-
-const Authorized: React.FunctionComponent<AuthorizedProps> = ({
- children,
- authority,
- noMatch = (
- <Result
- status={403}
- title="403"
- subTitle="Sorry, you are not authorized to access this page."
- />
- ),
-}) => {
- const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children;
- const dom = check(authority, childrenRender, noMatch);
- return <>{dom}</>;
-};
-
-export default Authorized as IAuthorizedType;
diff --git a/src/components/Authorized/AuthorizedRoute.tsx b/src/components/Authorized/AuthorizedRoute.tsx
deleted file mode 100644
index 7743eae..0000000
--- a/src/components/Authorized/AuthorizedRoute.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { Redirect, Route } from 'umi';
-
-import React from 'react';
-import Authorized from './Authorized';
-import { IAuthorityType } from './CheckPermissions';
-
-interface AuthorizedRoutePops {
- currentAuthority: string;
- component: React.ComponentClass<any, any>;
- render: (props: any) => React.ReactNode;
- redirectPath: string;
- authority: IAuthorityType;
-}
-
-const AuthorizedRoute: React.SFC<AuthorizedRoutePops> = ({
- component: Component,
- render,
- authority,
- redirectPath,
- ...rest
-}) => (
- <Authorized
- authority={authority}
- noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />}
- >
- <Route
- {...rest}
- render={(props: any) => (Component ? <Component {...props} /> : render(props))}
- />
- </Authorized>
-);
-
-export default AuthorizedRoute;
diff --git a/src/components/Authorized/CheckPermissions.tsx b/src/components/Authorized/CheckPermissions.tsx
deleted file mode 100644
index caa15a3..0000000
--- a/src/components/Authorized/CheckPermissions.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import React from 'react';
-import { CURRENT } from './renderAuthorize';
-// eslint-disable-next-line import/no-cycle
-import PromiseRender from './PromiseRender';
-
-export type IAuthorityType =
- | undefined
- | string
- | string[]
- | Promise<boolean>
- | ((currentAuthority: string | string[]) => IAuthorityType);
-
-/**
- * 通用权限检查方法
- * Common check permissions method
- * @param { 权限判定 | Permission judgment } authority
- * @param { 你的权限 | Your permission description } currentAuthority
- * @param { 通过的组件 | Passing components } target
- * @param { 未通过的组件 | no pass components } Exception
- */
-const checkPermissions = <T, K>(
- authority: IAuthorityType,
- currentAuthority: string | string[],
- target: T,
- Exception: K,
-): T | K | React.ReactNode => {
- // 没有判定权限.默认查看所有
- // Retirement authority, return target;
- if (!authority) {
- return target;
- }
- // 数组处理
- if (Array.isArray(authority)) {
- if (Array.isArray(currentAuthority)) {
- if (currentAuthority.some(item => authority.includes(item))) {
- return target;
- }
- } else if (authority.includes(currentAuthority)) {
- return target;
- }
- return Exception;
- }
- // string 处理
- if (typeof authority === 'string') {
- if (Array.isArray(currentAuthority)) {
- if (currentAuthority.some(item => authority === item)) {
- return target;
- }
- } else if (authority === currentAuthority) {
- return target;
- }
- return Exception;
- }
- // Promise 处理
- if (authority instanceof Promise) {
- return <PromiseRender<T, K> ok={target} error={Exception} promise={authority} />;
- }
- // Function 处理
- if (typeof authority === 'function') {
- try {
- const bool = authority(currentAuthority);
- // 函数执行后返回值是 Promise
- if (bool instanceof Promise) {
- return <PromiseRender<T, K> ok={target} error={Exception} promise={bool} />;
- }
- if (bool) {
- return target;
- }
- return Exception;
- } catch (error) {
- throw error;
- }
- }
- throw new Error('unsupported parameters');
-};
-
-export { checkPermissions };
-
-function check<T, K>(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode {
- return checkPermissions<T, K>(authority, CURRENT, target, Exception);
-}
-
-export default check;
diff --git a/src/components/Authorized/PromiseRender.tsx b/src/components/Authorized/PromiseRender.tsx
deleted file mode 100644
index 25f2597..0000000
--- a/src/components/Authorized/PromiseRender.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import React from 'react';
-import { Spin } from 'antd';
-import isEqual from 'lodash/isEqual';
-import { isComponentClass } from './Secured';
-// eslint-disable-next-line import/no-cycle
-
-interface PromiseRenderProps<T, K> {
- ok: T;
- error: K;
- promise: Promise<boolean>;
-}
-
-interface PromiseRenderState {
- component: React.ComponentClass | React.FunctionComponent;
-}
-
-export default class PromiseRender<T, K> extends React.Component<
- PromiseRenderProps<T, K>,
- PromiseRenderState
-> {
- state: PromiseRenderState = {
- component: () => null,
- };
-
- componentDidMount() {
- this.setRenderComponent(this.props);
- }
-
- shouldComponentUpdate = (nextProps: PromiseRenderProps<T, K>, nextState: PromiseRenderState) => {
- const { component } = this.state;
- if (!isEqual(nextProps, this.props)) {
- this.setRenderComponent(nextProps);
- }
- if (nextState.component !== component) return true;
- return false;
- };
-
- // set render Component : ok or error
- setRenderComponent(props: PromiseRenderProps<T, K>) {
- const ok = this.checkIsInstantiation(props.ok);
- const error = this.checkIsInstantiation(props.error);
- props.promise
- .then(() => {
- this.setState({
- component: ok,
- });
- return true;
- })
- .catch(() => {
- this.setState({
- component: error,
- });
- });
- }
-
- // Determine whether the incoming component has been instantiated
- // AuthorizedRoute is already instantiated
- // Authorized render is already instantiated, children is no instantiated
- // Secured is not instantiated
- checkIsInstantiation = (
- target: React.ReactNode | React.ComponentClass,
- ): React.FunctionComponent => {
- if (isComponentClass(target)) {
- const Target = target as React.ComponentClass;
- return (props: any) => <Target {...props} />;
- }
- if (React.isValidElement(target)) {
- return (props: any) => React.cloneElement(target, props);
- }
- return () => target as React.ReactNode & null;
- };
-
- render() {
- const { component: Component } = this.state;
- const { ok, error, promise, ...rest } = this.props;
-
- return Component ? (
- <Component {...rest} />
- ) : (
- <div
- style={{
- width: '100%',
- height: '100%',
- margin: 'auto',
- paddingTop: 50,
- textAlign: 'center',
- }}
- >
- <Spin size="large" />
- </div>
- );
- }
-}
diff --git a/src/components/Authorized/Secured.tsx b/src/components/Authorized/Secured.tsx
deleted file mode 100644
index 0bdbbe4..0000000
--- a/src/components/Authorized/Secured.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import React from 'react';
-import CheckPermissions from './CheckPermissions';
-
-/**
- * 默认不能访问任何页面
- * default is "NULL"
- */
-const Exception403 = () => 403;
-
-export const isComponentClass = (component: React.ComponentClass | React.ReactNode): boolean => {
- if (!component) return false;
- const proto = Object.getPrototypeOf(component);
- if (proto === React.Component || proto === Function.prototype) return true;
- return isComponentClass(proto);
-};
-
-// Determine whether the incoming component has been instantiated
-// AuthorizedRoute is already instantiated
-// Authorized render is already instantiated, children is no instantiated
-// Secured is not instantiated
-const checkIsInstantiation = (target: React.ComponentClass | React.ReactNode) => {
- if (isComponentClass(target)) {
- const Target = target as React.ComponentClass;
- return (props: any) => <Target {...props} />;
- }
- if (React.isValidElement(target)) {
- return (props: any) => React.cloneElement(target, props);
- }
- return () => target;
-};
-
-/**
- * 用于判断是否拥有权限访问此 view 权限
- * authority 支持传入 string, () => boolean | Promise
- * e.g. 'user' 只有 user 用户能访问
- * e.g. 'user,admin' user 和 admin 都能访问
- * e.g. ()=>boolean 返回true能访问,返回false不能访问
- * e.g. Promise then 能访问 catch不能访问
- * e.g. authority support incoming string, () => boolean | Promise
- * e.g. 'user' only user user can access
- * e.g. 'user, admin' user and admin can access
- * e.g. () => boolean true to be able to visit, return false can not be accessed
- * e.g. Promise then can not access the visit to catch
- * @param {string | function | Promise} authority
- * @param {ReactNode} error 非必需参数
- */
-const authorize = (authority: string, error?: React.ReactNode) => {
- /**
- * conversion into a class
- * 防止传入字符串时找不到staticContext造成报错
- * String parameters can cause staticContext not found error
- */
- let classError: boolean | React.FunctionComponent = false;
- if (error) {
- classError = (() => error) as React.FunctionComponent;
- }
- if (!authority) {
- throw new Error('authority is required');
- }
- return function decideAuthority(target: React.ComponentClass | React.ReactNode) {
- const component = CheckPermissions(authority, target, classError || Exception403);
- return checkIsInstantiation(component);
- };
-};
-
-export default authorize;
diff --git a/src/components/Authorized/index.tsx b/src/components/Authorized/index.tsx
deleted file mode 100644
index 6703a46..0000000
--- a/src/components/Authorized/index.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import Authorized from './Authorized';
-import Secured from './Secured';
-import check from './CheckPermissions';
-import renderAuthorize from './renderAuthorize';
-
-Authorized.Secured = Secured;
-Authorized.check = check;
-
-const RenderAuthorize = renderAuthorize(Authorized);
-
-export default RenderAuthorize;
diff --git a/src/components/Authorized/renderAuthorize.ts b/src/components/Authorized/renderAuthorize.ts
deleted file mode 100644
index df00875..0000000
--- a/src/components/Authorized/renderAuthorize.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/* eslint-disable eslint-comments/disable-enable-pair */
-/* eslint-disable import/no-mutable-exports */
-let CURRENT: string | string[] = 'NULL';
-
-type CurrentAuthorityType = string | string[] | (() => typeof CURRENT);
-/**
- * use authority or getAuthority
- * @param {string|()=>String} currentAuthority
- */
-const renderAuthorize = <T>(Authorized: T): ((currentAuthority: CurrentAuthorityType) => T) => (
- currentAuthority: CurrentAuthorityType,
-): T => {
- if (currentAuthority) {
- if (typeof currentAuthority === 'function') {
- CURRENT = currentAuthority();
- }
- if (
- Object.prototype.toString.call(currentAuthority) === '[object String]' ||
- Array.isArray(currentAuthority)
- ) {
- CURRENT = currentAuthority as string[];
- }
- } else {
- CURRENT = 'NULL';
- }
- return Authorized;
-};
-
-export { CURRENT };
-export default <T>(Authorized: T) => renderAuthorize<T>(Authorized);
diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx
new file mode 100644
index 0000000..0f6648b
--- /dev/null
+++ b/src/components/Footer/index.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { GithubOutlined } from '@ant-design/icons';
+import { DefaultFooter } from '@ant-design/pro-layout';
+
+export default () => (
+ <DefaultFooter
+ copyright="2020 Apache APISIX"
+ links={[
+ {
+ key: 'GitHub',
+ title: <GithubOutlined />,
+ href: 'https://github.com/apache/incubator-apisix',
+ blankTarget: true,
+ },
+ ]}
+ />
+);
diff --git a/src/components/GlobalHeader/AvatarDropdown.tsx b/src/components/GlobalHeader/AvatarDropdown.tsx
deleted file mode 100644
index 4cf5340..0000000
--- a/src/components/GlobalHeader/AvatarDropdown.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
-import { Avatar, Menu, Spin } from 'antd';
-import { ClickParam } from 'antd/es/menu';
-import React from 'react';
-import { history, connect } from 'umi';
-import { ConnectProps, ConnectState } from '@/models/connect';
-import { CurrentUser } from '@/models/user';
-import HeaderDropdown from '../HeaderDropdown';
-import styles from './index.less';
-
-export interface GlobalHeaderRightProps extends ConnectProps {
- currentUser?: CurrentUser;
- menu?: boolean;
-}
-
-class AvatarDropdown extends React.Component<GlobalHeaderRightProps> {
- onMenuClick = (event: ClickParam) => {
- const { key } = event;
-
- if (key === 'logout') {
- const { dispatch } = this.props;
-
- if (dispatch) {
- dispatch({
- type: 'login/logout',
- });
- }
-
- return;
- }
-
- history.push(`/account/${key}`);
- };
-
- render(): React.ReactNode {
- const {
- currentUser = {
- avatar: '',
- name: '',
- },
- menu,
- } = this.props;
- const menuHeaderDropdown = (
- <Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
- {menu && (
- <Menu.Item key="center">
- <UserOutlined />
- 个人中心
- </Menu.Item>
- )}
- {menu && (
- <Menu.Item key="settings">
- <SettingOutlined />
- 个人设置
- </Menu.Item>
- )}
- {menu && <Menu.Divider />}
-
- <Menu.Item key="logout">
- <LogoutOutlined />
- 退出登录
- </Menu.Item>
- </Menu>
- );
- return currentUser && currentUser.name ? (
- <HeaderDropdown overlay={menuHeaderDropdown}>
- <span className={`${styles.action} ${styles.account}`}>
- <Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" />
- <span className={styles.name}>{currentUser.name}</span>
- </span>
- </HeaderDropdown>
- ) : (
- <Spin
- size="small"
- style={{
- marginLeft: 8,
- marginRight: 8,
- }}
- />
- );
- }
-}
-
-export default connect(({ user }: ConnectState) => ({
- currentUser: user.currentUser,
-}))(AvatarDropdown);
diff --git a/src/components/GlobalHeader/NoticeIconView.tsx b/src/components/GlobalHeader/NoticeIconView.tsx
deleted file mode 100644
index f450087..0000000
--- a/src/components/GlobalHeader/NoticeIconView.tsx
+++ /dev/null
@@ -1,171 +0,0 @@
-import React, { Component } from 'react';
-import { Tag, message } from 'antd';
-import { connect } from 'umi';
-import groupBy from 'lodash/groupBy';
-import moment from 'moment';
-import { NoticeItem } from '@/models/global';
-import { CurrentUser } from '@/models/user';
-import { ConnectProps, ConnectState } from '@/models/connect';
-import NoticeIcon from '../NoticeIcon';
-import styles from './index.less';
-
-export interface GlobalHeaderRightProps extends ConnectProps {
- notices?: NoticeItem[];
- currentUser?: CurrentUser;
- fetchingNotices?: boolean;
- onNoticeVisibleChange?: (visible: boolean) => void;
- onNoticeClear?: (tabName?: string) => void;
-}
-
-class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
- componentDidMount() {
- const { dispatch } = this.props;
-
- if (dispatch) {
- dispatch({
- type: 'global/fetchNotices',
- });
- }
- }
-
- changeReadState = (clickedItem: NoticeItem): void => {
- const { id } = clickedItem;
- const { dispatch } = this.props;
-
- if (dispatch) {
- dispatch({
- type: 'global/changeNoticeReadState',
- payload: id,
- });
- }
- };
-
- handleNoticeClear = (title: string, key: string) => {
- const { dispatch } = this.props;
- message.success(`${'清空了'} ${title}`);
-
- if (dispatch) {
- dispatch({
- type: 'global/clearNotices',
- payload: key,
- });
- }
- };
-
- getNoticeData = (): {
- [key: string]: NoticeItem[];
- } => {
- const { notices = [] } = this.props;
-
- if (notices.length === 0) {
- return {};
- }
-
- const newNotices = notices.map(notice => {
- const newNotice = { ...notice };
-
- if (newNotice.datetime) {
- newNotice.datetime = moment(notice.datetime as string).fromNow();
- }
-
- if (newNotice.id) {
- newNotice.key = newNotice.id;
- }
-
- if (newNotice.extra && newNotice.status) {
- const color = {
- todo: '',
- processing: 'blue',
- urgent: 'red',
- doing: 'gold',
- }[newNotice.status];
- newNotice.extra = (
- <Tag
- color={color}
- style={{
- marginRight: 0,
- }}
- >
- {newNotice.extra}
- </Tag>
- );
- }
-
- return newNotice;
- });
- return groupBy(newNotices, 'type');
- };
-
- getUnreadData = (noticeData: { [key: string]: NoticeItem[] }) => {
- const unreadMsg: {
- [key: string]: number;
- } = {};
- Object.keys(noticeData).forEach(key => {
- const value = noticeData[key];
-
- if (!unreadMsg[key]) {
- unreadMsg[key] = 0;
- }
-
- if (Array.isArray(value)) {
- unreadMsg[key] = value.filter(item => !item.read).length;
- }
- });
- return unreadMsg;
- };
-
- render() {
- const { currentUser, fetchingNotices, onNoticeVisibleChange } = this.props;
- const noticeData = this.getNoticeData();
- const unreadMsg = this.getUnreadData(noticeData);
- return (
- <NoticeIcon
- className={styles.action}
- count={currentUser && currentUser.unreadCount}
- onItemClick={item => {
- this.changeReadState(item as NoticeItem);
- }}
- loading={fetchingNotices}
- clearText="清空"
- viewMoreText="查看更多"
- onClear={this.handleNoticeClear}
- onPopupVisibleChange={onNoticeVisibleChange}
- onViewMore={() => message.info('Click on view more')}
- clearClose
- >
- <NoticeIcon.Tab
- tabKey="notification"
- count={unreadMsg.notification}
- list={noticeData.notification}
- title="通知"
- emptyText="你已查看所有通知"
- showViewMore
- />
- <NoticeIcon.Tab
- tabKey="message"
- count={unreadMsg.message}
- list={noticeData.message}
- title="消息"
- emptyText="您已读完所有消息"
- showViewMore
- />
- <NoticeIcon.Tab
- tabKey="event"
- title="待办"
- emptyText="你已完成所有待办"
- count={unreadMsg.event}
- list={noticeData.event}
- showViewMore
- />
- </NoticeIcon>
- );
- }
-}
-
-export default connect(({ user, global, loading }: ConnectState) => ({
- currentUser: user.currentUser,
- collapsed: global.collapsed,
- fetchingMoreNotices: loading.effects['global/fetchMoreNotices'],
- fetchingNotices: loading.effects['global/fetchNotices'],
- notices: global.notices,
-}))(GlobalHeaderRight);
diff --git a/src/components/GlobalHeader/RightContent.tsx b/src/components/GlobalHeader/RightContent.tsx
deleted file mode 100644
index 9841729..0000000
--- a/src/components/GlobalHeader/RightContent.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { Tooltip, Tag } from 'antd';
-import { QuestionCircleOutlined } from '@ant-design/icons';
-import React from 'react';
-import { connect } from 'umi';
-import { ConnectProps, ConnectState } from '@/models/connect';
-import Avatar from './AvatarDropdown';
-import SelectLang from '../SelectLang';
-import styles from './index.less';
-
-export type SiderTheme = 'light' | 'dark';
-export interface GlobalHeaderRightProps extends ConnectProps {
- theme?: SiderTheme;
- layout: 'sidemenu' | 'topmenu';
-}
-
-const ENVTagColor = {
- dev: 'orange',
- test: 'green',
- pre: '#87d068',
-};
-
-const GlobalHeaderRight: React.SFC<GlobalHeaderRightProps> = props => {
- const { theme, layout } = props;
- let className = styles.right;
-
- if (theme === 'dark' && layout === 'topmenu') {
- className = `${styles.right} ${styles.dark}`;
- }
-
- return (
- <div className={className}>
- <Tooltip title="Documentation">
- <a
- target="_blank"
- href="https://github.com/apache/incubator-apisix"
- rel="noopener noreferrer"
- className={styles.action}
- >
- <QuestionCircleOutlined />
- </a>
- </Tooltip>
- <Avatar />
- {REACT_APP_ENV && (
- <span>
- <Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>
- </span>
- )}
- <SelectLang className={styles.action} />
- </div>
- );
-};
-
-export default connect(({ settings }: ConnectState) => ({
- theme: settings.navTheme,
- layout: settings.layout,
-}))(GlobalHeaderRight);
diff --git a/src/components/HeaderSearch/index.less b/src/components/HeaderSearch/index.less
deleted file mode 100644
index 9af69d5..0000000
--- a/src/components/HeaderSearch/index.less
+++ /dev/null
@@ -1,30 +0,0 @@
-@import '~antd/es/style/themes/default.less';
-
-.headerSearch {
- .input {
- width: 0;
- min-width: 0;
- overflow: hidden;
- background: transparent;
- border-radius: 0;
- transition: width 0.3s, margin-left 0.3s;
- :global(.ant-select-selection) {
- background: transparent;
- }
- input {
- padding-right: 0;
- padding-left: 0;
- border: 0;
- box-shadow: none !important;
- }
- &,
- &:hover,
- &:focus {
- border-bottom: 1px solid @border-color-base;
- }
- &.show {
- width: 210px;
- margin-left: 8px;
- }
- }
-}
diff --git a/src/components/HeaderSearch/index.tsx b/src/components/HeaderSearch/index.tsx
deleted file mode 100644
index e09cf5b..0000000
--- a/src/components/HeaderSearch/index.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { SearchOutlined } from '@ant-design/icons';
-import { AutoComplete, Input } from 'antd';
-import useMergeValue from 'use-merge-value';
-import { AutoCompleteProps } from 'antd/es/auto-complete';
-import React, { useRef } from 'react';
-
-import classNames from 'classnames';
-import styles from './index.less';
-
-export interface HeaderSearchProps {
- onSearch?: (value?: string) => void;
- onChange?: (value?: string) => void;
- onVisibleChange?: (b: boolean) => void;
- className?: string;
- placeholder?: string;
- options: AutoCompleteProps['options'];
- defaultOpen?: boolean;
- open?: boolean;
- defaultValue?: string;
- value?: string;
-}
-
-const HeaderSearch: React.FC<HeaderSearchProps> = props => {
- const {
- className,
- defaultValue,
- onVisibleChange,
- placeholder,
- open,
- defaultOpen,
- ...restProps
- } = props;
-
- const inputRef = useRef<Input | null>(null);
-
- const [value, setValue] = useMergeValue<string | undefined>(defaultValue, {
- value: props.value,
- onChange: props.onChange,
- });
-
- const [searchMode, setSearchMode] = useMergeValue(defaultOpen || false, {
- value: props.open,
- onChange: onVisibleChange,
- });
-
- const inputClass = classNames(styles.input, {
- [styles.show]: searchMode,
- });
-
- return (
- <div
- className={classNames(className, styles.headerSearch)}
- onClick={() => {
- setSearchMode(true);
- if (searchMode && inputRef.current) {
- inputRef.current.focus();
- }
- }}
- onTransitionEnd={({ propertyName }) => {
- if (propertyName === 'width' && !searchMode) {
- if (onVisibleChange) {
- onVisibleChange(searchMode);
- }
- }
- }}
- >
- <SearchOutlined
- key="Icon"
- style={{
- cursor: 'pointer',
- }}
- />
- <AutoComplete
- key="AutoComplete"
- className={inputClass}
- value={value}
- style={{
- height: 28,
- marginTop: -6,
- }}
- options={restProps.options}
- onChange={setValue}
- >
- <Input
- ref={inputRef}
- defaultValue={defaultValue}
- aria-label={placeholder}
- placeholder={placeholder}
- onKeyDown={e => {
- if (e.key === 'Enter') {
- if (restProps.onSearch) {
- restProps.onSearch(value);
- }
- }
- }}
- onBlur={() => {
- setSearchMode(false);
- }}
- />
- </AutoComplete>
- </div>
- );
-};
-
-export default HeaderSearch;
diff --git a/src/components/RightContent/AvatarDropdown.tsx b/src/components/RightContent/AvatarDropdown.tsx
new file mode 100644
index 0000000..5555355
--- /dev/null
+++ b/src/components/RightContent/AvatarDropdown.tsx
@@ -0,0 +1,99 @@
+import React, { useCallback } from 'react';
+import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
+import { Avatar, Menu, Spin } from 'antd';
+import { ClickParam } from 'antd/es/menu';
+import { history, useModel } from 'umi';
+import { getPageQuery } from '@/utils/utils';
+
+import { stringify } from 'querystring';
+import HeaderDropdown from '../HeaderDropdown';
+import styles from './index.less';
+
+export interface GlobalHeaderRightProps {
+ menu?: boolean;
+}
+
+/**
+ * 退出登录,并且将当前的 url 保存
+ */
+const loginOut = async () => {
+ const { redirect } = getPageQuery();
+ // Note: There may be security issues, please note
+ if (window.location.pathname !== '/user/login' && !redirect) {
+ history.replace({
+ pathname: '/user/login',
+ search: stringify({
+ redirect: window.location.href,
+ }),
+ });
+ }
+};
+
+const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
+ const { initialState, setInitialState } = useModel('@@initialState');
+
+ const onMenuClick = useCallback((event: ClickParam) => {
+ const { key } = event;
+ if (key === 'logout') {
+ setInitialState({ ...initialState, currentUser: undefined });
+ loginOut();
+ return;
+ }
+ history.push(`/account/${key}`);
+ }, []);
+
+ const loading = (
+ <span className={`${styles.action} ${styles.account}`}>
+ <Spin
+ size="small"
+ style={{
+ marginLeft: 8,
+ marginRight: 8,
+ }}
+ />
+ </span>
+ );
+
+ if (!initialState) {
+ return loading;
+ }
+
+ const { currentUser } = initialState;
+
+ if (!currentUser || !currentUser.name) {
+ return loading;
+ }
+
+ const menuHeaderDropdown = (
+ <Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}>
+ {menu && (
+ <Menu.Item key="center">
+ <UserOutlined />
+ 个人中心
+ </Menu.Item>
+ )}
+ {menu && (
+ <Menu.Item key="settings">
+ <SettingOutlined />
+ 个人设置
+ </Menu.Item>
+ )}
+ {menu && <Menu.Divider />}
+
+ <Menu.Item key="logout">
+ <LogoutOutlined />
+ 退出登录
+ </Menu.Item>
+ </Menu>
+ );
+ return (
+ <HeaderDropdown overlay={menuHeaderDropdown}>
+ <span className={`${styles.action} ${styles.account}`}>
+ <Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" />
+ <span className={`${styles.name} anticon`}>{currentUser.name}</span>
+ </span>
+ </HeaderDropdown>
+ );
+};
+
+export default AvatarDropdown;
diff --git a/src/components/GlobalHeader/index.less b/src/components/RightContent/index.less
similarity index 66%
rename from src/components/GlobalHeader/index.less
rename to src/components/RightContent/index.less
index 6a156db..ef2549e 100644
--- a/src/components/GlobalHeader/index.less
+++ b/src/components/RightContent/index.less
@@ -14,20 +14,17 @@
.right {
display: flex;
float: right;
- height: @layout-header-height;
+ height: 48px;
margin-left: auto;
overflow: hidden;
.action {
display: flex;
align-items: center;
- height: 100%;
+ height: 48px;
padding: 0 12px;
cursor: pointer;
transition: all 0.3s;
- > span {
- color: @text-color;
- vertical-align: middle;
- }
+
&:hover {
background: @pro-header-hover-bg;
}
@@ -43,7 +40,6 @@
}
.account {
.avatar {
- margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0;
margin-right: 8px;
color: @primary-color;
vertical-align: top;
@@ -54,30 +50,11 @@
.dark {
.action {
- color: rgba(255, 255, 255, 0.85);
- > span {
- color: rgba(255, 255, 255, 0.85);
+ &:hover {
+ background: #252a3d;
}
- &:hover,
&:global(.opened) {
- background: @primary-color;
- }
- }
-}
-
-:global(.ant-pro-global-header) {
- .dark {
- .action {
- color: @text-color;
- > span {
- color: @text-color;
- }
- &:hover {
- color: rgba(255, 255, 255, 0.85);
- > span {
- color: rgba(255, 255, 255, 0.85);
- }
- }
+ background: #252a3d;
}
}
}
diff --git a/src/components/RightContent/index.tsx b/src/components/RightContent/index.tsx
new file mode 100644
index 0000000..f54e5b7
--- /dev/null
+++ b/src/components/RightContent/index.tsx
@@ -0,0 +1,51 @@
+import { Tooltip, Tag, Space } from 'antd';
+import { QuestionCircleOutlined } from '@ant-design/icons';
+import React from 'react';
+import { useModel, SelectLang } from 'umi';
+import Avatar from './AvatarDropdown';
+import styles from './index.less';
+
+export type SiderTheme = 'light' | 'dark';
+
+const ENVTagColor = {
+ dev: 'orange',
+ test: 'green',
+ pre: '#87d068',
+};
+
+const GlobalHeaderRight: React.FC<{}> = () => {
+ const { initialState } = useModel('@@initialState');
+
+ if (!initialState || !initialState.settings) {
+ return null;
+ }
+
+ const { navTheme, layout } = initialState.settings;
+ let className = styles.right;
+
+ if ((navTheme === 'dark' && layout === 'top') || layout === 'mix') {
+ className = `${styles.right} ${styles.dark}`;
+ }
+ return (
+ <Space className={className}>
+ <Tooltip title="Documentation">
+ <span
+ className={styles.action}
+ onClick={() => {
+ window.location.href = 'https://github.com/apache/incubator-apisix';
+ }}
+ >
+ <QuestionCircleOutlined />
+ </span>
+ </Tooltip>
+ <Avatar />
+ {REACT_APP_ENV && (
+ <span>
+ <Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>
+ </span>
+ )}
+ <SelectLang className={styles.action} />
+ </Space>
+ );
+};
+export default GlobalHeaderRight;
diff --git a/src/components/SelectLang/index.less b/src/components/SelectLang/index.less
deleted file mode 100644
index c0da9b4..0000000
--- a/src/components/SelectLang/index.less
+++ /dev/null
@@ -1,24 +0,0 @@
-@import '~antd/es/style/themes/default.less';
-
-.menu {
- :global(.anticon) {
- margin-right: 8px;
- }
- :global(.ant-dropdown-menu-item) {
- min-width: 160px;
- }
-}
-
-.dropDown {
- line-height: @layout-header-height;
- vertical-align: top;
- cursor: pointer;
- > span {
- font-size: 16px !important;
- transform: none !important;
- svg {
- position: relative;
- top: -1px;
- }
- }
-}
diff --git a/src/components/SelectLang/index.tsx b/src/components/SelectLang/index.tsx
deleted file mode 100644
index fbb6658..0000000
--- a/src/components/SelectLang/index.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { GlobalOutlined } from '@ant-design/icons';
-import { Menu } from 'antd';
-import { getLocale, setLocale } from 'umi';
-import { ClickParam } from 'antd/es/menu';
-import React from 'react';
-import classNames from 'classnames';
-import HeaderDropdown from '../HeaderDropdown';
-import styles from './index.less';
-
-interface SelectLangProps {
- className?: string;
-}
-
-const SelectLang: React.FC<SelectLangProps> = props => {
- const { className } = props;
- const selectedLang = getLocale();
-
- const changeLang = ({ key }: ClickParam): void => setLocale(key);
-
- const locales = ['zh-CN', 'en-US'];
- const languageLabels = {
- 'zh-CN': '简体中文',
- 'en-US': 'English',
- };
- const languageIcons = {
- 'zh-CN': '🇨🇳',
- 'en-US': '🇺🇸',
- };
- const langMenu = (
- <Menu className={styles.menu} selectedKeys={[selectedLang]} onClick={changeLang}>
- {locales.map(locale => (
- <Menu.Item key={locale}>
- <span role="img" aria-label={languageLabels[locale]}>
- {languageIcons[locale]}
- </span>{' '}
- {languageLabels[locale]}
- </Menu.Item>
- ))}
- </Menu>
- );
- return (
- <HeaderDropdown overlay={langMenu} placement="bottomRight">
- <span className={classNames(styles.dropDown, className)}>
- <GlobalOutlined title="语言" />
- </span>
- </HeaderDropdown>
- );
-};
-
-export default SelectLang;
diff --git a/src/global.tsx b/src/global.tsx
index 4c9014c..f7620c4 100644
--- a/src/global.tsx
+++ b/src/global.tsx
@@ -1,15 +1,15 @@
import { Button, message, notification } from 'antd';
+
import React from 'react';
-import { useIntl } from 'umi';
+import { formatMessage } from 'umi';
import defaultSettings from '../config/defaultSettings';
const { pwa } = defaultSettings;
-
// if pwa is true
if (pwa) {
// Notify user if offline now
window.addEventListener('sw.offline', () => {
- message.warning(useIntl().formatMessage({ id: 'app.pwa.offline' }));
+ message.warning(formatMessage({ id: 'app.pwa.offline' }));
});
// Pop up a prompt on the page asking the user if they want to use the latest version
@@ -25,7 +25,7 @@ if (pwa) {
// Send skip-waiting event to waiting SW with MessageChannel
await new Promise((resolve, reject) => {
const channel = new MessageChannel();
- channel.port1.onmessage = msgEvent => {
+ channel.port1.onmessage = (msgEvent) => {
if (msgEvent.data.error) {
reject(msgEvent.data.error);
} else {
@@ -47,12 +47,12 @@ if (pwa) {
reloadSW();
}}
>
- {useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.ok' })}
+ {formatMessage({ id: 'app.pwa.serviceworker.updated.ok' })}
</Button>
);
notification.open({
- message: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated' }),
- description: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }),
+ message: formatMessage({ id: 'app.pwa.serviceworker.updated' }),
+ description: formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }),
btn,
key,
onClose: async () => {},
@@ -62,20 +62,20 @@ if (pwa) {
// unregister service worker
const { serviceWorker } = navigator;
if (serviceWorker.getRegistrations) {
- serviceWorker.getRegistrations().then(sws => {
- sws.forEach(sw => {
+ serviceWorker.getRegistrations().then((sws) => {
+ sws.forEach((sw) => {
sw.unregister();
});
});
}
- serviceWorker.getRegistration().then(sw => {
+ serviceWorker.getRegistration().then((sw) => {
if (sw) sw.unregister();
});
// remove all caches
if (window.caches && window.caches.keys) {
- caches.keys().then(keys => {
- keys.forEach(key => {
+ caches.keys().then((keys) => {
+ keys.forEach((key) => {
caches.delete(key);
});
});
diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx
deleted file mode 100644
index 6f68a6a..0000000
--- a/src/layouts/BasicLayout.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import ProLayout, {
- MenuDataItem,
- BasicLayoutProps as ProLayoutProps,
- Settings,
- DefaultFooter,
-} from '@ant-design/pro-layout';
-import React, { useEffect } from 'react';
-import { Link, useIntl, connect, Dispatch } from 'umi';
-import { GithubOutlined } from '@ant-design/icons';
-import { Result, Button } from 'antd';
-import Authorized from '@/utils/Authorized';
-import RightContent from '@/components/GlobalHeader/RightContent';
-import { ConnectState } from '@/models/connect';
-import { getAuthorityFromRouter } from '@/utils/utils';
-import logo from '../assets/logo.svg';
-
-const noMatch = (
- <Result
- status={403}
- title="403"
- subTitle="Sorry, you are not authorized to access this page."
- extra={
- <Button type="primary">
- <Link to="/user/login">Go Login</Link>
- </Button>
- }
- />
-);
-export interface BasicLayoutProps extends ProLayoutProps {
- breadcrumbNameMap: {
- [path: string]: MenuDataItem;
- };
- route: ProLayoutProps['route'] & {
- authority: string[];
- };
- settings: Settings;
- dispatch: Dispatch;
-}
-export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
- breadcrumbNameMap: {
- [path: string]: MenuDataItem;
- };
-};
-/**
- * use Authorized check all menu item
- */
-
-const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] =>
- menuList.map(item => {
- const localItem = { ...item, children: item.children ? menuDataRender(item.children) : [] };
- return Authorized.check(item.authority, localItem, null) as MenuDataItem;
- });
-
-const defaultFooterDom = (
- <DefaultFooter
- copyright="2020 Apache APISIX"
- links={[
- {
- key: 'GitHub',
- title: <GithubOutlined />,
- href: 'https://github.com/apache/incubator-apisix',
- blankTarget: true,
- },
- ]}
- />
-);
-
-const BasicLayout: React.FC<BasicLayoutProps> = props => {
- const {
- dispatch,
- children,
- settings,
- location = {
- pathname: '/',
- },
- } = props;
- /**
- * constructor
- */
-
- useEffect(() => {
- if (dispatch) {
- dispatch({
- type: 'user/fetchCurrent',
- });
- }
- }, []);
- /**
- * init variables
- */
-
- const handleMenuCollapse = (payload: boolean): void => {
- if (dispatch) {
- dispatch({
- type: 'global/changeLayoutCollapsed',
- payload,
- });
- }
- }; // get children authority
-
- const authorized = getAuthorityFromRouter(props.route.routes, location.pathname || '/') || {
- authority: undefined,
- };
- const { formatMessage } = useIntl();
-
- return (
- <ProLayout
- logo={logo}
- formatMessage={formatMessage}
- menuHeaderRender={(logoDom, titleDom) => (
- <Link to="/">
- {logoDom}
- {titleDom}
- </Link>
- )}
- onCollapse={handleMenuCollapse}
- menuItemRender={(menuItemProps, defaultDom) => {
- if (menuItemProps.isUrl || menuItemProps.children || !menuItemProps.path) {
- return defaultDom;
- }
-
- return <Link to={menuItemProps.path}>{defaultDom}</Link>;
- }}
- breadcrumbRender={(routers = []) => [
- {
- path: '/',
- breadcrumbName: formatMessage({ id: 'menu.home' }),
- },
- ...routers,
- ]}
- itemRender={(route, params, routes, paths) => {
- const first = routes.indexOf(route) === 0;
- return first ? (
- <Link to={paths.join('/')}>{route.breadcrumbName}</Link>
- ) : (
- <span>{route.breadcrumbName}</span>
- );
- }}
- footerRender={() => defaultFooterDom}
- menuDataRender={menuDataRender}
- rightContentRender={() => <RightContent />}
- {...props}
- {...settings}
- >
- <Authorized authority={authorized!.authority} noMatch={noMatch}>
- {children}
- </Authorized>
- </ProLayout>
- );
-};
-
-export default connect(({ global, settings }: ConnectState) => ({
- collapsed: global.collapsed,
- settings,
-}))(BasicLayout);
diff --git a/src/layouts/BlankLayout.tsx b/src/layouts/BlankLayout.tsx
deleted file mode 100644
index cdc55b0..0000000
--- a/src/layouts/BlankLayout.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import React from 'react';
-
-const Layout: React.FC = ({ children }) => <>{children}</>;
-
-export default Layout;
diff --git a/src/layouts/SecurityLayout.tsx b/src/layouts/SecurityLayout.tsx
deleted file mode 100644
index 6024b65..0000000
--- a/src/layouts/SecurityLayout.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import React from 'react';
-import { PageLoading } from '@ant-design/pro-layout';
-import { Redirect, connect } from 'umi';
-import { stringify } from 'querystring';
-import { ConnectState, ConnectProps } from '@/models/connect';
-import { CurrentUser } from '@/models/user';
-
-interface SecurityLayoutProps extends ConnectProps {
- loading?: boolean;
- currentUser?: CurrentUser;
-}
-
-interface SecurityLayoutState {
- isReady: boolean;
-}
-
-class SecurityLayout extends React.Component<SecurityLayoutProps, SecurityLayoutState> {
- state: SecurityLayoutState = {
- isReady: false,
- };
-
- componentDidMount() {
- this.setState({
- isReady: true,
- });
- const { dispatch } = this.props;
- if (dispatch) {
- dispatch({
- type: 'user/fetchCurrent',
- });
- }
- }
-
- render() {
- const { isReady } = this.state;
- const { children, loading, currentUser } = this.props;
- // You can replace it to your authentication rule (such as check token exists)
- // 你可以把它替换成你自己的登录认证规则(比如判断 token 是否存在)
- const isLogin = currentUser && currentUser.userid;
- const queryString = stringify({
- redirect: window.location.href,
- });
-
- if ((!isLogin && loading) || !isReady) {
- return <PageLoading />;
- }
- if (!isLogin && window.location.pathname !== '/user/login') {
- return <Redirect to={`/user/login?${queryString}`} />;
- }
- return children;
- }
-}
-
-export default connect(({ user, loading }: ConnectState) => ({
- currentUser: user.currentUser,
- loading: loading.models.user,
-}))(SecurityLayout);
diff --git a/src/layouts/UserLayout.less b/src/layouts/UserLayout.less
deleted file mode 100755
index cdc207e..0000000
--- a/src/layouts/UserLayout.less
+++ /dev/null
@@ -1,71 +0,0 @@
-@import '~antd/es/style/themes/default.less';
-
-.container {
- display: flex;
- flex-direction: column;
- height: 100vh;
- overflow: auto;
- background: @layout-body-background;
-}
-
-.lang {
- width: 100%;
- height: 40px;
- line-height: 44px;
- text-align: right;
- :global(.ant-dropdown-trigger) {
- margin-right: 24px;
- }
-}
-
-.content {
- flex: 1;
- padding: 32px 0;
-}
-
-@media (min-width: @screen-md-min) {
- .container {
- background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
- background-repeat: no-repeat;
- background-position: center 110px;
- background-size: 100%;
- }
-
- .content {
- padding: 32px 0 24px;
- }
-}
-
-.top {
- text-align: center;
-}
-
-.header {
- height: 44px;
- line-height: 44px;
- a {
- text-decoration: none;
- }
-}
-
-.logo {
- height: 44px;
- margin-right: 16px;
- vertical-align: top;
-}
-
-.title {
- position: relative;
- top: 2px;
- color: @heading-color;
- font-weight: 600;
- font-size: 33px;
- font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
-}
-
-.desc {
- margin-top: 12px;
- margin-bottom: 40px;
- color: @text-color-secondary;
- font-size: @font-size-base;
-}
diff --git a/src/layouts/UserLayout.tsx b/src/layouts/UserLayout.tsx
deleted file mode 100644
index c759ab3..0000000
--- a/src/layouts/UserLayout.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import { DefaultFooter, MenuDataItem, getMenuData, getPageTitle } from '@ant-design/pro-layout';
-import { Helmet, HelmetProvider } from 'react-helmet-async';
-import { Link, useIntl, ConnectProps, connect } from 'umi';
-import React from 'react';
-import { GithubOutlined } from '@ant-design/icons';
-import SelectLang from '@/components/SelectLang';
-import { ConnectState } from '@/models/connect';
-import logo from '../assets/logo.svg';
-import styles from './UserLayout.less';
-
-export interface UserLayoutProps extends Partial<ConnectProps> {
- breadcrumbNameMap: {
- [path: string]: MenuDataItem;
- };
-}
-
-const UserLayout: React.FC<UserLayoutProps> = props => {
- const {
- route = {
- routes: [],
- },
- } = props;
- const { routes = [] } = route;
- const {
- children,
- location = {
- pathname: '',
- },
- } = props;
- const { formatMessage } = useIntl();
- const { breadcrumb } = getMenuData(routes);
- const title = getPageTitle({
- pathname: location.pathname,
- formatMessage,
- breadcrumb,
- ...props,
- });
- return (
- <HelmetProvider>
- <Helmet>
- <title>{title}</title>
- <meta name="description" content={title} />
- </Helmet>
-
- <div className={styles.container}>
- <div className={styles.lang}>
- <SelectLang />
- </div>
- <div className={styles.content}>
- <div className={styles.top}>
- <div className={styles.header}>
- <Link to="/">
- <img alt="logo" className={styles.logo} src={logo} />
- <span className={styles.title}>APISIX Dashboard</span>
- </Link>
- </div>
- <div className={styles.desc}>Cloud-Native Microservices API Gateway</div>
- </div>
- {children}
- </div>
- <DefaultFooter
- copyright="2020 Apache APISIX"
- links={[
- {
- key: 'GitHub',
- title: <GithubOutlined />,
- href: 'https://github.com/apache/incubator-apisix',
- blankTarget: true,
- },
- ]}
- />
- </div>
- </HelmetProvider>
- );
-};
-
-export default connect(({ settings }: ConnectState) => ({ ...settings }))(UserLayout);
diff --git a/src/models/connect.d.ts b/src/models/connect.d.ts
deleted file mode 100644
index a0696dc..0000000
--- a/src/models/connect.d.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { MenuDataItem } from '@ant-design/pro-layout';
-import { GlobalModelState } from './global';
-import { DefaultSettings as SettingModelState } from '../../config/defaultSettings';
-import { UserModelState } from './user';
-import { StateType } from './login';
-
-export { GlobalModelState, SettingModelState, UserModelState };
-
-export interface Loading {
- global: boolean;
- effects: { [key: string]: boolean | undefined };
- models: {
- global?: boolean;
- menu?: boolean;
- setting?: boolean;
- user?: boolean;
- login?: boolean;
- };
-}
-
-export interface ConnectState {
- global: GlobalModelState;
- loading: Loading;
- settings: SettingModelState;
- user: UserModelState;
- login: StateType;
-}
-
-export interface Route extends MenuDataItem {
- routes?: Route[];
-}
diff --git a/src/models/global.ts b/src/models/global.ts
deleted file mode 100644
index a9432f6..0000000
--- a/src/models/global.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import { Subscription, Effect, Reducer } from 'umi';
-
-import { queryNotices } from '@/services/user';
-import { ConnectState } from './connect.d';
-
-export interface NoticeItem extends API.NoticeIconData {
- id: string;
- type: string;
- status: string;
-}
-
-export interface GlobalModelState {
- collapsed: boolean;
- notices: NoticeItem[];
-}
-
-export interface GlobalModelType {
- namespace: 'global';
- state: GlobalModelState;
- effects: {
- fetchNotices: Effect;
- clearNotices: Effect;
- changeNoticeReadState: Effect;
- };
- reducers: {
- changeLayoutCollapsed: Reducer<GlobalModelState>;
- saveNotices: Reducer<GlobalModelState>;
- saveClearedNotices: Reducer<GlobalModelState>;
- };
- subscriptions: { setup: Subscription };
-}
-
-const GlobalModel: GlobalModelType = {
- namespace: 'global',
-
- state: {
- collapsed: false,
- notices: [],
- },
-
- effects: {
- *fetchNotices(_, { call, put, select }) {
- const data = yield call(queryNotices);
- yield put({
- type: 'saveNotices',
- payload: data,
- });
- const unreadCount: number = yield select(
- (state: ConnectState) => state.global.notices.filter((item) => !item.read).length,
- );
- yield put({
- type: 'user/changeNotifyCount',
- payload: {
- totalCount: data.length,
- unreadCount,
- },
- });
- },
- *clearNotices({ payload }, { put, select }) {
- yield put({
- type: 'saveClearedNotices',
- payload,
- });
- const count: number = yield select((state: ConnectState) => state.global.notices.length);
- const unreadCount: number = yield select(
- (state: ConnectState) => state.global.notices.filter((item) => !item.read).length,
- );
- yield put({
- type: 'user/changeNotifyCount',
- payload: {
- totalCount: count,
- unreadCount,
- },
- });
- },
- *changeNoticeReadState({ payload }, { put, select }) {
- const notices: NoticeItem[] = yield select((state: ConnectState) =>
- state.global.notices.map((item) => {
- const notice = { ...item };
- if (notice.id === payload) {
- notice.read = true;
- }
- return notice;
- }),
- );
-
- yield put({
- type: 'saveNotices',
- payload: notices,
- });
-
- yield put({
- type: 'user/changeNotifyCount',
- payload: {
- totalCount: notices.length,
- unreadCount: notices.filter((item) => !item.read).length,
- },
- });
- },
- },
-
- reducers: {
- changeLayoutCollapsed(state = { notices: [], collapsed: true }, { payload }): GlobalModelState {
- return {
- ...state,
- collapsed: payload,
- };
- },
- saveNotices(state, { payload }): GlobalModelState {
- return {
- collapsed: false,
- ...state,
- notices: payload,
- };
- },
- saveClearedNotices(state = { notices: [], collapsed: true }, { payload }): GlobalModelState {
- return {
- collapsed: false,
- ...state,
- notices: state.notices.filter((item): boolean => item.type !== payload),
- };
- },
- },
-
- subscriptions: {
- setup({ history }): void {
- // Subscribe history(url) change, trigger `load` action if pathname is `/`
- history.listen(({ pathname, search }): void => {
- if (typeof window.ga !== 'undefined') {
- window.ga('send', 'pageview', pathname + search);
- }
- });
- },
- },
-};
-
-export default GlobalModel;
diff --git a/src/models/login.ts b/src/models/login.ts
deleted file mode 100644
index 446f5ac..0000000
--- a/src/models/login.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { stringify } from 'querystring';
-import { history, Effect, Reducer } from 'umi';
-
-import { fakeAccountLogin } from '@/services/login';
-import { setAuthority } from '@/utils/authority';
-import { getPageQuery } from '@/utils/utils';
-
-export interface StateType {
- status?: 'ok' | 'error';
- type?: string;
- currentAuthority?: 'user' | 'guest' | 'admin';
-}
-
-export interface LoginModelType {
- namespace: string;
- state: StateType;
- effects: {
- login: Effect;
- logout: Effect;
- };
- reducers: {
- changeLoginStatus: Reducer<StateType>;
- };
-}
-
-const Model: LoginModelType = {
- namespace: 'login',
-
- state: {
- status: undefined,
- },
-
- effects: {
- *login({ payload }, { call, put }) {
- const response = yield call(fakeAccountLogin, payload);
- yield put({
- type: 'changeLoginStatus',
- payload: response,
- });
- // Login successfully
- if (response.status === 'ok') {
- const urlParams = new URL(window.location.href);
- const params = getPageQuery();
- let { redirect } = params as { redirect: string };
- if (redirect) {
- const redirectUrlParams = new URL(redirect);
- if (redirectUrlParams.origin === urlParams.origin) {
- redirect = redirect.substr(urlParams.origin.length);
- if (redirect.match(/^\/.*#/)) {
- redirect = redirect.substr(redirect.indexOf('#') + 1);
- }
- } else {
- window.location.href = '/';
- return;
- }
- }
- history.replace(redirect || '/');
- }
- },
-
- logout() {
- const { redirect } = getPageQuery();
- // Note: There may be security issues, please note
- if (window.location.pathname !== '/user/login' && !redirect) {
- history.replace({
- pathname: '/user/login',
- search: stringify({
- redirect: window.location.href,
- }),
- });
- }
- },
- },
-
- reducers: {
- changeLoginStatus(state, { payload }) {
- setAuthority(payload.currentAuthority);
- return {
- ...state,
- status: payload.status,
- type: payload.type,
- };
- },
- },
-};
-
-export default Model;
diff --git a/src/models/setting.ts b/src/models/setting.ts
deleted file mode 100644
index 59fa70a..0000000
--- a/src/models/setting.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { Reducer } from 'umi';
-import defaultSettings, { DefaultSettings } from '../../config/defaultSettings';
-
-export interface SettingModelType {
- namespace: 'settings';
- state: DefaultSettings;
- reducers: {
- changeSetting: Reducer<DefaultSettings>;
- };
-}
-
-const updateColorWeak: (colorWeak: boolean) => void = colorWeak => {
- const root = document.getElementById('root');
- if (root) {
- root.className = colorWeak ? 'colorWeak' : '';
- }
-};
-
-const SettingModel: SettingModelType = {
- namespace: 'settings',
- state: defaultSettings,
- reducers: {
- changeSetting(state = defaultSettings, { payload }) {
- const { colorWeak, contentWidth } = payload;
-
- if (state.contentWidth !== contentWidth && window.dispatchEvent) {
- window.dispatchEvent(new Event('resize'));
- }
- updateColorWeak(!!colorWeak);
- return {
- ...state,
- ...payload,
- };
- },
- },
-};
-export default SettingModel;
diff --git a/src/models/user.ts b/src/models/user.ts
deleted file mode 100644
index ff5160d..0000000
--- a/src/models/user.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import { Effect, Reducer } from 'umi';
-
-import { queryCurrent, query as queryUsers } from '@/services/user';
-
-export interface CurrentUser {
- avatar?: string;
- name?: string;
- title?: string;
- group?: string;
- signature?: string;
- tags?: {
- key: string;
- label: string;
- }[];
- userid?: string;
- unreadCount?: number;
-}
-
-export interface UserModelState {
- currentUser?: CurrentUser;
-}
-
-export interface UserModelType {
- namespace: 'user';
- state: UserModelState;
- effects: {
- fetch: Effect;
- fetchCurrent: Effect;
- };
- reducers: {
- saveCurrentUser: Reducer<UserModelState>;
- changeNotifyCount: Reducer<UserModelState>;
- };
-}
-
-const UserModel: UserModelType = {
- namespace: 'user',
-
- state: {
- currentUser: {},
- },
-
- effects: {
- *fetch(_, { call, put }) {
- const response = yield call(queryUsers);
- yield put({
- type: 'save',
- payload: response,
- });
- },
- *fetchCurrent(_, { call, put }) {
- const response = yield call(queryCurrent);
- yield put({
- type: 'saveCurrentUser',
- payload: response,
- });
- },
- },
-
- reducers: {
- saveCurrentUser(state, action) {
- return {
- ...state,
- currentUser: action.payload || {},
- };
- },
- changeNotifyCount(
- state = {
- currentUser: {},
- },
- action,
- ) {
- return {
- ...state,
- currentUser: {
- ...state.currentUser,
- notifyCount: action.payload.totalCount,
- unreadCount: action.payload.unreadCount,
- },
- };
- },
- },
-};
-
-export default UserModel;
diff --git a/src/pages/Authorized.tsx b/src/pages/Authorized.tsx
deleted file mode 100644
index 468543d..0000000
--- a/src/pages/Authorized.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from 'react';
-import { Redirect, connect, ConnectProps } from 'umi';
-import Authorized from '@/utils/Authorized';
-import { getRouteAuthority } from '@/utils/utils';
-import { ConnectState, UserModelState } from '@/models/connect';
-
-interface AuthComponentProps extends ConnectProps {
- user: UserModelState;
-}
-
-const AuthComponent: React.FC<AuthComponentProps> = ({
- children,
- route = {
- routes: [],
- },
- location = {
- pathname: '',
- },
- user,
-}) => {
- const { currentUser } = user;
- const { routes = [] } = route;
- const isLogin = currentUser && currentUser.name;
- return (
- <Authorized
- authority={getRouteAuthority(location.pathname, routes) || ''}
- noMatch={isLogin ? <Redirect to="/exception/403" /> : <Redirect to="/user/login" />}
- >
- {children}
- </Authorized>
- );
-};
-
-export default connect(({ user }: ConnectState) => ({
- user,
-}))(AuthComponent);
diff --git a/src/pages/document.ejs b/src/pages/document.ejs
index 8c4db94..387a509 100644
--- a/src/pages/document.ejs
+++ b/src/pages/document.ejs
@@ -181,7 +181,7 @@
</div>
<div style="display: flex;justify-content: center;align-items: center;">
<img
- src="https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg"
+ src="/favicon.png"
width="32"
style="margin-right: 8px;"
/>
diff --git a/src/pages/ssl/components/Step3/index.tsx b/src/pages/ssl/components/Step3/index.tsx
index 10e5ab4..e1a921c 100644
--- a/src/pages/ssl/components/Step3/index.tsx
+++ b/src/pages/ssl/components/Step3/index.tsx
@@ -8,8 +8,7 @@ const Step3: React.FC<StepProps> = ({ data, onStepChange }) => {
const [form] = Form.useForm();
const submit = () => {
createSSL({
- // sni: data.sni!.split(';'),
- sni: 'www.baidu.com',
+ sni: data.sni!.split(';'),
cert: data.cert!,
key: data.key!,
}).then(() => {
diff --git a/src/pages/user/login/components/Login/LoginItem.tsx b/src/pages/user/login/components/Login/LoginItem.tsx
index bd027b0..9473240 100644
--- a/src/pages/user/login/components/Login/LoginItem.tsx
+++ b/src/pages/user/login/components/Login/LoginItem.tsx
@@ -1,12 +1,9 @@
-import { Button, Col, Input, Row, Form, message } from 'antd';
-import React, { useState, useCallback, useEffect } from 'react';
-import omit from 'omit.js';
+import { Input, Form } from 'antd';
+import React from 'react';
import { FormItemProps } from 'antd/es/form/FormItem';
-import { getFakeCaptcha } from '@/services/login';
import ItemMap from './map';
import LoginContext, { LoginContextProps } from './LoginContext';
-import styles from './index.less';
export type WrappedLoginItemProps = LoginItemProps;
export type LoginItemKeyType = keyof typeof ItemMap;
@@ -57,9 +54,7 @@ const getFormItemOptions = ({
return options;
};
-const LoginItem: React.FC<LoginItemProps> = props => {
- const [count, setCount] = useState<number>(props.countDown || 0);
- const [timing, setTiming] = useState(false);
+const LoginItem: React.FC<LoginItemProps> = (props) => {
// 这么写是为了防止restProps中 带入 onChange, defaultValue, rules props tabUtil
const {
onChange,
@@ -75,33 +70,6 @@ const LoginItem: React.FC<LoginItemProps> = props => {
...restProps
} = props;
- const onGetCaptcha = useCallback(async (mobile: string) => {
- const result = await getFakeCaptcha(mobile);
- if (result === false) {
- return;
- }
- message.success('获取验证码成功!验证码为:1234');
- setTiming(true);
- }, []);
-
- useEffect(() => {
- let interval: number = 0;
- const { countDown } = props;
- if (timing) {
- interval = window.setInterval(() => {
- setCount(preSecond => {
- if (preSecond <= 1) {
- setTiming(false);
- clearInterval(interval);
- // 重置秒数
- return countDown || 60;
- }
- return preSecond - 1;
- });
- }, 1000);
- }
- return () => clearInterval(interval);
- }, [timing]);
if (!name) {
return null;
}
@@ -109,36 +77,6 @@ const LoginItem: React.FC<LoginItemProps> = props => {
const options = getFormItemOptions(props);
const otherProps = restProps || {};
- if (type === 'Captcha') {
- const inputProps = omit(otherProps, ['onGetCaptcha', 'countDown']);
-
- return (
- <FormItem shouldUpdate>
- {({ getFieldValue }) => (
- <Row gutter={8}>
- <Col span={16}>
- <FormItem name={name} {...options}>
- <Input {...customProps} {...inputProps} />
- </FormItem>
- </Col>
- <Col span={8}>
- <Button
- disabled={timing}
- className={styles.getCaptcha}
- size="large"
- onClick={() => {
- const value = getFieldValue('mobile');
- onGetCaptcha(value);
- }}
- >
- {timing ? `${count} 秒` : '获取验证码'}
- </Button>
- </Col>
- </Row>
- )}
- </FormItem>
- );
- }
return (
<FormItem name={name} {...options}>
<Input {...customProps} {...otherProps} />
@@ -148,11 +86,11 @@ const LoginItem: React.FC<LoginItemProps> = props => {
const LoginItems: Partial<LoginItemType> = {};
-Object.keys(ItemMap).forEach(key => {
+Object.keys(ItemMap).forEach((key) => {
const item = ItemMap[key];
LoginItems[key] = (props: LoginItemProps) => (
<LoginContext.Consumer>
- {context => (
+ {(context) => (
<LoginItem
customProps={item.props}
rules={item.rules}
diff --git a/src/pages/user/login/components/Login/LoginTab.tsx b/src/pages/user/login/components/Login/LoginTab.tsx
index 935f2fd..adbed2a 100644
--- a/src/pages/user/login/components/Login/LoginTab.tsx
+++ b/src/pages/user/login/components/Login/LoginTab.tsx
@@ -18,7 +18,7 @@ interface LoginTabProps extends TabPaneProps {
active?: boolean;
}
-const LoginTab: React.FC<LoginTabProps> = props => {
+const LoginTab: React.FC<LoginTabProps> = (props) => {
useEffect(() => {
const uniqueId = generateId('login-tab-');
const { tabUtil } = props;
@@ -32,9 +32,9 @@ const LoginTab: React.FC<LoginTabProps> = props => {
const WrapContext: React.FC<TabPaneProps> & {
typeName: string;
-} = props => (
+} = (props) => (
<LoginContext.Consumer>
- {value => <LoginTab tabUtil={value.tabUtil} {...props} />}
+ {(value) => <LoginTab tabUtil={value.tabUtil} {...props} />}
</LoginContext.Consumer>
);
diff --git a/src/pages/user/login/components/Login/index.tsx b/src/pages/user/login/components/Login/index.tsx
index 4148c25..8c550e3 100644
--- a/src/pages/user/login/components/Login/index.tsx
+++ b/src/pages/user/login/components/Login/index.tsx
@@ -26,14 +26,12 @@ interface LoginType extends React.FC<LoginProps> {
Submit: typeof LoginSubmit;
UserName: React.FunctionComponent<LoginItemProps>;
Password: React.FunctionComponent<LoginItemProps>;
- Mobile: React.FunctionComponent<LoginItemProps>;
- Captcha: React.FunctionComponent<LoginItemProps>;
}
-const Login: LoginType = props => {
+const Login: LoginType = (props) => {
const { className } = props;
const [tabs, setTabs] = useState<string[]>([]);
- const [active, setActive] = useState();
+ const [active, setActive] = useState({});
const [type, setType] = useMergeValue('', {
value: props.activeKey,
onChange: props.onTabChange,
@@ -57,14 +55,15 @@ const Login: LoginType = props => {
<LoginContext.Provider
value={{
tabUtil: {
- addTab: id => {
+ addTab: (id) => {
setTabs([...tabs, id]);
},
- removeTab: id => {
- setTabs(tabs.filter(currentId => currentId !== id));
+ removeTab: (id) => {
+ setTabs(tabs.filter((currentId) => currentId !== id));
},
},
- updateActive: activeItem => {
+ updateActive: (activeItem) => {
+ if (!active) return;
if (active[type]) {
active[type].push(activeItem);
} else {
@@ -77,7 +76,7 @@ const Login: LoginType = props => {
<div className={classNames(className, styles.login)}>
<Form
form={props.from}
- onFinish={values => {
+ onFinish={(values) => {
if (props.onSubmit) {
props.onSubmit(values as LoginParamsType);
}
@@ -89,7 +88,7 @@ const Login: LoginType = props => {
animated={false}
className={styles.tabs}
activeKey={type}
- onChange={activeKey => {
+ onChange={(activeKey) => {
setType(activeKey);
}}
>
@@ -111,7 +110,5 @@ Login.Submit = LoginSubmit;
Login.UserName = LoginItem.UserName;
Login.Password = LoginItem.Password;
-Login.Mobile = LoginItem.Mobile;
-Login.Captcha = LoginItem.Captcha;
export default Login;
diff --git a/src/pages/user/login/index.tsx b/src/pages/user/login/index.tsx
index 0690290..238dee8 100644
--- a/src/pages/user/login/index.tsx
+++ b/src/pages/user/login/index.tsx
@@ -1,19 +1,13 @@
-import { Alert, Checkbox } from 'antd';
+import { Alert, Checkbox, message } from 'antd';
import React, { useState } from 'react';
-import { connect, Dispatch, useIntl, FormattedMessage } from 'umi';
-import { StateType } from '@/models/login';
-import { LoginParamsType } from '@/services/login';
-import { ConnectState } from '@/models/connect';
-import LoginForm from './components/Login';
-
+import { Link, SelectLang, history, useModel } from 'umi';
+import { getPageQuery } from '@/utils/utils';
+import logo from '@/assets/logo.svg';
+import { fakeAccountLogin } from '@/services/login';
+import LoginFrom from './components/Login';
import styles from './style.less';
-const { Tab, UserName, Password, Submit } = LoginForm;
-interface LoginProps {
- dispatch: Dispatch;
- userLogin: StateType;
- submitting?: boolean;
-}
+const { Tab, UserName, Password, Submit } = LoginFrom;
const LoginMessage: React.FC<{
content: string;
@@ -28,67 +22,121 @@ const LoginMessage: React.FC<{
/>
);
-const Login: React.FC<LoginProps> = (props) => {
- const { userLogin = {}, submitting } = props;
- const { status, type: loginType } = userLogin;
+/**
+ * 此方法会跳转到 redirect 参数所在的位置
+ */
+const replaceGoto = () => {
+ const urlParams = new URL(window.location.href);
+ const params = getPageQuery();
+ let { redirect } = params as { redirect: string };
+ if (redirect) {
+ const redirectUrlParams = new URL(redirect);
+ if (redirectUrlParams.origin === urlParams.origin) {
+ redirect = redirect.substr(urlParams.origin.length);
+ if (redirect.match(/^\/.*#/)) {
+ redirect = redirect.substr(redirect.indexOf('#') + 1);
+ }
+ } else {
+ window.location.href = '/';
+ return;
+ }
+ }
+ history.replace(redirect || '/');
+};
+
+const Login: React.FC<{}> = () => {
+ const [userLoginState, setUserLoginState] = useState<API.LoginStateType>({});
+ const [submitting, setSubmitting] = useState(false);
+
+ const { refresh } = useModel('@@initialState');
const [autoLogin, setAutoLogin] = useState(true);
- const [type, setType] = useState('account');
- const { formatMessage } = useIntl();
+ const [type, setType] = useState<string>('account');
- const handleSubmit = (values: LoginParamsType) => {
- const { dispatch } = props;
- dispatch({
- type: 'login/login',
- payload: { ...values, type },
- });
+ const handleSubmit = async () => {
+ setSubmitting(true);
+ try {
+ // 登录
+ const msg = await fakeAccountLogin();
+ if (msg.status === 'ok') {
+ message.success('登陆成功!');
+ replaceGoto();
+ setTimeout(() => {
+ refresh();
+ }, 0);
+ return;
+ }
+ // 如果失败去设置用户错误信息
+ setUserLoginState(msg);
+ } catch (error) {
+ message.error('登陆失败,请重试!');
+ }
+ setSubmitting(false);
};
+
+ const { status, type: loginType } = userLoginState;
+
return (
- <div className={styles.main}>
- <LoginForm activeKey={type} onTabChange={setType} onSubmit={handleSubmit}>
- <Tab key="account" tab={formatMessage({ id: 'component.user.loginByPassword' })}>
- {status === 'error' && loginType === 'account' && !submitting && (
- <LoginMessage
- content={formatMessage({ id: 'component.user.wrongUsernameOrPassword' })}
- />
- )}
+ <div className={styles.container}>
+ <div className={styles.lang}>
+ <SelectLang />
+ </div>
+ <div className={styles.content}>
+ <div className={styles.top}>
+ <div className={styles.header}>
+ <Link to="/">
+ <img alt="logo" className={styles.logo} src={logo} />
+ <span className={styles.title}>APISIX Dashboard</span>
+ </Link>
+ </div>
+ <div className={styles.desc}>Cloud-Native Microservices API Gateway</div>
+ </div>
+
+ <div className={styles.main}>
+ <LoginFrom activeKey={type} onTabChange={setType} onSubmit={handleSubmit}>
+ <Tab key="account" tab="账户密码登录">
+ {status === 'error' && loginType === 'account' && !submitting && (
+ <LoginMessage content="账户或密码错误" />
+ )}
- <UserName
- name="userName"
- placeholder={formatMessage({ id: 'component.user.username' })}
- defaultValue="admin"
- rules={[
- {
- required: true,
- message: formatMessage({ id: 'component.user.inputUsername' }),
- },
- ]}
- />
- <Password
- name="password"
- placeholder={formatMessage({ id: 'component.user.password' })}
- defaultValue="123456"
- rules={[
- {
- required: true,
- message: formatMessage({ id: 'component.user.inputPassword' }),
- },
- ]}
- />
- </Tab>
- <div>
- <Checkbox checked={autoLogin} onChange={(e) => setAutoLogin(e.target.checked)}>
- <FormattedMessage id="component.user.rememberMe" />
- </Checkbox>
+ <UserName
+ name="userName"
+ placeholder="请输入用户名"
+ rules={[
+ {
+ required: true,
+ message: '请输入用户名!',
+ },
+ ]}
+ />
+ <Password
+ name="password"
+ placeholder="请输入密码"
+ rules={[
+ {
+ required: true,
+ message: '请输入密码!',
+ },
+ ]}
+ />
+ </Tab>
+ <div>
+ <Checkbox checked={autoLogin} onChange={(e) => setAutoLogin(e.target.checked)}>
+ 自动登录
+ </Checkbox>
+ <a
+ style={{
+ float: 'right',
+ }}
+ >
+ 忘记密码
+ </a>
+ </div>
+ <Submit loading={submitting}>登录</Submit>
+ </LoginFrom>
</div>
- <Submit loading={submitting}>
- <FormattedMessage id="component.user.login" />
- </Submit>
- </LoginForm>
+ </div>
</div>
);
};
-export default connect(({ login, loading }: ConnectState) => ({
- userLogin: login,
- submitting: loading.effects['login/login'],
-}))(Login);
+export default Login;
diff --git a/src/pages/user/login/style.less b/src/pages/user/login/style.less
index d9bbbf3..c195315 100644
--- a/src/pages/user/login/style.less
+++ b/src/pages/user/login/style.less
@@ -1,5 +1,75 @@
@import '~antd/es/style/themes/default.less';
+.container {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ overflow: auto;
+ background: @layout-body-background;
+}
+
+.lang {
+ width: 100%;
+ height: 40px;
+ line-height: 44px;
+ text-align: right;
+ :global(.ant-dropdown-trigger) {
+ margin-right: 24px;
+ }
+}
+
+.content {
+ flex: 1;
+ padding: 32px 0;
+}
+
+@media (min-width: @screen-md-min) {
+ .container {
+ background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
+ background-repeat: no-repeat;
+ background-position: center 110px;
+ background-size: 100%;
+ }
+
+ .content {
+ padding: 32px 0 24px;
+ }
+}
+
+.top {
+ text-align: center;
+}
+
+.header {
+ height: 44px;
+ line-height: 44px;
+ a {
+ text-decoration: none;
+ }
+}
+
+.logo {
+ height: 44px;
+ margin-right: 16px;
+ vertical-align: top;
+}
+
+.title {
+ position: relative;
+ top: 2px;
+ color: @heading-color;
+ font-weight: 600;
+ font-size: 33px;
+ font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
+}
+
+.desc {
+ margin-top: 12px;
+ margin-bottom: 40px;
+ color: @text-color-secondary;
+ font-size: @font-size-base;
+}
+
.main {
width: 368px;
margin: 0 auto;
@@ -20,16 +90,6 @@
}
}
- .other {
- margin-top: 24px;
- line-height: 22px;
- text-align: left;
-
- .register {
- float: right;
- }
- }
-
:global {
.antd-pro-login-submit {
width: 100%;
diff --git a/src/services/API.d.ts b/src/services/API.d.ts
index dccbbd9..d31fb9e 100644
--- a/src/services/API.d.ts
+++ b/src/services/API.d.ts
@@ -2,13 +2,6 @@ declare namespace API {
export interface CurrentUser {
avatar?: string;
name?: string;
- title?: string;
- group?: string;
- signature?: string;
- tags?: {
- key: string;
- label: string;
- }[];
userid?: string;
access?: 'user' | 'guest' | 'admin';
unreadCount?: number;
diff --git a/src/services/login.ts b/src/services/login.ts
index a4e081e..94c781b 100644
--- a/src/services/login.ts
+++ b/src/services/login.ts
@@ -1,5 +1,3 @@
-import { request } from 'umi';
-
export interface LoginParamsType {
userName: string;
password: string;
@@ -7,15 +5,16 @@ export interface LoginParamsType {
captcha: string;
}
-export async function fakeAccountLogin() {
+export async function fakeAccountLogin(): Promise<API.LoginStateType> {
// NOTE: APISIX doesn‘t support user login currently, we return fake data directly.
- return {
+ return Promise.resolve({
+ name: 'APISIX User',
+ avatar:
+ 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjxzdmcgdmlld0JveD0iMCAwIDUwMCA1MDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+DQogIDxkZWZzPg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iaWQwIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjI1MTE5LjgiIHkxPSIxMTA1Mi41IiB4Mj0iMjE3MjUuNyIgeTI9IjIxNTUxLjciIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMC4wMjgwOTUsIDAsIDAsIDAuMDI4MDk1LCAtNDkyLjg2NzA5NiwgLTE0NC43Njk4MjEpIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3R5bGU9InN0b3AtY29 [...]
+ userid: '00000001',
+ notifyCount: 12,
+ unreadCount: 11,
+ access: 'admin',
status: 'ok',
- type: 'account',
- currentAuthority: 'admin',
- };
-}
-
-export async function getFakeCaptcha(mobile: string) {
- return request(`/api/login/captcha?mobile=${mobile}`);
+ });
}
diff --git a/src/services/user.ts b/src/services/user.ts
index 870c4c7..3189a54 100644
--- a/src/services/user.ts
+++ b/src/services/user.ts
@@ -11,6 +11,9 @@ export async function queryCurrent() {
avatar:
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjxzdmcgdmlld0JveD0iMCAwIDUwMCA1MDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+DQogIDxkZWZzPg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iaWQwIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjI1MTE5LjgiIHkxPSIxMTA1Mi41IiB4Mj0iMjE3MjUuNyIgeTI9IjIxNTUxLjciIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoMC4wMjgwOTUsIDAsIDAsIDAuMDI4MDk1LCAtNDkyLjg2NzA5NiwgLTE0NC43Njk4MjEpIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3R5bGU9InN0b3AtY29 [...]
userid: '00000001',
+ notifyCount: 12,
+ unreadCount: 11,
+ access: 'admin',
});
}
diff --git a/src/utils/Authorized.ts b/src/utils/Authorized.ts
deleted file mode 100644
index e3b813a..0000000
--- a/src/utils/Authorized.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import RenderAuthorize from '../components/Authorized';
-import { getAuthority } from './authority';
-/* eslint-disable eslint-comments/disable-enable-pair */
-/* eslint-disable import/no-mutable-exports */
-let Authorized = RenderAuthorize(getAuthority());
-
-// Reload the rights component
-const reloadAuthorized = (): void => {
- Authorized = RenderAuthorize(getAuthority());
-};
-
-/**
- * hard code
- * block need it。
- */
-window.reloadAuthorized = reloadAuthorized;
-
-export { reloadAuthorized };
-export default Authorized;
diff --git a/src/utils/authority.ts b/src/utils/authority.ts
deleted file mode 100644
index d99659d..0000000
--- a/src/utils/authority.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { reloadAuthorized } from './Authorized';
-
-// use localStorage to store the authority info, which might be sent from server in actual project.
-export function getAuthority(str?: string): string | string[] {
- const authorityString =
- typeof str === 'undefined' && localStorage ? localStorage.getItem('antd-pro-authority') : str;
- // authorityString could be admin, "admin", ["admin"]
- let authority;
- try {
- if (authorityString) {
- authority = JSON.parse(authorityString);
- }
- } catch (e) {
- authority = authorityString;
- }
- if (typeof authority === 'string') {
- return [authority];
- }
- // preview.pro.ant.design only do not use in your production.
- // preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
- if (!authority && ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') {
- return ['admin'];
- }
- return authority;
-}
-
-export function setAuthority(authority: string | string[]): void {
- const proAuthority = typeof authority === 'string' ? [authority] : authority;
- localStorage.setItem('antd-pro-authority', JSON.stringify(proAuthority));
- // auto reload
- reloadAuthorized();
-}