Просмотр исходного кода

Merge branch 'iapt-platform:agile' into agile

visuddhinanda 3 лет назад
Родитель
Сommit
e79e620f39

+ 1 - 0
dashboard/.env.orig

@@ -8,3 +8,4 @@ PORT=20080
 REACT_APP_DEFAULT_LOCALE=zh-Hans
 REACT_APP_LANGUAGES=en-US,zh-Hans,zh-Hant
 REACT_APP_API_HOST=https://jeremy.spring.wikipali.org
+REACT_APP_ENABLE_LOCAL_TOKEN=true

+ 21 - 11
dashboard/src/App.tsx

@@ -1,31 +1,41 @@
 import { BrowserRouter } from "react-router-dom";
 import { ConfigProvider } from "antd";
 import { IntlProvider } from "react-intl";
+import { Provider } from "react-redux";
+import { pdfjs } from "react-pdf";
 
 import Router from "./Router";
+import store from "./store";
 import locales, {
   get as getLocale,
   DEFAULT as DEFAULT_LOCALE,
 } from "./locales";
+import { API_HOST } from "./request";
+import onLoad from "./load";
 
 import "./App.css";
 
+pdfjs.GlobalWorkerOptions.workerSrc = `${API_HOST}/assets/pdf.worker.min.js`;
+
+onLoad();
 const lang = getLocale();
 const i18n = locales(lang);
 
 function Widget() {
   return (
-    <IntlProvider
-      messages={i18n.messages}
-      locale={lang}
-      defaultLocale={DEFAULT_LOCALE}
-    >
-      <ConfigProvider locale={i18n.antd}>
-        <BrowserRouter basename={process.env.PUBLIC_URL}>
-          <Router />
-        </BrowserRouter>
-      </ConfigProvider>
-    </IntlProvider>
+    <Provider store={store}>
+      <IntlProvider
+        messages={i18n.messages}
+        locale={lang}
+        defaultLocale={DEFAULT_LOCALE}
+      >
+        <ConfigProvider locale={i18n.antd}>
+          <BrowserRouter basename={process.env.PUBLIC_URL}>
+            <Router />
+          </BrowserRouter>
+        </ConfigProvider>
+      </IntlProvider>
+    </Provider>
   );
 }
 

+ 9 - 0
dashboard/src/components/nut/users/SignIn.tsx

@@ -1,6 +1,11 @@
 import { useIntl } from "react-intl";
 import { ProForm, ProFormText } from "@ant-design/pro-components";
 import { message } from "antd";
