Browse Source

:construction: merge redux models

Jeremy Zheng 3 years ago
parent
commit
792d558a2c

+ 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>

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

@@ -0,0 +1,63 @@
+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;
+
+export const get = (): string | null => {
+  return sessionStorage.getItem(KEY);
+};
+
+const set = (token: string) => {
+  sessionStorage.setItem(KEY, token);
+};
+
+const remove = () => {
+  sessionStorage.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;