From 839f5c7c67714138e986f245c8fe797b3a353eb1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 4 Jan 2024 20:10:39 +0800 Subject: [PATCH] [#1113][#1224] improvement(UI): TS support and improve global request handing (#1332) ### What changes were proposed in this pull request? 1. Support TypeScript 2. Improve global reauest handing and add error messages ### Why are the changes needed? Fix: #1113 Fix: #1224 Fix: #1225 ### Does this PR introduce _any_ user-facing change? N/A ### How was this patch tested? N/A Co-authored-by: CHEYNE --- build.gradle.kts | 6 +- web/.prettierignore | 1 + web/.vscode/settings.json | 4 +- web/LICENSE.bin | 12 +- web/app/layout.js | 17 +- web/jsconfig.json | 22 --- web/lib/api/auth/index.js | 14 +- web/lib/api/catalogs/index.js | 15 +- web/lib/api/index.js | 47 ----- web/lib/api/metalakes/index.js | 23 +-- web/lib/api/schemas/index.js | 12 +- web/lib/api/tables/index.js | 12 +- web/lib/api/version/index.js | 5 +- web/lib/enums/httpEnum.ts | 47 +++++ web/lib/provider/session.js | 79 ++------ web/lib/store/auth/index.js | 80 +++++++- web/lib/store/metalakes/index.js | 28 +-- web/lib/store/sys/index.js | 12 +- web/lib/utils/axios/Axios.ts | 272 ++++++++++++++++++++++++++ web/lib/utils/axios/axiosCancel.ts | 71 +++++++ web/lib/utils/axios/axiosRetry.ts | 49 +++++ web/lib/utils/axios/axiosTransform.ts | 72 +++++++ web/lib/utils/axios/checkStatus.ts | 105 ++++++++++ web/lib/utils/axios/helper.ts | 74 +++++++ web/lib/utils/axios/index.ts | 271 +++++++++++++++++++++++++ web/lib/utils/index.js | 44 +++++ web/lib/utils/is.ts | 18 ++ web/licenses/lodash.txt | 49 +++++ web/licenses/qs.txt | 13 ++ web/licenses/side-channel.txt | 21 ++ web/licenses/sweetalert2.txt | 21 ++ web/licenses/undici-types.txt | 21 ++ web/licenses/vben.txt | 21 ++ web/package.json | 10 +- web/tsconfig.json | 40 ++++ web/types/axios.d.ts | 96 +++++++++ web/types/global.d.ts | 6 + web/yarn.lock | 60 ++++++ 38 files changed, 1547 insertions(+), 223 deletions(-) delete mode 100644 web/jsconfig.json delete mode 100644 web/lib/api/index.js create mode 100644 web/lib/enums/httpEnum.ts create mode 100644 web/lib/utils/axios/Axios.ts create mode 100644 web/lib/utils/axios/axiosCancel.ts create mode 100644 web/lib/utils/axios/axiosRetry.ts create mode 100644 web/lib/utils/axios/axiosTransform.ts create mode 100644 web/lib/utils/axios/checkStatus.ts create mode 100644 web/lib/utils/axios/helper.ts create mode 100644 web/lib/utils/axios/index.ts create mode 100644 web/lib/utils/is.ts create mode 100644 web/licenses/lodash.txt create mode 100644 web/licenses/qs.txt create mode 100644 web/licenses/side-channel.txt create mode 100644 web/licenses/sweetalert2.txt create mode 100644 web/licenses/undici-types.txt create mode 100644 web/licenses/vben.txt create mode 100644 web/tsconfig.json create mode 100644 web/types/axios.d.ts create mode 100644 web/types/global.d.ts diff --git a/build.gradle.kts b/build.gradle.kts index ed1b59048d2..7a72dc44027 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -273,10 +273,12 @@ tasks.rat { "**/licenses/*.md", "integration-test/**", "web/.**", + "web/next-env.d.ts", "web/dist/**/*", "web/node_modules/**/*", - "web/src/iconify-bundle/bundle-icons-react.js", - "web/src/iconify-bundle/icons-bundle-react.js", + "web/lib/utils/axios/**/*", + "web/lib/enums/httpEnum.ts", + "web/types/axios.d.ts", "web/yarn.lock", "**/LICENSE.*", "**/NOTICE.*" diff --git a/web/.prettierignore b/web/.prettierignore index bd294647985..c9c2b8a9b80 100644 --- a/web/.prettierignore +++ b/web/.prettierignore @@ -5,3 +5,4 @@ .next node_modules +tsconfig.json diff --git a/web/.vscode/settings.json b/web/.vscode/settings.json index 35dd12563c4..f65b666d349 100644 --- a/web/.vscode/settings.json +++ b/web/.vscode/settings.json @@ -3,8 +3,8 @@ "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true, - "source.organizeImports": false + "source.fixAll.eslint": "explicit", + "source.organizeImports": "never" }, "eslint.validate": ["javascript", "javascriptreact"], "eslint.format.enable": true, diff --git a/web/LICENSE.bin b/web/LICENSE.bin index 037faa776f6..b34e159cd47 100644 --- a/web/LICENSE.bin +++ b/web/LICENSE.bin @@ -203,15 +203,21 @@ This product bundles various third-party components also under the Apache Software License 2.0. - + Apache Zeppelin ./web/WEB-INF/web.xml @swc/helpers + typescript This product bundles various third-party components also under the MIT license. + Vben + ./web/lib/utils/axios + ./web/types/axios.d.ts + ./web/lib/enums/httpEnum.ts + @babel/code-frame @babel/helper-module-imports @babel/helper-string-parser @@ -321,6 +327,7 @@ prop-types property-expr proxy-from-env + qs react-dom react-hook-form react-hot-toast @@ -338,6 +345,7 @@ rtl-css-js scheduler screenfull + side-channel stack-generator stackframe stacktrace-gps @@ -348,11 +356,13 @@ stylis supports-color supports-preserve-symlinks-flag + sweetalert2 throttle-debounce tiny-case to-fast-properties toggle-selection toposort + undici-types use-sync-external-store watchpack yup diff --git a/web/app/layout.js b/web/app/layout.js index 6f0d380c010..84e1813132c 100644 --- a/web/app/layout.js +++ b/web/app/layout.js @@ -12,13 +12,15 @@ import { NavigationEvents } from '@/lib/layout/navigation-events' import Provider from '@/lib/provider' import Layout from '@/lib/layout/Layout' +import Loading from '@/lib/layout/Loading' + +import { Toaster } from 'react-hot-toast' + export const metadata = { title: 'Gravitino', description: 'A high-performance, geo-distributed and federated metadata lake.' } -import Loading from '@/lib/layout/Loading' - const RootLayout = props => { const { children } = props @@ -26,13 +28,12 @@ const RootLayout = props => { - { - }> - - {children} - - } + }> + + {children} + + ) diff --git a/web/jsconfig.json b/web/jsconfig.json deleted file mode 100644 index e79749354dd..00000000000 --- a/web/jsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "paths": { - "@/*": ["./*"] - } - }, - "exclude": [ - ".git", - ".next", - ".app-cache", - ".npm", - ".npm-tmp", - "dist", - "dist*", - "node_modules", - "**/dist/*", - "**/node_modules/*", - ".gradle", - ".node", - "build" - ] -} diff --git a/web/lib/api/auth/index.js b/web/lib/api/auth/index.js index b67cbadcf68..00313f37c97 100644 --- a/web/lib/api/auth/index.js +++ b/web/lib/api/auth/index.js @@ -3,19 +3,17 @@ * This software is licensed under the Apache License version 2. */ -import axios from 'axios' +import { defHttp } from '@/lib/utils/axios' +import qs from 'qs' export const getAuthConfigsApi = () => { - return axios({ - url: `/configs`, - method: 'get' + return defHttp.get({ + url: '/configs' }) } export const loginApi = (url, params) => { - return axios({ - url, - method: 'post', - params + return defHttp.post({ + url: `${url}?${qs.stringify(params)}` }) } diff --git a/web/lib/api/catalogs/index.js b/web/lib/api/catalogs/index.js index 318117cdb5d..f3d3f0af3cc 100644 --- a/web/lib/api/catalogs/index.js +++ b/web/lib/api/catalogs/index.js @@ -3,7 +3,7 @@ * This software is licensed under the Apache License version 2. */ -import defHttp from '@/lib/api' +import { defHttp } from '@/lib/utils/axios' const Apis = { GET: ({ metalake }) => `/api/metalakes/${metalake}/catalogs`, @@ -12,23 +12,20 @@ const Apis = { } export const getCatalogsApi = params => { - return defHttp.request({ - url: `${Apis.GET(params)}`, - method: 'get' + return defHttp.get({ + url: `${Apis.GET(params)}` }) } export const getCatalogDetailsApi = ({ metalake, catalog }) => { - return defHttp.request({ - url: `${Apis.GET_DETAIL({ metalake, catalog })}`, - method: 'get' + return defHttp.get({ + url: `${Apis.GET_DETAIL({ metalake, catalog })}` }) } export const createCatalogApi = ({ data, metalake }) => { - return defHttp.request({ + return defHttp.post({ url: `${Apis.CREATE({ metalake })}`, - method: 'post', data }) } diff --git a/web/lib/api/index.js b/web/lib/api/index.js deleted file mode 100644 index 717e0f651d2..00000000000 --- a/web/lib/api/index.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2023 Datastrato Pvt Ltd. - * This software is licensed under the Apache License version 2. - */ - -import axios from 'axios' - -import { useRouter } from 'next/navigation' - -const defHttp = axios.create({ - baseURL: '/', - headers: { - Accept: 'application/vnd.gravitino.v1+json' - } -}) - -defHttp.interceptors.request.use(config => { - if (config.url.includes('/oauth2/token')) { - return config - } - const accessToken = typeof window !== 'undefined' ? window.localStorage.getItem('accessToken') : null - - if (accessToken) { - config.headers.Authorization = `Bearer ${accessToken}` - } - - return config -}) - -defHttp.interceptors.response.use( - res => { - return res - }, - err => { - console.error(err) - if (err.response.status === 401) { - if (typeof window !== 'undefined') { - window.localStorage.removeItem('accessToken') - window.localStorage.removeItem('version') - const router = useRouter() - router.replace('/login') - } - } - } -) - -export default defHttp diff --git a/web/lib/api/metalakes/index.js b/web/lib/api/metalakes/index.js index 0e47ceace89..3a2d354888a 100644 --- a/web/lib/api/metalakes/index.js +++ b/web/lib/api/metalakes/index.js @@ -3,7 +3,7 @@ * This software is licensed under the Apache License version 2. */ -import defHttp from '@/lib/api' +import { defHttp } from '@/lib/utils/axios' const Apis = { GET: '/api/metalakes', @@ -13,38 +13,33 @@ const Apis = { } export const getMetalakesApi = () => { - return defHttp.request({ - url: `${Apis.GET}`, - method: 'get' + return defHttp.get({ + url: `${Apis.GET}` }) } export const getMetalakeDetailsApi = name => { - return defHttp.request({ - url: `${Apis.GET}/${name}`, - method: 'get' + return defHttp.get({ + url: `${Apis.GET}/${name}` }) } export const createMetalakeApi = data => { - return defHttp.request({ + return defHttp.post({ url: `${Apis.CREATE}`, - method: 'post', data }) } export const deleteMetalakeApi = name => { - return defHttp.request({ - url: `${Apis.DELETE}/${name}`, - method: 'delete' + return defHttp.delete({ + url: `${Apis.DELETE}/${name}` }) } export const updateMetalakeApi = ({ name, data }) => { - return defHttp.request({ + return defHttp.put({ url: `${Apis.UPDATE}/${name}`, - method: 'put', data }) } diff --git a/web/lib/api/schemas/index.js b/web/lib/api/schemas/index.js index 117be42d108..1d217cc5049 100644 --- a/web/lib/api/schemas/index.js +++ b/web/lib/api/schemas/index.js @@ -3,7 +3,7 @@ * This software is licensed under the Apache License version 2. */ -import defHttp from '@/lib/api' +import { defHttp } from '@/lib/utils/axios' const Apis = { GET: ({ metalake, catalog }) => `/api/metalakes/${metalake}/catalogs/${catalog}/schemas`, @@ -11,15 +11,13 @@ const Apis = { } export const getSchemasApi = params => { - return defHttp.request({ - url: `${Apis.GET(params)}`, - method: 'get' + return defHttp.get({ + url: `${Apis.GET(params)}` }) } export const getSchemaDetailsApi = ({ metalake, catalog, schema }) => { - return defHttp.request({ - url: `${Apis.GET_DETAIL({ metalake, catalog, schema })}`, - method: 'get' + return defHttp.get({ + url: `${Apis.GET_DETAIL({ metalake, catalog, schema })}` }) } diff --git a/web/lib/api/tables/index.js b/web/lib/api/tables/index.js index 9420495f90e..8271e671b17 100644 --- a/web/lib/api/tables/index.js +++ b/web/lib/api/tables/index.js @@ -3,7 +3,7 @@ * This software is licensed under the Apache License version 2. */ -import defHttp from '@/lib/api' +import { defHttp } from '@/lib/utils/axios' const Apis = { GET: ({ metalake, catalog, schema }) => `/api/metalakes/${metalake}/catalogs/${catalog}/schemas/${schema}/tables`, @@ -12,15 +12,13 @@ const Apis = { } export const getTablesApi = params => { - return defHttp.request({ - url: `${Apis.GET(params)}`, - method: 'get' + return defHttp.get({ + url: `${Apis.GET(params)}` }) } export const getTableDetailsApi = ({ metalake, catalog, schema, table }) => { - return defHttp.request({ - url: `${Apis.GET_DETAIL({ metalake, catalog, schema, table })}`, - method: 'get' + return defHttp.get({ + url: `${Apis.GET_DETAIL({ metalake, catalog, schema, table })}` }) } diff --git a/web/lib/api/version/index.js b/web/lib/api/version/index.js index 9f30482629c..0d630b3ce5a 100644 --- a/web/lib/api/version/index.js +++ b/web/lib/api/version/index.js @@ -3,15 +3,14 @@ * This software is licensed under the Apache License version 2. */ -import defHttp from '@/lib/api' +import { defHttp } from '@/lib/utils/axios' const Apis = { GET: '/api/version' } export const getVersionApi = () => { - return defHttp.request({ - method: 'get', + return defHttp.get({ url: `${Apis.GET}` }) } diff --git a/web/lib/enums/httpEnum.ts b/web/lib/enums/httpEnum.ts new file mode 100644 index 00000000000..4bd15de1ed6 --- /dev/null +++ b/web/lib/enums/httpEnum.ts @@ -0,0 +1,47 @@ +/* +MIT License + +Copyright (c) 2020-present, Vben + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * Referred from src/enums/httpEnum.ts + */ + +export enum ContentTypeEnum { + JSON = 'application/json;charset=UTF-8', + FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8', + FORM_DATA = 'multipart/form-data;charset=UTF-8' +} + +export enum RequestEnum { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', + DELETE = 'DELETE' +} + +export enum ResultEnum { + SUCCESS = 0, + ERROR = -1, + TIMEOUT = 401, + TYPE = 'success' +} diff --git a/web/lib/provider/session.js b/web/lib/provider/session.js index b615e0789a8..d60fdf0ee09 100644 --- a/web/lib/provider/session.js +++ b/web/lib/provider/session.js @@ -12,15 +12,8 @@ import { useRouter } from 'next/navigation' import { useAppDispatch, useAppSelector } from '@/lib/hooks/useStore' import { initialVersion, setVersion as setStoreVersion } from '@/lib/store/sys' -import { useLocalStorage } from 'react-use' - -import { getVersionApi } from '@/lib/api/version' -import { loginApi } from '@/lib/api/auth' - -import { isProdEnv, to, loggerVersion } from '../utils' -import { getAuthConfigs, setAuthParams, setAuthToken, setExpiredIn, refreshToken } from '../store/auth' - -const devOauthUrl = process.env.NEXT_PUBLIC_OAUTH_PATH +import { to } from '../utils' +import { getAuthConfigs, setAuthToken, setIntervalId, loginAction } from '../store/auth' const authProvider = { version: '', @@ -37,90 +30,44 @@ export const useAuth = () => useContext(AuthContext) const AuthProvider = ({ children }) => { const router = useRouter() const [loading, setLoading] = useState(authProvider.loading) - const [authParams, setLocalAuthParams] = useLocalStorage('authParams', '', { raw: true }) - const [localExpiredIn, setLocalExpiredIn] = useLocalStorage('expiredIn', '', { raw: true }) - const [token, setToken] = useLocalStorage('accessToken', '', { raw: true }) - const [version, setVersion] = useLocalStorage('version', authProvider.version, { raw: false }) + + const token = (typeof window !== 'undefined' && localStorage.getItem('accessToken')) || null + const version = (typeof window !== 'undefined' && localStorage.getItem('version')) || null + const authStore = useAppSelector(state => state.auth) const dispatch = useAppDispatch() const handleLogin = async params => { - let oauthUrl = authStore.oauthUrl - - dispatch(setAuthParams(params)) - setLocalAuthParams(params) - - const getTokenAction = new Promise(async resolve => { - const url = isProdEnv ? oauthUrl : devOauthUrl - const [err, res] = await to(loginApi(url, params)) - - if (err || !res) { - throw new Error(err) - } - - const { access_token, expires_in } = res.data - - setToken(access_token) - dispatch(setAuthToken(access_token)) - setLocalExpiredIn(expires_in) - dispatch(setExpiredIn(expires_in)) - resolve(access_token) - }) - - getTokenAction.then(async token => { - if (!token) { - throw new Error('Token not found') - } - const [verErr, resVer] = await to(getVersionApi()) - - const { version } = resVer.data - - loggerVersion(version.version) - setVersion(version) - dispatch(setStoreVersion(version.version)) - dispatch(refreshToken()) - router.replace('/') - }) + await dispatch(loginAction({ params, router })) } const handleLogout = () => { - setVersion('') + localStorage.removeItem('version') + localStorage.removeItem('accessToken') + localStorage.removeItem('authParams') + dispatch(setStoreVersion('')) - setToken('') dispatch(setAuthToken('')) router.push('/login') } useEffect(() => { const initAuth = async () => { - // ** the expired time obtained from the backend is in seconds, default value is 299 seconds - const expired = (authStore.expiredIn ?? Number(localExpiredIn)) * (2 / 3) * 1000 - const defaultExpired = 299 * (2 / 3) * 1000 - - let intervalId = null - const [authConfigsErr, resAuthConfigs] = await to(dispatch(getAuthConfigs())) - const { authType, oauthUrl } = resAuthConfigs.payload + const { authType } = resAuthConfigs.payload if (authType === 'simple') { dispatch(initialVersion()) router.replace('/') } else { if (token) { - intervalId = setInterval(() => { - dispatch(refreshToken()) - }, expired || defaultExpired) - + dispatch(setIntervalId()) dispatch(setAuthToken(token)) dispatch(initialVersion()) } else { router.replace('/login') } } - - return () => { - clearInterval(intervalId) - } } initAuth() diff --git a/web/lib/store/auth/index.js b/web/lib/store/auth/index.js index 2850befba8f..f5a320b7c9e 100644 --- a/web/lib/store/auth/index.js +++ b/web/lib/store/auth/index.js @@ -5,27 +5,30 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' -import { to, isProdEnv } from '@/lib/utils' +import { to, isProdEnv, loggerVersion } from '@/lib/utils' import { getAuthConfigsApi, loginApi } from '@/lib/api/auth' +import { getVersionApi } from '@/lib/api/version' + +import { setVersion as setStoreVersion } from '@/lib/store/sys' const devOauthUrl = process.env.NEXT_PUBLIC_OAUTH_PATH -export const getAuthConfigs = createAsyncThunk('sys/getAuthConfigs', async () => { +export const getAuthConfigs = createAsyncThunk('auth/getAuthConfigs', async () => { let oauthUrl = null let authType = null const [err, res] = await to(getAuthConfigsApi()) if (!err && res) { - oauthUrl = `${res.data['gravitino.authenticator.oauth.serverUri']}${res.data['gravitino.authenticator.oauth.tokenPath']}` + oauthUrl = `${res['gravitino.authenticator.oauth.serverUri']}${res['gravitino.authenticator.oauth.tokenPath']}` - authType = res.data['gravitino.authenticator'] + authType = res['gravitino.authenticator'] } return { oauthUrl, authType } }) -export const refreshToken = createAsyncThunk('sys/refreshToken', async (data, { getState, dispatch }) => { +export const refreshToken = createAsyncThunk('auth/refreshToken', async (data, { getState, dispatch }) => { const authParams = getState().auth.authParams || window.localStorage.getItem('authParams') let params = typeof authParams === 'string' ? JSON.parse(authParams) : authParams @@ -42,16 +45,79 @@ export const refreshToken = createAsyncThunk('sys/refreshToken', async (data, { return { token: access_token, expiredIn: expires_in } }) +export const loginAction = createAsyncThunk('auth/loginAction', async ({ params, router }, { getState, dispatch }) => { + const preLogin = new Promise(async resolve => { + dispatch(setAuthParams(params)) + localStorage.setItem('authParams', JSON.stringify(params)) + + const url = isProdEnv ? getState().auth.oauthUrl : devOauthUrl + + const [err, res] = await to(loginApi(url, params)) + + if (err || !res) { + throw new Error(err) + } + + const { access_token, expires_in } = res + + localStorage.setItem('accessToken', access_token) + localStorage.setItem('expiredIn', expires_in) + dispatch(setAuthToken(access_token)) + dispatch(setExpiredIn(expires_in)) + + resolve(access_token) + }) + + preLogin.then(async token => { + if (!token) { + throw new Error('Token not found') + } + + const [verErr, resVer] = await to(getVersionApi()) + const { version } = resVer + + loggerVersion(version.version) + localStorage.setItem('version', JSON.stringify(version)) + dispatch(setStoreVersion(version.version)) + + router.replace('/') + }) +}) + +export const setIntervalId = createAsyncThunk('auth/setIntervalId', async (expiredIn, { dispatch }) => { + const localExpiredIn = localStorage.getItem('expiredIn') + + // ** the expired time obtained from the backend is in seconds, default value is 299 seconds + const expired = (expiredIn ?? Number(localExpiredIn)) * (2 / 3) * 1000 + const defaultExpired = 299 * (2 / 3) * 1000 + + let intervalId = setInterval(() => { + dispatch(refreshToken()) + }, expired || defaultExpired) + + return { + intervalId + } +}) + export const authSlice = createSlice({ name: 'auth', initialState: { oauthUrl: null, authType: null, - authToken: '', + authToken: null, authParams: null, - expiredIn: null + expiredIn: null, + intervalId: null }, reducers: { + setIntervalId(state, action) { + state.intervalId = this.setIntervalId() + }, + clearIntervalId(state, action) { + clearInterval(state.intervalId) + state.intervalId = null + }, setAuthToken(state, action) { state.authToken = action.payload }, diff --git a/web/lib/store/metalakes/index.js b/web/lib/store/metalakes/index.js index aadd72cd559..3400111c846 100644 --- a/web/lib/store/metalakes/index.js +++ b/web/lib/store/metalakes/index.js @@ -21,7 +21,7 @@ export const fetchMetalakes = createAsyncThunk('appMetalakes/fetchMetalakes', as try { const response = await getMetalakesApi() - const { metalakes } = response.data + const { metalakes } = response return { metalakes @@ -49,7 +49,7 @@ export const deleteMetalake = createAsyncThunk('appMetalakes/deleteMetalake', as dispatch(fetchMetalakes()) - return response.data + return response } catch (error) { throw new Error(error) } @@ -61,7 +61,7 @@ export const updateMetalake = createAsyncThunk('appMetalakes/updateMetalake', as dispatch(fetchMetalakes()) - return response.data + return response } catch (error) { throw new Error(error) } @@ -75,7 +75,7 @@ export const initMetalakeTree = createAsyncThunk( const catalogsData = await getCatalogsApi({ metalake }) - const { identifiers: catalogs = [] } = catalogsData.data + const { identifiers: catalogs = [] } = catalogsData for (const catalogItem of catalogs) { const catalogNode = { @@ -92,7 +92,7 @@ export const initMetalakeTree = createAsyncThunk( dispatch(setExpandedTreeNode([catalogNode.id])) const schemasData = await getSchemasApi({ metalake, catalog }) - const { identifiers: schemas = [] } = schemasData.data + const { identifiers: schemas = [] } = schemasData for (const schemaItem of schemas) { const schemaNode = { @@ -111,7 +111,7 @@ export const initMetalakeTree = createAsyncThunk( if (schema) { if (schema === schemaNode.name) { const tablesData = await getTablesApi({ metalake, catalog, schema }) - const { identifiers: tables = [] } = tablesData.data + const { identifiers: tables = [] } = tablesData for (const tableItem of tables) { const tableNode = { @@ -199,7 +199,7 @@ export const setIntoTreeAction = createAsyncThunk( export const getMetalakeDetails = createAsyncThunk('appMetalakes/getMetalakeDetails', async ({ metalake }) => { const response = await getMetalakeDetailsApi(metalake) - const { metalake: resMetalake } = response.data + const { metalake: resMetalake } = response return resMetalake }) @@ -210,7 +210,7 @@ export const fetchCatalogs = createAsyncThunk( try { const response = await getCatalogsApi({ metalake }) - const { identifiers = [] } = response.data + const { identifiers = [] } = response const catalogs = identifiers.map(catalog => { return { @@ -237,7 +237,7 @@ export const fetchCatalogs = createAsyncThunk( export const getCatalogDetails = createAsyncThunk('appMetalakes/getCatalogDetails', async ({ metalake, catalog }) => { const response = await getCatalogDetailsApi({ metalake, catalog }) - const { catalog: resCatalog } = response.data + const { catalog: resCatalog } = response return resCatalog }) @@ -248,7 +248,7 @@ export const createCatalog = createAsyncThunk( try { const response = await createCatalogApi({ data, metalake }) - const { catalog: catalogItem } = response.data + const { catalog: catalogItem } = response dispatch(setIntoTreeAction({ catalogItem, nodeIds: [metalake] })) @@ -263,7 +263,7 @@ export const fetchSchemas = createAsyncThunk('appMetalakes/fetchSchemas', async try { const response = await getSchemasApi({ metalake, catalog }) - const { identifiers = [] } = response.data + const { identifiers = [] } = response const schemas = identifiers.map(schema => { return { @@ -287,7 +287,7 @@ export const getSchemaDetails = createAsyncThunk( async ({ metalake, catalog, schema }) => { const response = await getSchemaDetailsApi({ metalake, catalog, schema }) - const { schema: resSchema } = response.data + const { schema: resSchema } = response return resSchema } @@ -299,7 +299,7 @@ export const fetchTables = createAsyncThunk( try { const response = await getTablesApi({ metalake, catalog, schema }) - const { identifiers = [] } = response.data + const { identifiers = [] } = response const tables = identifiers.map(table => { return { @@ -324,7 +324,7 @@ export const getTableDetails = createAsyncThunk( async ({ metalake, catalog, schema, table }, { dispatch }) => { const response = await getTableDetailsApi({ metalake, catalog, schema, table }) - const { table: resTable } = response.data + const { table: resTable } = response return resTable } diff --git a/web/lib/store/sys/index.js b/web/lib/store/sys/index.js index 77bbc53effe..5e23c998dcb 100644 --- a/web/lib/store/sys/index.js +++ b/web/lib/store/sys/index.js @@ -13,13 +13,15 @@ export const initialVersion = createAsyncThunk('sys/fetchVersion', async (params let version = null const [err, res] = await to(getVersionApi()) - if (!err && res) { - version = res.data.version - typeof window !== 'undefined' && window.localStorage.setItem('version', JSON.stringify(version)) - - loggerVersion(version.version) + if (err || !res) { + throw new Error(err) } + version = res.version + typeof window !== 'undefined' && window.localStorage.setItem('version', JSON.stringify(version)) + + loggerVersion(version.version) + return version }) diff --git a/web/lib/utils/axios/Axios.ts b/web/lib/utils/axios/Axios.ts new file mode 100644 index 00000000000..e96b41d605c --- /dev/null +++ b/web/lib/utils/axios/Axios.ts @@ -0,0 +1,272 @@ +/* +MIT License + +Copyright (c) 2020-present, Vben + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * Referred from src/utils/http/axios/Axios.ts + */ + +import type { AxiosRequestConfig, AxiosInstance, AxiosResponse, AxiosError, InternalAxiosRequestConfig } from 'axios' +import type { RequestOptions, Result, UploadFileParams } from '@/types/axios' +import type { CreateAxiosOptions } from './axiosTransform' +import axios from 'axios' +import qs from 'qs' +import { AxiosCanceler } from './axiosCancel' +import { isFunction } from '../is' +import { cloneDeep } from 'lodash-es' +import { ContentTypeEnum, RequestEnum } from '../../enums/httpEnum' + +export * from './axiosTransform' + +/** + * @description: axios module + */ +export class NextAxios { + private axiosInstance: AxiosInstance + private readonly options: CreateAxiosOptions + + constructor(options: CreateAxiosOptions) { + this.options = options + this.axiosInstance = axios.create(options) + this.setupInterceptors() + } + + /** + * @description: Create axios instance + */ + private createAxios(config: CreateAxiosOptions): void { + this.axiosInstance = axios.create(config) + } + + private getTransform() { + const { transform } = this.options + + return transform + } + + getAxios(): AxiosInstance { + return this.axiosInstance + } + + /** + * @description: Reconfigure axios + */ + configAxios(config: CreateAxiosOptions) { + if (!this.axiosInstance) { + return + } + this.createAxios(config) + } + + /** + * @description: Set general header + */ + setHeader(headers: any): void { + if (!this.axiosInstance) { + return + } + Object.assign(this.axiosInstance.defaults.headers, headers) + } + + /** + * @description: Interceptor configuration + */ + private setupInterceptors() { + const { + axiosInstance, + options: { transform } + } = this + if (!transform) { + return + } + + const { requestInterceptors, requestInterceptorsCatch, responseInterceptors, responseInterceptorsCatch } = transform + + const axiosCanceler = new AxiosCanceler() + + // ** Request interceptor configuration processing + this.axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => { + // ** If cancel repeat request is turned on, then cancel repeat request is prohibited + const requestOptions = (config as unknown as any).requestOptions ?? this.options.requestOptions + const ignoreCancelToken = requestOptions?.ignoreCancelToken ?? true + + !ignoreCancelToken && axiosCanceler.addPending(config) + + if (requestInterceptors && isFunction(requestInterceptors)) { + config = requestInterceptors(config, this.options) + } + + return config + }, undefined) + + // ** Request interceptor error capture + requestInterceptorsCatch && + isFunction(requestInterceptorsCatch) && + this.axiosInstance.interceptors.request.use(undefined, requestInterceptorsCatch) + + // ** Response result interceptor processing + this.axiosInstance.interceptors.response.use((res: AxiosResponse) => { + res && axiosCanceler.removePending(res.config) + if (responseInterceptors && isFunction(responseInterceptors)) { + res = responseInterceptors(res) + } + + return res + }, undefined) + + // ** Response result interceptor error capture + responseInterceptorsCatch && + isFunction(responseInterceptorsCatch) && + this.axiosInstance.interceptors.response.use(undefined, error => { + return responseInterceptorsCatch(axiosInstance, error) + }) + } + + /** + * @description: File Upload + */ + uploadFile(config: AxiosRequestConfig, params: UploadFileParams) { + const formData = new window.FormData() + const customFilename = params.name || 'file' + + if (params.filename) { + formData.append(customFilename, params.file, params.filename) + } else { + formData.append(customFilename, params.file) + } + + if (params.data) { + Object.keys(params.data).forEach(key => { + const value = params.data![key] + if (Array.isArray(value)) { + value.forEach(item => { + formData.append(`${key}[]`, item) + }) + + return + } + + formData.append(key, params.data![key]) + }) + } + + return this.axiosInstance.request({ + ...config, + method: 'POST', + data: formData, + headers: { + 'Content-type': ContentTypeEnum.FORM_DATA, + ignoreCancelToken: true + } + }) + } + + // ** support form-data + supportFormData(config: AxiosRequestConfig) { + const headers = config.headers || this.options.headers + const contentType = headers?.['Content-Type'] || headers?.['content-type'] + + if ( + contentType !== ContentTypeEnum.FORM_URLENCODED || + !Reflect.has(config, 'data') || + config.method?.toUpperCase() === RequestEnum.GET + ) { + return config + } + + return { + ...config, + data: qs.stringify(config.data, { arrayFormat: 'brackets' }) + } + } + + get(config: AxiosRequestConfig, options?: RequestOptions): Promise { + return this.request({ ...config, method: 'GET' }, options) + } + + post(config: AxiosRequestConfig, options?: RequestOptions): Promise { + return this.request({ ...config, method: 'POST' }, options) + } + + put(config: AxiosRequestConfig, options?: RequestOptions): Promise { + return this.request({ ...config, method: 'PUT' }, options) + } + + delete(config: AxiosRequestConfig, options?: RequestOptions): Promise { + return this.request({ ...config, method: 'DELETE' }, options) + } + + request(config: AxiosRequestConfig, options?: RequestOptions): Promise { + let conf: CreateAxiosOptions = cloneDeep(config) + if (config.cancelToken) { + conf.cancelToken = config.cancelToken + } + + if (config.signal) { + conf.signal = config.signal + } + + const transform = this.getTransform() + + const { requestOptions } = this.options + + const opt: RequestOptions = Object.assign({}, requestOptions, options) + + const { beforeRequestHook, requestCatchHook, transformResponseHook } = transform || {} + if (beforeRequestHook && isFunction(beforeRequestHook)) { + conf = beforeRequestHook(conf, opt) + } + conf.requestOptions = opt + + conf = this.supportFormData(conf) + + return new Promise((resolve, reject) => { + this.axiosInstance + .request>(conf) + .then((res: AxiosResponse) => { + if (transformResponseHook && isFunction(transformResponseHook)) { + try { + const ret = transformResponseHook(res, opt) + resolve(ret) + } catch (err) { + reject(err || new Error('request error!')) + } + + return + } + resolve(res as unknown as Promise) + }) + .catch((e: Error | AxiosError) => { + if (requestCatchHook && isFunction(requestCatchHook)) { + reject(requestCatchHook(e, opt)) + + return + } + if (axios.isAxiosError(e)) { + // ** rewrite error message from axios in here + } + reject(e) + }) + }) + } +} diff --git a/web/lib/utils/axios/axiosCancel.ts b/web/lib/utils/axios/axiosCancel.ts new file mode 100644 index 00000000000..ac4413a442c --- /dev/null +++ b/web/lib/utils/axios/axiosCancel.ts @@ -0,0 +1,71 @@ +/* +MIT License + +Copyright (c) 2020-present, Vben + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * Referred from src/utils/http/axios/axiosCancel.ts + */ + +import type { AxiosRequestConfig } from 'axios' + +const pendingMap = new Map() + +const getPendingUrl = (config: AxiosRequestConfig): string => { + return [config.method, config.url].join('&') +} + +export class AxiosCanceler { + public addPending(config: AxiosRequestConfig): void { + this.removePending(config) + const url = getPendingUrl(config) + const controller = new AbortController() + config.signal = config.signal || controller.signal + if (!pendingMap.has(url)) { + pendingMap.set(url, controller) + } + } + + public removeAllPending(): void { + pendingMap.forEach(abortController => { + if (abortController) { + abortController.abort() + } + }) + this.reset() + } + + public removePending(config: AxiosRequestConfig): void { + const url = getPendingUrl(config) + if (pendingMap.has(url)) { + const abortController = pendingMap.get(url) + if (abortController) { + abortController.abort(url) + } + pendingMap.delete(url) + } + } + + public reset(): void { + pendingMap.clear() + } +} diff --git a/web/lib/utils/axios/axiosRetry.ts b/web/lib/utils/axios/axiosRetry.ts new file mode 100644 index 00000000000..5082cc38796 --- /dev/null +++ b/web/lib/utils/axios/axiosRetry.ts @@ -0,0 +1,49 @@ +/* +MIT License + +Copyright (c) 2020-present, Vben + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * Referred from src/utils/http/axios/axiosRetry.ts + */ + +import { AxiosError, AxiosInstance } from 'axios' + +export class AxiosRetry { + retry(axiosInstance: AxiosInstance, error: AxiosError) { + const { config }: any = error.response + const { waitTime, count } = config?.requestOptions?.retryRequest ?? {} + config.__retryCount = config.__retryCount || 0 + if (config.__retryCount >= count) { + return Promise.reject(error) + } + config.__retryCount += 1 + + delete config.headers + + return this.delay(waitTime).then(() => axiosInstance(config)) + } + + private delay(waitTime: number) { + return new Promise(resolve => setTimeout(resolve, waitTime)) + } +} diff --git a/web/lib/utils/axios/axiosTransform.ts b/web/lib/utils/axios/axiosTransform.ts new file mode 100644 index 00000000000..5c467d86249 --- /dev/null +++ b/web/lib/utils/axios/axiosTransform.ts @@ -0,0 +1,72 @@ +/* +MIT License + +Copyright (c) 2020-present, Vben + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * Referred from src/utils/http/axios/axiosTransform.ts + */ + +// ** Data processing class, can be configured according to the project +import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios' +import type { RequestOptions, Result } from '@/types/axios' + +export interface CreateAxiosOptions extends AxiosRequestConfig { + authenticationScheme?: string + transform?: AxiosTransform + requestOptions?: RequestOptions +} + +export abstract class AxiosTransform { + // ** A function that is called before a request is sent. It can modify the request configuration as needed. + beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig + + /** + * @description: Processing response data + */ + transformResponseHook?: (res: AxiosResponse, options: RequestOptions) => any + + /** + * @description: Handling of failed requests + */ + requestCatchHook?: (e: Error, options: RequestOptions) => Promise + + /** + * @description: Interceptor before request + */ + requestInterceptors?: (config: InternalAxiosRequestConfig, options: CreateAxiosOptions) => InternalAxiosRequestConfig + + /** + * @description: Interceptor after request + */ + responseInterceptors?: (res: AxiosResponse) => AxiosResponse + + /** + * @description: Interceptors error handing before request + */ + requestInterceptorsCatch?: (error: Error) => void + + /** + * @description: Interceptors error handing after request + */ + responseInterceptorsCatch?: (axiosInstance: AxiosInstance, error: Error) => void +} diff --git a/web/lib/utils/axios/checkStatus.ts b/web/lib/utils/axios/checkStatus.ts new file mode 100644 index 00000000000..0c55d39de23 --- /dev/null +++ b/web/lib/utils/axios/checkStatus.ts @@ -0,0 +1,105 @@ +/* +MIT License + +Copyright (c) 2020-present, Vben + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * Referred from src/utils/http/axios/checkStatus.ts + */ + +import type { ErrorMessageMode } from '@/types/axios' +import toast from 'react-hot-toast' +import Swal from 'sweetalert2' +import { useRouter as Router } from 'next/navigation' + +export function checkStatus(status: number, msg: string, errorMessageMode: ErrorMessageMode = 'message'): void { + let errMessage = '' + + switch (status) { + case 400: + errMessage = `${msg}` + break + + case 401: + errMessage = msg || 'The user does not have permission (token, user name, password error or expired)!' + + localStorage.removeItem('accessToken') + localStorage.removeItem('version') + + Router().replace('/login') + + break + case 403: + errMessage = 'The user is authorized, but access is forbidden!' + break + + case 404: + errMessage = 'Network request error, the resource was not found!' + break + + case 405: + errMessage = 'Network request error, request method not allowed!' + break + + case 408: + errMessage = 'Network request timed out!' + break + + case 409: + errMessage = msg || 'Conflict with the current resource state!' + break + + case 500: + errMessage = msg || 'Server error, unable to connect Gravitino!' + break + + case 501: + errMessage = 'The network is not implemented!' + break + + case 502: + errMessage = 'Network Error!' + break + + case 503: + errMessage = 'The service is unavailable, the server is temporarily overloaded or maintained!' + break + + case 504: + errMessage = 'Network timeout!' + break + + case 505: + errMessage = 'The http version does not support the request!' + break + + default: + } + + if (errMessage) { + if (errorMessageMode === 'modal') { + Swal.fire({ title: 'Error Tip', text: errMessage, icon: 'error' }) + } else if (errorMessageMode === 'message') { + toast.error(errMessage, { id: `global_error_message_status_${status}` }) + } + } +} diff --git a/web/lib/utils/axios/helper.ts b/web/lib/utils/axios/helper.ts new file mode 100644 index 00000000000..18a1d0c6a73 --- /dev/null +++ b/web/lib/utils/axios/helper.ts @@ -0,0 +1,74 @@ +/* +MIT License + +Copyright (c) 2020-present, Vben + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * Referred from src/utils/http/axios/helper.ts + */ + +import { isObject, isString } from '@/lib/utils/is' + +const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss' + +export function joinTimestamp(join: boolean, restful: T): T extends true ? string : object + +export function joinTimestamp(join: boolean, restful = false): string | object { + if (!join) { + return restful ? '' : {} + } + const now = new Date().getTime() + if (restful) { + return `?_t=${now}` + } + + return { _t: now } +} + +/** + * @description: Format request parameter time + */ +export function formatRequestDate(params: Recordable) { + if (Object.prototype.toString.call(params) !== '[object Object]') { + return + } + + for (const key in params) { + const format = params[key]?.format ?? null + if (format && typeof format === 'function') { + params[key] = params[key].format(DATE_TIME_FORMAT) + } + if (isString(key)) { + const value = params[key] + if (value) { + try { + params[key] = isString(value) ? value.trim() : value + } catch (error: any) { + throw new Error(error) + } + } + } + if (isObject(params[key])) { + formatRequestDate(params[key]) + } + } +} diff --git a/web/lib/utils/axios/index.ts b/web/lib/utils/axios/index.ts new file mode 100644 index 00000000000..4c27ce63659 --- /dev/null +++ b/web/lib/utils/axios/index.ts @@ -0,0 +1,271 @@ +/* +MIT License + +Copyright (c) 2020-present, Vben + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * Referred from src/utils/http/axios/index.ts + */ + +// ** The axios configuration can be changed according to the project, just change the file, other files can be left unchanged + +import type { AxiosInstance, AxiosResponse } from 'axios' +import { clone } from 'lodash-es' +import type { RequestOptions, Result } from '@/types/axios' +import type { AxiosTransform, CreateAxiosOptions } from './axiosTransform' +import { NextAxios } from './Axios' +import { checkStatus } from './checkStatus' +import toast from 'react-hot-toast' +import Swal from 'sweetalert2' +import { RequestEnum, ResultEnum, ContentTypeEnum } from '@/lib/enums/httpEnum' +import { isString, isUndefined, isNull, isEmpty } from '@/lib/utils/is' +import { setObjToUrlParams, deepMerge } from '@/lib/utils' +import { joinTimestamp, formatRequestDate } from './helper' +import { AxiosRetry } from './axiosRetry' +import axios from 'axios' +import { useAuth as Auth } from '../../provider/session' + +/** + * @description: Data processing to facilitate the distinction of multiple processing methods + */ +const transform: AxiosTransform = { + /** + * @description: Handling response data. If the data does not conform to the expected format, an error can be thrown directly + */ + transformResponseHook: (res: AxiosResponse, options: RequestOptions) => { + const { isTransformResponse, isReturnNativeResponse } = options + + // ** Whether to return the native response headers, for example: use this property when you need to access the response headers + if (isReturnNativeResponse) { + return res + } + + // ** No processing is performed; return as is + // ** Enable when it is necessary to directly access the code, data, and message information in the page code + if (!isTransformResponse) { + return res.data + } + + const { data } = res + if (!data) { + // ** return '[HTTP] Request has no return value'; + throw new Error('The interface request failed, please try again later!') + } + + const { code, result, message } = data + + const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS + if (hasSuccess) { + let successMsg = message + + if (isNull(successMsg) || isUndefined(successMsg) || isEmpty(successMsg)) { + successMsg = `Operation Success` + } + + if (options.successMessageMode === 'modal') { + Swal.fire({ title: 'Success Tip', text: successMsg, icon: 'success' }) + } else if (options.successMessageMode === 'message') { + toast.success(successMsg) + } + + return result + } + + let timeoutMsg = '' + switch (code) { + case ResultEnum.TIMEOUT: + timeoutMsg = 'Login timed out, please log in again!' + Auth().logout() + + break + default: + if (message) { + timeoutMsg = message + } + } + + if (options.errorMessageMode === 'modal') { + timeoutMsg && Swal.fire({ title: 'Error Tip', text: timeoutMsg, icon: 'error' }) + } else if (options.errorMessageMode === 'message') { + timeoutMsg && toast.error(timeoutMsg) + } + + throw new Error(timeoutMsg || 'The interface request failed, please try again later!') + }, + + // ** Handling Configuration Prior to Request + beforeRequestHook: (config, options) => { + const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options + + if (joinPrefix) { + config.url = `${urlPrefix}${config.url}` + } + + if (apiUrl && isString(apiUrl)) { + config.url = `${apiUrl}${config.url}` + } + const params = config.params || {} + const data = config.data || false + formatDate && data && !isString(data) && formatRequestDate(data) + if (config.method?.toUpperCase() === RequestEnum.GET) { + if (!isString(params)) { + // ** Add a timestamp parameter to the GET request to avoid retrieving data from the cache + config.params = Object.assign(params || {}, joinTimestamp(joinTime, false)) + } else { + // ** Supporting RESTful Style + config.url = config.url + params + `${joinTimestamp(joinTime, true)}` + config.params = undefined + } + } else { + if (!isString(params)) { + formatDate && formatRequestDate(params) + if ( + Reflect.has(config, 'data') && + config.data && + (Object.keys(config.data).length > 0 || config.data instanceof FormData) + ) { + config.data = data + config.params = params + } else { + // ** If no data is provided for non-GET requests, the params will be treated as data + config.data = params + config.params = undefined + } + if (joinParamsToUrl) { + config.url = setObjToUrlParams(config.url as string, Object.assign({}, config.params, config.data)) + } + } else { + // ** Supporting RESTful Style + config.url = config.url + params + config.params = undefined + } + } + + return config + }, + + /** + * @description: Interceptor Handling of Requests + */ + requestInterceptors: (config, options) => { + // ** Pre-Request Configuration Handling + const token = localStorage.getItem('accessToken') + + if (token && (config as Recordable)?.requestOptions?.withToken !== false) { + // ** jwt token + ;(config as Recordable).headers.Authorization = options.authenticationScheme + ? `${options.authenticationScheme} ${token}` + : token + } + + return config + }, + + /** + * @description: Interceptor Handling of Responses + */ + responseInterceptors: (res: AxiosResponse) => { + return res + }, + + /** + * @description: Error Response Handling + */ + responseInterceptorsCatch: (axiosInstance: AxiosInstance, error: any) => { + const { response, code, message, config } = error || {} + const errorMessageMode = config?.requestOptions?.errorMessageMode || 'none' + const msg: string = response?.data?.error?.message ?? response?.data?.message ?? '' + const err: string = error?.toString?.() ?? '' + let errMessage = '' + + if (axios.isCancel(error)) { + return Promise.reject(error) + } + + try { + if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) { + errMessage = 'The interface request timed out, please refresh the page and try again!' + } + if (err?.includes('Network Error')) { + errMessage = 'Please check if your network connection is normal! The network is abnormal' + } + + if (errMessage) { + if (errorMessageMode === 'modal') { + Swal.fire({ title: 'Error Tip', text: errMessage, icon: 'error' }) + } else if (errorMessageMode === 'message') { + toast.error(errMessage) + } + + return Promise.reject(error) + } + } catch (error) { + throw new Error(error as unknown as string) + } + + checkStatus(error?.response?.status, msg, errorMessageMode) + + const retryRequest = new AxiosRetry() + const { isOpenRetry } = config.requestOptions.retryRequest + config.method?.toUpperCase() === RequestEnum.GET && isOpenRetry && retryRequest.retry(axiosInstance, error) + + return Promise.reject(error) + } +} + +function createAxios(opt?: Partial) { + return new NextAxios( + deepMerge( + { + // ** See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes + // ** authentication schemes, e.g: Bearer + authenticationScheme: 'Bearer', + timeout: 0, + headers: { 'Content-Type': ContentTypeEnum.JSON }, + transform: clone(transform), + + // ** request configuration settings + requestOptions: { + joinPrefix: true, + isReturnNativeResponse: false, + isTransformResponse: false, + joinParamsToUrl: false, + formatDate: true, + errorMessageMode: 'message', + apiUrl: '', + urlPrefix: '', + joinTime: true, + ignoreCancelToken: true, + withToken: true, + retryRequest: { + isOpenRetry: false, + count: 5, + waitTime: 100 + } + } + }, + opt || {} + ) + ) +} + +export const defHttp = createAxios() diff --git a/web/lib/utils/index.js b/web/lib/utils/index.js index 3d4b07a9cad..b66ad8601a5 100644 --- a/web/lib/utils/index.js +++ b/web/lib/utils/index.js @@ -3,6 +3,9 @@ * This software is licensed under the Apache License version 2. */ +import { intersectionWith, isEqual, mergeWith, unionWith } from 'lodash-es' +import { isArray, isObject } from './is' + export const isDevEnv = process.env.NODE_ENV === 'development' export const isProdEnv = process.env.NODE_ENV === 'production' @@ -64,3 +67,44 @@ export const genUpdates = (originalData, newData) => { export const hasNull = obj => { return Object.keys(obj).some(key => obj[key] === null) } + +export const deepMerge = (source, target, mergeArrays = 'replace') => { + if (!target) { + return source + } + if (!source) { + return target + } + + return mergeWith({}, source, target, (sourceValue, targetValue) => { + if (isArray(targetValue) && isArray(sourceValue)) { + switch (mergeArrays) { + case 'union': + return unionWith(sourceValue, targetValue, isEqual) + case 'intersection': + return intersectionWith(sourceValue, targetValue, isEqual) + case 'concat': + return sourceValue.concat(targetValue) + case 'replace': + return targetValue + default: + throw new Error(`Unknown merge array strategy: ${mergeArrays}`) + } + } + if (isObject(targetValue) && isObject(sourceValue)) { + return deepMerge(sourceValue, targetValue, mergeArrays) + } + + return undefined + }) +} + +export function setObjToUrlParams(baseUrl, obj) { + let parameters = '' + for (const key in obj) { + parameters += key + '=' + encodeURIComponent(obj[key]) + '&' + } + parameters = parameters.replace(/&$/, '') + + return /\?$/.test(baseUrl) ? baseUrl + parameters : baseUrl.replace(/\/?$/, '?') + parameters +} diff --git a/web/lib/utils/is.ts b/web/lib/utils/is.ts new file mode 100644 index 00000000000..d6430c9fc96 --- /dev/null +++ b/web/lib/utils/is.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 Datastrato Pvt Ltd. + * This software is licensed under the Apache License version 2. + */ + +export { isString, isFunction, isUndefined, isEmpty, isNull } from 'lodash-es' + +export function is(val: unknown, type: string) { + return toString.call(val) === `[object ${type}]` +} + +export function isObject(val: any): val is Record { + return val !== null && is(val, 'Object') +} + +export function isArray(val: any): val is Array { + return val && Array.isArray(val) +} diff --git a/web/licenses/lodash.txt b/web/licenses/lodash.txt new file mode 100644 index 00000000000..4773fd8e84c --- /dev/null +++ b/web/licenses/lodash.txt @@ -0,0 +1,49 @@ +The MIT License + +Copyright JS Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. diff --git a/web/licenses/qs.txt b/web/licenses/qs.txt new file mode 100644 index 00000000000..6d66a3d10f4 --- /dev/null +++ b/web/licenses/qs.txt @@ -0,0 +1,13 @@ +BSD 3-Clause License + +Copyright (c) 2014, Nathan LaFreniere and other contributors All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/web/licenses/side-channel.txt b/web/licenses/side-channel.txt new file mode 100644 index 00000000000..3900dd7e2ff --- /dev/null +++ b/web/licenses/side-channel.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/web/licenses/sweetalert2.txt b/web/licenses/sweetalert2.txt new file mode 100644 index 00000000000..388ef23743b --- /dev/null +++ b/web/licenses/sweetalert2.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Tristan Edwards & Limon Monte + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/web/licenses/undici-types.txt b/web/licenses/undici-types.txt new file mode 100644 index 00000000000..e7323bb52ec --- /dev/null +++ b/web/licenses/undici-types.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Matteo Collina and Undici contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/web/licenses/vben.txt b/web/licenses/vben.txt new file mode 100644 index 00000000000..ba2fe3b14c1 --- /dev/null +++ b/web/licenses/vben.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-present, Vben + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/web/package.json b/web/package.json index fd2fe1a4b78..86a0905ffe0 100644 --- a/web/package.json +++ b/web/package.json @@ -30,19 +30,26 @@ "chroma-js": "^2.4.2", "clsx": "^2.0.0", "dayjs": "^1.11.10", + "lodash-es": "^4.17.21", "next": "14.0.3", "nprogress": "^0.2.0", + "qs": "^6.11.2", "react": "^18", "react-dom": "^18", "react-hook-form": "^7.48.2", "react-hot-toast": "^2.4.1", "react-redux": "^8.1.3", "react-use": "^17.4.1", + "sweetalert2": "^11.10.2", "yup": "^1.3.2" }, "devDependencies": { "@iconify/react": "^4.1.1", "@next/bundle-analyzer": "^14.0.4", + "@types/lodash-es": "^4.17.12", + "@types/node": "^20.10.5", + "@types/qs": "^6.9.11", + "@types/react": "^18.2.46", "autoprefixer": "^10.4.16", "env-cmd": "^10.1.0", "eslint": "^8", @@ -50,6 +57,7 @@ "eslint-config-prettier": "^9.0.0", "postcss": "^8", "prettier": "^3.1.0", - "tailwindcss": "^3.3.5" + "tailwindcss": "^3.3.5", + "typescript": "^5.3.3" } } diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 00000000000..d6df38c1dc8 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "paths": { + "@/*": [ + "./*" + ] + }, + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + "dist/types/**/*.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/web/types/axios.d.ts b/web/types/axios.d.ts new file mode 100644 index 00000000000..fdecf9d0aae --- /dev/null +++ b/web/types/axios.d.ts @@ -0,0 +1,96 @@ +/* +MIT License + +Copyright (c) 2020-present, Vben + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * Referred from types/axios.d.ts + */ + +export type ErrorMessageMode = 'none' | 'modal' | 'message' | undefined + +export type SuccessMessageMode = ErrorMessageMode + +export interface RequestOptions { + joinParamsToUrl?: boolean + + // ** Format request parameter time + formatDate?: boolean + + // ** Whether to process the request result + isTransformResponse?: boolean + + // ** Whether to return native response headers + // ** For example: use this attribute when you need to get the response headers + isReturnNativeResponse?: boolean + + // ** Whether to join url + joinPrefix?: boolean + + // ** Interface address, use the default apiUrl if you leave it blank + apiUrl?: string + + // ** Concatenating Path for the Request + urlPrefix?: string + + // ** Error message prompt type + errorMessageMode?: ErrorMessageMode + + // ** Success message prompt type + successMessageMode?: SuccessMessageMode + + // ** Whether to add a timestamp + joinTime?: boolean + ignoreCancelToken?: boolean + + // ** Whether to send token in header + withToken?: boolean + + // ** Retry Mechanism for Requests + retryRequest?: RetryRequest +} + +export interface RetryRequest { + isOpenRetry: boolean + count: number + waitTime: number +} + +export interface Result { + code: number + type: 'success' | 'error' | 'warning' + message: string + result: T +} + +// ** multipart/form-data: upload file +export interface UploadFileParams { + data?: Recordable + + // ** File parameter interface field name + name?: string + + file: File | Blob + + filename?: string + [key: string]: any +} diff --git a/web/types/global.d.ts b/web/types/global.d.ts new file mode 100644 index 00000000000..36cb991038b --- /dev/null +++ b/web/types/global.d.ts @@ -0,0 +1,6 @@ +/* + * Copyright 2023 Datastrato Pvt Ltd. + * This software is licensed under the Apache License version 2. + */ + +declare type Recordable = Record diff --git a/web/yarn.lock b/web/yarn.lock index 2b49621dcd1..0f7a433ae87 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -552,6 +552,25 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/lodash-es@^4.17.12": + version "4.17.12" + resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b" + integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.202" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.202.tgz#f09dbd2fb082d507178b2f2a5c7e74bd72ff98f8" + integrity sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ== + +"@types/node@^20.10.5": + version "20.10.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.5.tgz#47ad460b514096b7ed63a1dae26fad0914ed3ab2" + integrity sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw== + dependencies: + undici-types "~5.26.4" + "@types/parse-json@^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" @@ -562,6 +581,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563" integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng== +"@types/qs@^6.9.11": + version "6.9.11" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.11.tgz#208d8a30bc507bd82e03ada29e4732ea46a6bbda" + integrity sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ== + "@types/react-transition-group@^4.4.8", "@types/react-transition-group@^4.4.9": version "4.4.10" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.10.tgz#6ee71127bdab1f18f11ad8fb3322c6da27c327ac" @@ -578,6 +602,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^18.2.46": + version "18.2.46" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.46.tgz#f04d6c528f8f136ea66333bc66abcae46e2680df" + integrity sha512-nNCvVBcZlvX4NU1nRRNV/mFl1nNRuTuslAJglQsq+8ldXe5Xv0Wd2f7WTE3jOxhLH2BFfiZGC6GCp+kHQbgG+w== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/scheduler@*": version "0.16.8" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" @@ -2229,6 +2262,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -2657,6 +2695,13 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== +qs@^6.11.2: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -3126,6 +3171,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +sweetalert2@^11.10.2: + version "11.10.2" + resolved "https://registry.yarnpkg.com/sweetalert2/-/sweetalert2-11.10.2.tgz#084fba88028e7ab5667ceed00e4201dda7d25294" + integrity sha512-BYlIxGw6OF9Rw2z1wlnh1U+fvHHkvtg4BGyimV9nZxQRGvCBfx9uonxgwuYpJuYqCtM+2W1KOm8iMIEb/2v7Hg== + tailwindcss@^3.3.5: version "3.3.6" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.6.tgz#4dd7986bf4902ad385d90d45fd4b2fa5fab26d5f" @@ -3301,6 +3351,11 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" +typescript@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" @@ -3311,6 +3366,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + update-browserslist-db@^1.0.13: version "1.0.13" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4"