+import { useNavigate } from "react-router-dom";
+
+import { setTitle } from "../../../reducers/layout";
+import { useAppSelector, useAppDispatch } from "../../../hooks";
+import { signIn, TO_PROFILE } from "../../../reducers/current-user";
 
 interface IFormData {
   email: string;
@@ -8,12 +13,16 @@ interface IFormData {
 }
 const Widget = () => {
   const intl = useIntl();
+  const dispatch = useAppDispatch();
+  const navigate = useNavigate();
 
   return (
     <ProForm<IFormData>
       onFinish={async (values: IFormData) => {
         // TODO
         console.log(values);
+        // dispatch(signIn([user, token]));
+        // navigate(TO_PROFILE);
         message.success(intl.formatMessage({ id: "flashes.success" }));
       }}
     >

+ 5 - 0
dashboard/src/hooks.ts

@@ -0,0 +1,5 @@
+import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
+import type { RootState, AppDispatch } from "./store";
+
+export const useAppDispatch: () => AppDispatch = useDispatch;
+export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

+ 19 - 0
dashboard/src/load.ts

@@ -0,0 +1,19 @@
+import { Empty } from "google-protobuf/google/protobuf/empty_pb";
+import { Duration } from "google-protobuf/google/protobuf/duration_pb";
+
+import { DURATION, get as getToken, signIn } from "./reducers/current-user";
+import { refresh as refreshLayout } from "./reducers/layout";
+import { GRPC_HOST, get as httpGet, grpc_metadata } from "./request";
+import store from "./store";
+
+const init = () => {
+  // TODO ajax get site information, SEE reducers/layout/ISite
+  // store.dispatch(refreshLayout(it));
+
+  if (getToken()) {
+    // TODO get current user profile & new token, SEE reducers/current-user/IUser
+    // store.dispatch(signIn([user, token]));
+  }
+};
+
+export default init;

+ 5 - 0
dashboard/src/pages/nut/users/account-info.tsx

@@ -1,7 +1,12 @@
 import ChangePassword from "../../../components/nut/users/ChangePassword";
 import Profile from "../../../components/nut/users/Profile";
+import { useAppSelector } from "../../../hooks";
+import { currentUser } from "../../../reducers/current-user";
 
 const Widget = () => {
+  const user = useAppSelector(currentUser);
+  console.log(user?.avatar);
+
   return (
     <div>
       <h3> account info</h3>

+ 76 - 0
dashboard/src/reducers/current-user.ts

@@ -0,0 +1,76 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+
+import type { RootState } from "../store";
+
+export const ROLE_ROOT = "root";
+export const ROLE_ADMINISTRATOR = "administrator";
+
+export const TO_SIGN_IN = "/anonymous/users/sign-in";
+export const TO_PROFILE = "/dashboard/users/logs";
+
+const KEY = "token";
+export const DURATION = 60 * 60 * 24;
+
+const IS_LOCAL_ENABLE = process.env.REACT_APP_ENABLE_LOCAL_TOKEN === "true";
+
+export const get = (): string | null => {
+  const token = sessionStorage.getItem(KEY);
+  if (token) {
+    return token;
+  }
+  if (IS_LOCAL_ENABLE) {
+    return localStorage.getItem(KEY);
+  }
+  return null;
+};
+
+const set = (token: string) => {
+  sessionStorage.setItem(KEY, token);
+  if (IS_LOCAL_ENABLE) {
+    localStorage.setItem(KEY, token);
+  }
+};
+
+const remove = () => {
+  sessionStorage.removeItem(KEY);
+  localStorage.removeItem(KEY);
+};
+
+export interface IUser {
+  nickName: string;
+  realName: string;
+  avatar: string;
+  roles: string[];
+}
+
+interface IState {
+  payload?: IUser;
+}
+
+const initialState: IState = {};
+
+export const slice = createSlice({
+  name: "current-user",
+  initialState,
+  reducers: {
+    signIn: (state, action: PayloadAction<[IUser, string]>) => {
+      state.payload = action.payload[0];
+      set(action.payload[1]);
+    },
+    signOut: (state) => {
+      state.payload = undefined;
+      remove();
+    },
+  },
+});
+
+export const { signIn, signOut } = slice.actions;
+
+export const isRoot = (state: RootState): boolean =>
+  state.currentUser.payload?.roles.includes(ROLE_ROOT) || false;
+export const isAdministrator = (state: RootState): boolean =>
+  state.currentUser.payload?.roles.includes(ROLE_ADMINISTRATOR) || false;
+export const currentUser = (state: RootState): IUser | undefined =>
+  state.currentUser.payload;
+
+export default slice.reducer;

+ 50 - 0
dashboard/src/reducers/layout.ts

@@ -0,0 +1,50 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+
+import type { RootState } from "../store";
+
+export interface IAuthor {
+  name: string;
+  email: string;
+}
+
+export interface ISite {
+  logo: string;
+  title: string;
+  subhead: string;
+  keywords: string[];
+  description: string;
+  copyright: string;
+  author: IAuthor;
+}
+
+interface IState {
+  site?: ISite;
+  title?: string;
+}
+
+const initialState: IState = {};
+
+export const slice = createSlice({
+  name: "layout",
+  initialState,
+  reducers: {
+    refresh: (state, action: PayloadAction<ISite>) => {
+      state.site = action.payload;
+    },
+    setTitle: (state, action: PayloadAction<string>) => {
+      state.title = action.payload;
+    },
+  },
+});
+
+export const { refresh, setTitle } = slice.actions;
+
+export const layout = (state: RootState): IState => state.layout;
+
+export const siteInfo = (state: RootState): ISite | undefined =>
+  state.layout.site;
+
+export const pageTitle = (state: RootState): string | undefined =>
+  state.layout.title;
+
+export default slice.reducer;

+ 104 - 0
dashboard/src/request.ts

@@ -0,0 +1,104 @@
+import { Metadata } from "grpc-web";
+import { get as getLocale } from "./locales";
+
+import { get as getToken } from "./reducers/current-user";
+
+export const backend = (u: string) => `${API_HOST}/api${u}`;
+
+export const GRPC_HOST: string =
+  process.env.REACT_APP_GRPC_HOST || "http://127.0.0.1:9999";
+
+export const API_HOST: string =
+  process.env.NODE_ENV === "development" && process.env.REACT_APP_API_HOST
+    ? process.env.REACT_APP_API_HOST
+    : "";
+
+export const grpc_metadata = (): Metadata => {
+  return {
+    authorization: `Bearer ${getToken()}`,
+    "accept-language": getLocale(),
+  };
+};
+
+export const upload = () => {
+  return {
+    Authorization: `Bearer ${getToken()}`,
+  };
+};
+
+export const options = (method: string): RequestInit => {
+  return {
+    credentials: "include",
+    headers: {
+      Authorization: `Bearer ${getToken()}`,
+      "Content-Type": "application/json; charset=utf-8",
+    },
+    mode: "cors",
+    method,
+  };
+};
+
+export const get = async <R>(path: string): Promise<R> => {
+  const response = await fetch(backend(path), options("GET"));
+  const res: R = await response.json();
+  return res;
+};
+
+export const delete_ = async <R>(path: string): Promise<R> => {
+  const response = await fetch(backend(path), options("DELETE"));
+  const res: R = await response.json();
+  return res;
+};
+
+// https://github.github.io/fetch/#options
+export const post = async <Q, R>(path: string, body: Q): Promise<R> => {
+  const data = options("POST");
+  data.body = JSON.stringify(body);
+  const response = await fetch(backend(path), data);
+  const res: R = await response.json();
+  return res;
+};
+
+export const patch = <Request, Response>(
+  path: string,
+  body: Request
+): Promise<Response> => {
+  const data = options("PATCH");
+  data.body = JSON.stringify(body);
+  return fetch(backend(path), data).then((res) => {
+    if (res.status === 200) {
+      return res.json();
+    }
+    throw res.text();
+  });
+};
+
+export const put = <Request, Response>(
+  path: string,
+  body: Request
+): Promise<Response> => {
+  const data = options("PUT");
+  data.body = JSON.stringify(body);
+  return fetch(backend(path), data).then((res) =>
+    res.status === 200
+      ? res.json()
+      : res.json().then((err) => {
+          throw err;
+        })
+  );
+};
+
+export const download = (path: string, name: string) => {
+  const data = options("GET");
+  fetch(backend(path), data)
+    .then((response) => response.blob())
+    .then((blob) => {
+      var url = window.URL.createObjectURL(blob);
+      var a = document.createElement("a");
+      a.href = url;
+      a.download = name;
+      document.body.appendChild(a); // for firefox
+      a.click();
+      a.remove();
+    });
+};

+ 16 - 0
dashboard/src/store.ts

@@ -0,0 +1,16 @@
+import { configureStore } from "@reduxjs/toolkit";
+
+import currentUserReducer from "./reducers/current-user";
+import layoutReducer from "./reducers/layout";
+
+const store = configureStore({
+  reducer: {
+    layout: layoutReducer,
+    currentUser: currentUserReducer,
+  },
+});
+
+export type RootState = ReturnType<typeof store.getState>;
+export type AppDispatch = typeof store.dispatch;
+
+export default store;