diff --git a/web/config/config.js b/web/config/config.js
index 22a7be05c..230a77fb4 100644
--- a/web/config/config.js
+++ b/web/config/config.js
@@ -5,25 +5,69 @@ import webpackPlugin from "./plugin.config";
import defaultSettings from "../src/defaultSettings";
import packageJson from "../package.json";
-const ProxyTarget = "http://localhost:9001"
+const ProxyTarget = "https://localhost:9000";
+
+// 统一代理路径列表
+const proxyPaths = [
+ "/elasticsearch/",
+ "/_search-center/",
+ "/gateway/",
+ "/_info",
+ "/config/",
+ "/environments/",
+ "/pipeline/",
+ "/queue/",
+ "/task/",
+ "/tasks/",
+ "/debug/",
+ "/alerting/",
+ "/health",
+ "/stats",
+ "/keystore",
+ "/user",
+ "/role/",
+ "/permission/",
+ "/account/",
+ "/notification/",
+ "/agent/",
+ "/insight/",
+ "/host/",
+ "/_platform/",
+ "/migration/",
+ "/comparison/",
+ "/_license/",
+ "/setup/",
+ "/layout",
+ "/credential",
+ "/setting/",
+ "/email/",
+ "/data/",
+ "/instance",
+ "/collection/",
+];
+
+// 生成代理配置,统一加上 secure: false
+const proxy = proxyPaths.reduce(function(acc, path) {
+ acc[path] = {
+ target: ProxyTarget,
+ changeOrigin: true,
+ secure: false, // 忽略自签名证书
+ };
+ return acc;
+}, {});
export default {
- // add for transfer to umi
plugins: [
[
"umi-plugin-react",
{
antd: true,
- dva: {
- hmr: true,
- },
- targets: {
- ie: 11,
- },
+ dva: { hmr: true },
+ targets: { ie: 11 },
locale: {
- enable: true, // default false
- default: "en-US", // default zh-CN
- baseNavigator: true, // default true, when it is true, will use `navigator.language` overwrite default
+ enable: true,
+ default: "en-US",
+ baseNavigator: true,
},
dynamicImport: {
loadingComponent: "./components/PageLoading/index",
@@ -39,17 +83,8 @@ export default {
: {}),
},
],
- // [
- // 'umi-plugin-ga',
- // {
- // code: 'UA-12123-6',
- // judge: () => process.env.APP_TYPE === 'site',
- // },
- // ],
],
- targets: {
- ie: 11,
- },
+ targets: { ie: 11 },
define: {
APP_TYPE: process.env.APP_TYPE || "",
ENV: process.env.NODE_ENV,
@@ -59,170 +94,16 @@ export default {
APP_AUTHOR: packageJson.author,
APP_OFFICIAL_WEBSITE: packageJson.official_website || "",
},
- // 路由配置
routes: pageRoutes,
- // Theme for antd
- // https://ant.design/docs/react/customize-theme-cn
- theme: {
- "primary-color": defaultSettings.primaryColor,
- },
- externals: {
- // '@antv/data-set': 'DataSet',
- },
- proxy: {
- "/elasticsearch/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/_search-center/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/static/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/gateway/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/_info": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/config/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/environments/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/pipeline/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/queue/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/task/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/tasks/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/debug/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/alerting/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/health": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/stats": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/keystore": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/user": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/role/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/permission/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/account/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/notification/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/agent/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/insight/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/host/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/_platform/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/migration/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/comparison/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/_license/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/setup/": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/layout": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/credential": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/setting": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/email": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/data": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/instance": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- "/collection": {
- target: ProxyTarget,
- changeOrigin: true,
- },
- },
+ theme: { "primary-color": defaultSettings.primaryColor },
+ proxy: proxy,
ignoreMomentLocale: true,
- lessLoaderOptions: {
- javascriptEnabled: true,
- },
+ ...(process.env.NODE_ENV === "production" ? { devtool: false } : {}),
+ lessLoaderOptions: { javascriptEnabled: true },
disableRedirectHoist: true,
cssLoaderOptions: {
modules: true,
- getLocalIdent: (context, localIdentName, localName) => {
+ getLocalIdent: function(context, localIdentName, localName) {
if (
context.resourcePath.includes("node_modules") ||
context.resourcePath.includes("ant.design.pro.less") ||
@@ -236,33 +117,19 @@ export default {
const antdProPath = match[1].replace(".less", "");
const arr = antdProPath
.split("/")
- .map((a) => a.replace(/([A-Z])/g, "-$1"))
- .map((a) => a.toLowerCase());
+ .map(function(a) { return a.replace(/([A-Z])/g, "-$1"); })
+ .map(function(a) { return a.toLowerCase(); });
return `antd-pro${arr.join("-")}-${localName}`.replace(/--/g, "-");
}
return localName;
},
},
-
- // chainWebpack: webpackPlugin,
- cssnano: {
- mergeRules: false,
- },
-
- // extra configuration for egg
+ cssnano: { mergeRules: false },
runtimePublicPath: true,
hash: true,
outputPath: "../.public",
- manifest: {
- fileName: "../.public/manifest.json",
- publicPath: "",
- },
-
+ manifest: { fileName: "../.public/manifest.json", publicPath: "" },
copy: ["./src/assets/favicon.ico"],
history: "hash",
- // exportStatic: {
- // // htmlSuffix: true,
- // dynamicRoot: true,
- // },
sass: {},
};
diff --git a/web/config/router.config.js b/web/config/router.config.js
index b65fc9896..65f8994e3 100644
--- a/web/config/router.config.js
+++ b/web/config/router.config.js
@@ -1,3 +1,13 @@
+import fs from 'fs';
+import path from 'path';
+
+const pluginRoutes = [];
+
+const pluginRoutePath = path.resolve('config','router.enterprise.js');
+if (fs.existsSync(pluginRoutePath)) {
+ pluginRoutes.push(...require(pluginRoutePath).default);
+}
+
export default [
// user
{
@@ -155,7 +165,7 @@ export default [
},
],
},
-
+ ...pluginRoutes,
// alerting
{
path: "/alerting",
@@ -277,6 +287,11 @@ export default [
"agent.instance:read",
],
routes: [
+ {
+ path: "/resource/runtime",
+ hideInMenu: true,
+ hideInBreadcrumb: true,
+ },
{
path: "/resource/runtime/instance/new",
name: "runtime.new_instance",
@@ -381,6 +396,8 @@ export default [
name: "system",
icon: "setting",
authority: [
+ "system.cluster:all",
+ "system.cluster:read",
"system.credential:all",
"system.credential:read",
"system.security:all",
@@ -391,11 +408,27 @@ export default [
"system.smtp_server:read"
],
routes: [
+ {
+ path: "/system/settings",
+ name: "settings",
+ component: "./System/Settings/index",
+ authority: [
+ "system.cluster:all",
+ "system.cluster:read",
+ "system.smtp_server:all",
+ "system.smtp_server:read",
+ ],
+ },
{
path: "/system/email_server",
- name: "smtp_server",
- component: "./System/Email/Server",
- authority: ["system.smtp_server:all", "system.smtp_server:read"],
+ component: "./System/Settings/index",
+ hideInMenu: true,
+ authority: [
+ "system.cluster:all",
+ "system.cluster:read",
+ "system.smtp_server:all",
+ "system.smtp_server:read",
+ ],
},
{
path: "/system/credential",
diff --git a/web/mock/api.js b/web/mock/api.js
index b20bb393f..a45f60f4d 100644
--- a/web/mock/api.js
+++ b/web/mock/api.js
@@ -374,22 +374,6 @@ export default {
"eol_date": "2023-12-31T10:10:10Z"
},
"tagline": "A light-weight but powerful agent."
- },
- "basic_auth": {},
- "endpoint": "http://192.168.3.25:2900",
- "host": {
- "name": "INFINI-4.local",
- "os": {
- "name": "darwin",
- "architecture": "arm64",
- "version": "22.6.0"
- }
- },
- "network": {
- "ip": [
- "192.168.3.25"
- ],
- "major_ip": "192.168.3.25"
}
});
},
diff --git a/web/mock/rbac/admin.js b/web/mock/rbac/admin.js
index 4194be7da..398b96fca 100644
--- a/web/mock/rbac/admin.js
+++ b/web/mock/rbac/admin.js
@@ -101,18 +101,18 @@ export default {
"indices.get_mapping",
"indices.upgrade",
"indices.validate_query",
- "indices.exists_template",
+ "template.exists",
"indices.get_upgrade",
"indices.update_aliases",
"indices.analyze",
"indices.exists",
"indices.close",
- "indices.delete_template",
+ "template.delete",
"indices.get_field_mapping",
"indices.delete_alias",
"indices.exists_type",
- "indices.get_template",
- "indices.put_template",
+ "template.get",
+ "template.put",
"indices.refresh",
"indices.segments",
"indices.termvectors",
diff --git a/web/src/app.js b/web/src/app.js
index 79a046140..cb9efd9f2 100644
--- a/web/src/app.js
+++ b/web/src/app.js
@@ -21,12 +21,30 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-import { getAuthEnabled } from "./utils/authority";
+import {
+ getAuthEnabled,
+ getEnterpriseTaskManagerEnabled,
+ refreshApplicationSettings,
+} from "./utils/authority";
+import { startActivityAwareTokenRefresh } from "./utils/auth_session";
import request from "./utils/request";
import { setSetupRequired } from "@/utils/setup";
import { getHealth } from "@/services/system"
import PlatformContainer from "./components/PlatformContainer";
import React from "react";
+import { message, notification } from "antd";
+
+message.config({
+ maxCount: 3,
+});
+
+notification.config({
+ placement: "topRight",
+});
+
+const CHUNK_RELOAD_KEY = "console.chunk-reload";
+const CHUNK_RELOAD_WINDOW_MS = 15000;
+const CHUNK_RELOAD_QUERY_KEY = "_reload_ts";
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function(search, replacement) {
@@ -35,17 +53,176 @@ if (!String.prototype.replaceAll) {
};
}
+const isChunkLoadError = (error) => {
+ const reason =
+ error?.message ||
+ error?.reason?.message ||
+ error?.reason ||
+ "";
+ return /ChunkLoadError|Loading chunk [\d]+ failed|CSS_CHUNK_LOAD_FAILED/i.test(
+ `${reason}`
+ );
+};
+
+const isInjectedScriptConnectionError = (error) => {
+ const reason =
+ error?.message ||
+ error?.reason?.message ||
+ error?.reason ||
+ error ||
+ "";
+ const stack = `${error?.stack || error?.reason?.stack || ""}`;
+ return (
+ `${reason}`.includes("Could not establish connection. Receiving end does not exist.") &&
+ (stack.includes("single-file-bootstrap.bundle.js") || stack === "")
+ );
+};
+
+const suppressUnhandledRejection = (event) => {
+ event.preventDefault?.();
+ event.stopImmediatePropagation?.();
+ event.stopPropagation?.();
+};
+
+const getCanonicalLocation = () => {
+ const url = new URL(window.location.href);
+ url.searchParams.delete(CHUNK_RELOAD_QUERY_KEY);
+ return `${url.pathname}${url.search}${url.hash}`;
+};
+
+const buildReloadLocation = () => {
+ const url = new URL(window.location.href);
+ url.searchParams.set(CHUNK_RELOAD_QUERY_KEY, `${Date.now()}`);
+ return `${url.pathname}${url.search}${url.hash}`;
+};
+
+const shouldReloadForChunkError = () => {
+ const now = Date.now();
+ const currentLocation = getCanonicalLocation();
+
+ try {
+ const raw = sessionStorage.getItem(CHUNK_RELOAD_KEY);
+ const previous = raw ? JSON.parse(raw) : null;
+ if (
+ previous?.location === currentLocation &&
+ now - Number(previous?.timestamp || 0) < CHUNK_RELOAD_WINDOW_MS
+ ) {
+ return false;
+ }
+ sessionStorage.setItem(
+ CHUNK_RELOAD_KEY,
+ JSON.stringify({
+ location: currentLocation,
+ timestamp: now,
+ })
+ );
+ } catch (e) {}
+
+ return true;
+};
+
+const installChunkReloadRecovery = () => {
+ if (typeof window === "undefined" || window.__consoleChunkReloadRecoveryInstalled) {
+ return;
+ }
+
+ window.__consoleChunkReloadRecoveryInstalled = true;
+
+ const handleChunkFailure = (error) => {
+ if (!isChunkLoadError(error) || !shouldReloadForChunkError()) {
+ return false;
+ }
+ window.location.replace(buildReloadLocation());
+ return true;
+ };
+
+ window.addEventListener("error", (event) => {
+ handleChunkFailure(event?.error || new Error(event?.message || ""));
+ });
+
+ window.addEventListener("unhandledrejection", (event) => {
+ if (handleChunkFailure(event?.reason)) {
+ suppressUnhandledRejection(event);
+ return;
+ }
+ if (isInjectedScriptConnectionError(event?.reason || event)) {
+ suppressUnhandledRejection(event);
+ }
+ }, true);
+};
+
+const serializeConsoleArgs = (args = []) => {
+ return args
+ .map((arg) => {
+ if (typeof arg === "string") {
+ return arg;
+ }
+ if (arg instanceof Error) {
+ return arg.stack || arg.message;
+ }
+ try {
+ return JSON.stringify(arg);
+ } catch (e) {
+ return String(arg);
+ }
+ })
+ .join(" ");
+};
+
+const isUmiDllWarning = (args = []) => {
+ const message = serializeConsoleArgs(args);
+ if (!/warning:/i.test(message)) {
+ return false;
+ }
+ const stack = new Error().stack || "";
+ return stack.includes("/umi.dll.js");
+};
+
+const installConsoleWarningFilter = () => {
+ if (
+ typeof window === "undefined" ||
+ process.env.NODE_ENV === "development" ||
+ window.__consoleWarningFilterInstalled
+ ) {
+ return;
+ }
+
+ window.__consoleWarningFilterInstalled = true;
+
+ ["warn", "error"].forEach((method) => {
+ const original = console[method];
+ if (typeof original !== "function") {
+ return;
+ }
+ console[method] = function(...args) {
+ if (isUmiDllWarning(args)) {
+ return;
+ }
+ return original.apply(this, args);
+ };
+ });
+};
+
+installChunkReloadRecovery();
+installConsoleWarningFilter();
+
export async function patchRoutes(routes) {
+ await refreshApplicationSettings();
const healthRes = await getHealth();
setSetupRequired(`${healthRes?.setup_required}`);
+ if (getEnterpriseTaskManagerEnabled() !== "true") {
+ routes = hideRoutesInMenu(routes, ["data_tools"], "");
+ }
if (getAuthEnabled() === "false") {
routes = filterRoutes(routes, ["system.security"], "");
// routes = disableAuth(routes);
}
+ return routes;
}
export function render(oldRoutes) {
+ startActivityAwareTokenRefresh();
oldRoutes();
}
@@ -64,7 +241,7 @@ export function render(oldRoutes) {
function filterRoutes(routes, names, prefix) {
return routes.filter((route) => {
if (route.name) {
- const pn = `${prefix}.${route.name}`;
+ const pn = prefix ? `${prefix}.${route.name}` : route.name;
if (names.includes(pn)) {
return false;
}
@@ -80,7 +257,27 @@ function filterRoutes(routes, names, prefix) {
});
}
+function hideRoutesInMenu(routes, names, prefix) {
+ return (routes || []).map((route) => {
+ const nextRoute = { ...route };
+ if (nextRoute.name) {
+ const pn = prefix ? `${prefix}.${nextRoute.name}` : nextRoute.name;
+ if (names.includes(pn)) {
+ nextRoute.hideInMenu = true;
+ }
+ }
+ if (nextRoute.routes) {
+ let pn = "";
+ if (nextRoute.name) {
+ pn = prefix ? `${prefix}.${nextRoute.name}` : nextRoute.name;
+ }
+ nextRoute.routes = hideRoutesInMenu(nextRoute.routes, names, pn);
+ }
+ return nextRoute;
+ });
+}
+
export function rootContainer(container) {
return React.createElement(PlatformContainer, null, container);
-}
\ No newline at end of file
+}
diff --git a/web/src/components/GlobalHeader/RightContent.js b/web/src/components/GlobalHeader/RightContent.js
index 61a8171cc..663b5a2c8 100644
--- a/web/src/components/GlobalHeader/RightContent.js
+++ b/web/src/components/GlobalHeader/RightContent.js
@@ -20,8 +20,18 @@ import EmptyNoticeSvg from "@/assets/emptyNotice.svg";
import EmptyTodoSvg from "@/assets/emptyTodo.svg";
import router from "umi/router";
+const isMacPlatform = () =>
+ typeof window !== "undefined" &&
+ /(Mac|iPhone|iPad|iPod)/i.test(window.navigator.platform);
+
export default class GlobalHeaderRight extends PureComponent {
state = { consoleVisible: false, notificationPopupVisible: false };
+
+ isConsoleToggleShortcut = (event) => {
+ const key = (event.key || "").toLowerCase();
+ return (event.ctrlKey || event.metaKey) && event.shiftKey && (key === "o" || event.keyCode === 79);
+ };
+
getNoticeData() {
const { notices = [] } = this.props;
if (notices.length === 0) {
@@ -43,38 +53,33 @@ export default class GlobalHeaderRight extends PureComponent {
this.setState({
consoleVisible: visible,
});
+ document.body.style.overflow = visible ? "hidden" : "";
var sl = document.querySelector("#root>div");
if (sl) {
sl.style.paddingBottom = "0px";
}
};
onKeyDown = (e) => {
- const { keyCode } = e;
- if (this.keysPressed["17"] && this.keysPressed["16"] && keyCode == 79) {
+ const hasDevtoolPrivilege =
+ hasAuthority("devtool.console:all") ||
+ hasAuthority("devtool.console:read");
+ if (hasDevtoolPrivilege && this.isConsoleToggleShortcut(e)) {
+ e.preventDefault();
if (this.state.consoleVisible) document.body.style.overflow = "";
this.setConsoleVisible(!this.state.consoleVisible);
return true;
}
- this.keysPressed[keyCode] = e.type == "keydown";
return false;
};
- onKeyUp = (e) => {
- const { keyCode } = e;
- delete this.keysPressed[keyCode];
- };
constructor(props) {
super(props);
this.onKeyDown = this.onKeyDown.bind(this);
- this.onKeyUp = this.onKeyUp.bind(this);
}
componentDidMount() {
- this.keysPressed = {};
document.addEventListener("keydown", this.onKeyDown, false);
- document.addEventListener("keyup", this.onKeyUp, false);
}
componentWillUnmount() {
document.removeEventListener("keydown", this.onKeyDown);
- document.removeEventListener("keyup", this.onKeyUp);
}
render() {
@@ -132,6 +137,9 @@ export default class GlobalHeaderRight extends PureComponent {
const hasDevtoolPrivilege =
hasAuthority("devtool.console:all") ||
hasAuthority("devtool.console:read");
+ const consoleShortcutLabel = isMacPlatform()
+ ? "Cmd+Shift+O"
+ : "Ctrl+Shift+O";
return (
diff --git a/web/src/components/Licence/index.js b/web/src/components/Licence/index.js
index a2f9066d7..f11371dd3 100644
--- a/web/src/components/Licence/index.js
+++ b/web/src/components/Licence/index.js
@@ -51,7 +51,7 @@ export default forwardRef((props, ref) => {
return (
,
- placeholder: 'mobile number',
+ placeholder: formatMessage({ id: 'app.login.mobile.placeholder' }),
},
rules: [
{
required: true,
- message: 'Please enter mobile number!',
+ message: formatMessage({ id: 'app.login.mobile.required' }),
},
{
pattern: /^1\d{10}$/,
- message: 'Wrong mobile number format!',
+ message: formatMessage({ id: 'app.login.mobile.invalid' }),
},
],
},
@@ -51,12 +52,12 @@ export default {
props: {
size: 'large',
prefix: ,
- placeholder: 'captcha',
+ placeholder: formatMessage({ id: 'app.login.captcha.placeholder' }),
},
rules: [
{
required: true,
- message: 'Please enter Captcha!',
+ message: formatMessage({ id: 'app.login.captcha.required' }),
},
],
},
diff --git a/web/src/layouts/BasicLayout.js b/web/src/layouts/BasicLayout.js
index 30348ebe4..83b8a1940 100644
--- a/web/src/layouts/BasicLayout.js
+++ b/web/src/layouts/BasicLayout.js
@@ -19,7 +19,14 @@ import Header from "./Header";
import Context from "./MenuContext";
import Exception403 from "../pages/Exception/403";
import { GlobalContext } from "./GlobalContext";
-import { getAuthEnabled, getAuthority, isLogin } from "@/utils/authority";
+import {
+ APPLICATION_SETTINGS_UPDATED_EVENT,
+ getAuthEnabled,
+ getAuthority,
+ getEnterpriseTaskManagerEnabled,
+ isLogin,
+ refreshApplicationSettings,
+} from "@/utils/authority";
import { router, history } from "umi";
import request from "@/utils/request";
import HealthProvider from "@/components/HealthProvider";
@@ -69,7 +76,7 @@ const { Content } = Layout;
function formatter(data, parentAuthority, parentName) {
return data
.map((item) => {
- let locale = "menu";
+ let locale;
if (parentName && item.name) {
locale = `${parentName}.${item.name}`;
} else if (item.name) {
@@ -97,6 +104,19 @@ function formatter(data, parentAuthority, parentName) {
.filter((item) => item);
}
+function filterMenuDataByName(menuData, names = []) {
+ return (menuData || []).map((item) => {
+ const nextItem = { ...item };
+ if (names.includes(nextItem.name)) {
+ nextItem.hideInMenu = true;
+ }
+ if (nextItem.children) {
+ nextItem.children = filterMenuDataByName(nextItem.children, names);
+ }
+ return nextItem;
+ });
+}
+
const memoizeOneFormatter = memoizeOne(formatter, isEqual);
const query = {
@@ -179,6 +199,29 @@ class BasicLayout extends React.PureComponent {
// });
}
});
+ this.handleDataToolsLicenseRequired = () => {
+ this.licenceRef?.openToTab?.("license");
+ };
+ window.addEventListener(
+ "console:datatools-license-required",
+ this.handleDataToolsLicenseRequired
+ );
+ this.handleApplicationSettingsUpdated = async () => {
+ const menuData = this.getMenuData();
+ this.setState({ menuData });
+ await dispatch({
+ type: "global/saveData",
+ payload: {
+ menuData,
+ },
+ });
+ };
+ window.addEventListener(
+ APPLICATION_SETTINGS_UPDATED_EVENT,
+ this.handleApplicationSettingsUpdated
+ );
+ await refreshApplicationSettings(true);
+ await this.handleApplicationSettingsUpdated();
let firstLogin = localStorage.getItem("first-login");
if (firstLogin === "true" && isLogin()) {
localStorage.setItem("first-login", false);
@@ -240,6 +283,14 @@ class BasicLayout extends React.PureComponent {
componentWillUnmount() {
cancelAnimationFrame(this.renderRef);
unenquireScreen(this.enquireHandler);
+ window.removeEventListener(
+ "console:datatools-license-required",
+ this.handleDataToolsLicenseRequired
+ );
+ window.removeEventListener(
+ APPLICATION_SETTINGS_UPDATED_EVENT,
+ this.handleApplicationSettingsUpdated
+ );
}
getContext() {
@@ -254,7 +305,11 @@ class BasicLayout extends React.PureComponent {
const {
route: { routes },
} = this.props;
- return memoizeOneFormatter(routes);
+ let menuData = memoizeOneFormatter(routes);
+ if (getEnterpriseTaskManagerEnabled() !== "true") {
+ menuData = filterMenuDataByName(menuData, ["data_tools"]);
+ }
+ return menuData;
//
}
@@ -290,8 +345,12 @@ class BasicLayout extends React.PureComponent {
if (!currRouterData) {
return APP_TITLE;
}
+ const messageId = currRouterData.locale || currRouterData.name;
+ if (!messageId) {
+ return APP_TITLE;
+ }
const message = formatMessage({
- id: currRouterData.locale || currRouterData.name,
+ id: messageId,
defaultMessage: currRouterData.name,
});
return `${message} - ${APP_TITLE}`;
@@ -348,7 +407,16 @@ class BasicLayout extends React.PureComponent {
} = this.props;
const { isMobile, menuData } = this.state;
const isTop = PropsLayout === "topmenu";
+ const isDevtoolConsolePage = pathname.startsWith("/devtool/console");
+ const hideFooter = isDevtoolConsolePage;
const routerConfig = this.matchParamsPath(pathname);
+ const contentStyle = isDevtoolConsolePage
+ ? {
+ ...this.getContentStyle(),
+ margin: 0,
+ overflow: "hidden",
+ }
+ : this.getContentStyle();
const renderInvalidSecretNotification = () => {
const secretMismatch = localStorage.getItem("secret_mismatch");
@@ -389,7 +457,7 @@ class BasicLayout extends React.PureComponent {
{...this.props}
/>
-
+
}
@@ -405,7 +473,7 @@ class BasicLayout extends React.PureComponent {
-
+ {!hideFooter ? : null}
>
diff --git a/web/src/layouts/Footer.js b/web/src/layouts/Footer.js
index 28fff4553..57436ff79 100644
--- a/web/src/layouts/Footer.js
+++ b/web/src/layouts/Footer.js
@@ -7,7 +7,7 @@ const FooterView = () => (
);
diff --git a/web/src/layouts/Header.js b/web/src/layouts/Header.js
index 69ce157bc..a91a2041d 100644
--- a/web/src/layouts/Header.js
+++ b/web/src/layouts/Header.js
@@ -11,6 +11,7 @@ import Authorized from "@/utils/Authorized";
import { getSetupRequired } from "@/utils/setup";
const { Header } = Layout;
+const CLUSTER_STATUS_REFRESH_INTERVAL = 60 * 1000;
class HeaderView extends PureComponent {
state = {
@@ -28,15 +29,17 @@ class HeaderView extends PureComponent {
componentDidMount() {
document.addEventListener("scroll", this.handScroll, { passive: true });
- this.fetchClusterStatus();
+ document.addEventListener("visibilitychange", this.handleVisibilityChange);
+ window.addEventListener("focus", this.handleWindowFocus);
+ this.fetchClusterStatus({ force: true });
this.handleNoticeVisibleChange(true);
}
componentWillUnmount() {
document.removeEventListener("scroll", this.handScroll);
- if (this.fetchClusterStatusTimer) {
- clearTimeout(this.fetchClusterStatusTimer);
- }
+ document.removeEventListener("visibilitychange", this.handleVisibilityChange);
+ window.removeEventListener("focus", this.handleWindowFocus);
+ this.clearClusterStatusTimer();
}
getHeadWidth = () => {
@@ -149,24 +152,59 @@ class HeaderView extends PureComponent {
});
};
- fetchClusterStatus = async () => {
+ clearClusterStatusTimer = () => {
+ if (this.fetchClusterStatusTimer) {
+ clearTimeout(this.fetchClusterStatusTimer);
+ this.fetchClusterStatusTimer = null;
+ }
+ };
+
+ scheduleClusterStatusRefresh = () => {
+ this.clearClusterStatusTimer();
+ if (document.visibilityState === "hidden") {
+ return;
+ }
+ this.fetchClusterStatusTimer = setTimeout(() => {
+ this.fetchClusterStatus();
+ }, CLUSTER_STATUS_REFRESH_INTERVAL);
+ };
+
+ handleVisibilityChange = () => {
+ if (document.visibilityState === "hidden") {
+ this.clearClusterStatusTimer();
+ return;
+ }
+ this.fetchClusterStatus();
+ };
+
+ handleWindowFocus = () => {
+ if (document.visibilityState === "hidden") {
+ return;
+ }
+ this.fetchClusterStatus();
+ };
+
+ fetchClusterStatus = async ({ force = false } = {}) => {
if (
location.href.indexOf("/guide/initialization") !== -1 ||
- getSetupRequired() === "true"
+ getSetupRequired() === "true" ||
+ document.visibilityState === "hidden"
) {
+ this.clearClusterStatusTimer();
return;
}
const { dispatch } = this.props;
const res = await dispatch({
type: "global/fetchClusterStatus",
+ payload: {
+ force,
+ },
});
- if (this.fetchClusterStatusTimer) {
- clearTimeout(this.fetchClusterStatusTimer);
- }
+ this.clearClusterStatusTimer();
if (!res) {
return;
}
- this.fetchClusterStatusTimer = setTimeout(this.fetchClusterStatus, 10000);
+ this.scheduleClusterStatusRefresh();
};
render() {
diff --git a/web/src/layouts/UserLayout.js b/web/src/layouts/UserLayout.js
index 9362c59b2..63657e6ee 100644
--- a/web/src/layouts/UserLayout.js
+++ b/web/src/layouts/UserLayout.js
@@ -26,7 +26,7 @@ const links = [
];
const copyright = (
-
+
Copyright {new Date().getFullYear()} {APP_AUTHOR}
);
diff --git a/web/src/locales/en-US.js b/web/src/locales/en-US.js
index 4863bb071..6c42cc873 100644
--- a/web/src/locales/en-US.js
+++ b/web/src/locales/en-US.js
@@ -18,6 +18,11 @@ import listview from "./en-US/listview";
import audit from "./en-US/audit";
import error from "./en-US/error";
+let migration = {};
+try {
+ migration = require("./en-US/migration").default ?? {};
+} catch (e) {}
+
export default {
"navBar.lang": "Languages",
"layout.user.appslogon":
@@ -129,8 +134,63 @@ export default {
"form.button.clean.deleted.indices.desc": "Are you sure to clean indices that are deleted?",
"component.refreshGroup.label.title": "Auto Refresh",
"component.refreshGroup.label.every": "Every",
+ "data_tools.task.keyword": "Search by keyword",
+ "table.field.id": "ID",
"table.field.actions": "Actions",
+ "table.field.listen_address": "Listen Address",
+ "table.field.cmdline": "Cmdline",
+ "table.field.name": "Name",
+ "table.field.pid": "PID",
+ "system.security.tab.user": "User",
+ "system.security.tab.role": "Role",
+ "system.security.search.placeholder": "Type keyword to search",
+ "system.security.pagination.total": "{start}-{end} of {total} items",
+ "system.security.confirm.delete": "Are you sure you want to delete this item?",
+ "system.role.platform.name.required": "Please input name!",
+ "system.role.platform.feature_privilege.label": "Feature privileges",
+ "system.role.platform.feature_privilege.required":
+ "Please select platform feature privilege!",
+ "system.role.data.cluster.label": "Cluster",
+ "system.role.data.cluster.required": "Please select cluster!",
+ "system.role.data.cluster_privilege.label": "Cluster Privilege",
+ "system.role.data.cluster_privilege.required":
+ "Please select cluster privilege!",
+ "system.role.data.index_privilege.label": "Index Privilege",
+ "system.role.data.index_privilege.required":
+ "Please select index privilege!",
+ "system.role.data.api_privilege.category": "Category",
+ "system.role.data.api_privilege.privilege": "Privilege",
+ "system.role.data.index_privilege.index": "Index",
+ "system.role.data.index_privilege.privilege": "Privilege",
+ "system.security.user.table.name": "Name",
+ "system.security.user.table.nickname": "Nickname",
+ "system.security.user.table.roles": "Roles",
+ "system.security.user.table.phone": "Phone",
+ "system.security.user.table.email": "Email",
+ "system.security.user.table.tags": "Tags",
+ "system.security.user.action.reset_password": "Reset Password",
+ "system.security.user.form.name": "User Name",
+ "system.security.user.form.nickname": "Nick Name",
+ "system.security.user.form.phone": "Phone",
+ "system.security.user.form.email": "Email",
+ "system.security.user.form.roles": "Role",
+ "system.security.user.form.tags": "Tags",
+ "system.security.user.form.name.required": "Please input name!",
+ "system.security.user.form.email.invalid": "The input is not valid email!",
+ "system.security.user.form.roles.required": "Please select roles!",
+ "system.security.user.create.success": "Successfully Created User!",
+ "system.security.user.create.password": "Password: {password}",
+ "system.security.user.create.copy_password": "Copy Password",
+ "system.security.role.table.name": "Name",
+ "system.security.role.table.type": "Type",
+ "system.security.role.table.builtin": "Builtin",
+ "system.security.role.table.description": "Description",
+ "system.security.role.menu.add_platform": "Add Platform Role",
+ "system.security.role.menu.add_data": "Add Data Role",
+ "system.security.role.create.success": "Role created successfully!",
+ "system.security.role.create.button.view_list": "Back to Role List",
+ "system.security.role.create.button.continue": "Continue Creating",
"component.globalHeader.search": "Search",
"component.globalHeader.search.example1": "Search example 1",
@@ -158,6 +218,30 @@ export default {
"menu.insight": "DATA INSIGHT",
"menu.insight.dashboard": "DASHBOARD",
"menu.insight.discover": "DISCOVER",
+ "insight.config.title": "Insight Config",
+ "insight.config.tab.search": "Search",
+ "insight.config.search.track_total_hits": "Track Total Hits",
+ "insight.config.search.timeout": "Timeout",
+ "insight.config.search.field_summary": "Field Summary",
+ "insight.config.search.whether_to_sample": "Whether to Sample",
+ "insight.config.search.sample_records": "Sample Records",
+ "insight.config.search.sample_records.all": "All Records",
+ "insight.config.search.sample_records.manual": "Manual Setting",
+ "insight.config.search.top_number": "Top Number",
+ "insight.config.search.time_interval.seconds": "Seconds",
+ "insight.config.search.time_interval.minutes": "Minutes",
+ "insight.config.search.time_interval.hours": "Hours",
+ "insight.config.search.time_interval.days": "Days",
+ "insight.export.csv": "Export CSV",
+ "insight.export.excel": "Export Excel",
+ "insight.export.empty": "No data available to export",
+ "insight.discover.empty.no_indices_or_views":
+ "The current cluster has no indices or views",
+ "insight.discover.empty.create_now": "Create Now",
+ "insight.share.copy": "Copy share link",
+ "insight.share.success": "Share link copied",
+ "insight.share.failed": "Failed to copy share link",
+ "insight.button.updating": "Updating",
//new alert version
"menu.alerting.rule": "RULES",
@@ -226,12 +310,17 @@ export default {
"menu.resource.runtime.config": "CONFIG",
"menu.resource.cluster": "CLUSTER",
"menu.resource.registCluster": "REGISTER CLUSTER",
+ "menu.resource.editCluster": "EDIT CLUSTER",
"menu.sysresourcetem.editCluster": "EDIT CLUSTER",
"menu.resource.agent": "AGENTS",
"menu.resource.agent.new_instance": "REGISTER AGENT",
"menu.resource.agent.edit_instance": "EDIT AGENT",
+ "menu.data_tools": "DATA TOOLS",
+ "menu.data_tools.migration": "MIGRATION",
+ "menu.data_tools.comparison": "COMPARISON",
+
"menu.search": "SEARCH",
"menu.search.overview": "OVERVIEW",
"menu.search.template": "TEMPLATES",
@@ -251,8 +340,8 @@ export default {
"menu.backup.index": "BACKUPS",
"menu.backup.lifecycle": "POLICIES",
- "menu.system": "SETTINGS",
- "menu.system.settings": "SETTINGS",
+ "menu.system": "SYSTEM",
+ "menu.system.settings": "SYSTEM SETTINGS",
"menu.system.settings.global": "GLOBAL",
"menu.system.settings.gateway": "GATEWAY",
@@ -389,7 +478,9 @@ export default {
"There is no available cluster, click OK to automatically jump to System Settings => Cluster Settings",
"app.message.system-tips.no-available-cluster-data-permission":
"There is no available cluster, please make sure you have cluster data permission",
- "app.message.confirm.delete": "Sure to delete?",
+ "app.message.confirm.delete": "Are you sure you want to delete this item?",
+ "app.message.confirm.delete.multiple": "Are you sure you want to delete these {count} items?",
+ "document.confirm.cancel": "Sure to cancel?",
"app.message.warning.table.select-row": "Please select a table row",
"app.message.warning.invalid.params": "Invalid parameter",
@@ -402,6 +493,13 @@ export default {
"app.login.sign-in-with": "Sign in with SSO",
"app.login.signup": "Sign up",
"app.login.login": "Login",
+ "app.login.username.required": "Please enter username!",
+ "app.login.password.required": "Please enter password!",
+ "app.login.mobile.placeholder": "Mobile number",
+ "app.login.mobile.required": "Please enter mobile number!",
+ "app.login.mobile.invalid": "Wrong mobile number format!",
+ "app.login.captcha.placeholder": "Captcha",
+ "app.login.captcha.required": "Please enter captcha!",
"app.register.register": "Register",
"app.register.get-verification-code": "Get code",
"app.register.sing-in": "Already have an account?",
@@ -624,6 +722,7 @@ export default {
...alias,
...guide,
...license,
+ ...migration,
...credential,
...overview,
...dashboard,
diff --git a/web/src/locales/en-US/command.js b/web/src/locales/en-US/command.js
index 5193c91ff..2a35da081 100644
--- a/web/src/locales/en-US/command.js
+++ b/web/src/locales/en-US/command.js
@@ -1,7 +1,11 @@
export default {
"command.table.field.name": "Name",
"command.table.field.tag": "Tag",
+ "command.table.field.creator": "Creator",
+ "command.table.field.created": "Created",
"command.table.field.content": "Content",
+ "command.table.field.summary": "Summary",
+ "command.table.summary.requests": "{count} requests",
"command.manage.edit.title": "Command",
"command.btn.newtag": "Add New Tag",
"command.message.invalid.tag": "invalid tag",
@@ -9,7 +13,7 @@ export default {
"command.manage.title": "COMMANDS",
"command.manage.description":
"Commonly used commands can help you save frequently used requests and load them quickly through the LOAD command in the development tool.",
- "console.menu.copy_as_curl": "Copy As Curl",
+ "console.menu.copy_as_curl": "Copy As cURL Command",
"console.menu.auto_indent": "Auto Indent",
"console.menu.save_as_command": "Save As Command",
};
diff --git a/web/src/locales/en-US/credential.js b/web/src/locales/en-US/credential.js
index 308ea42c6..afa3e0022 100644
--- a/web/src/locales/en-US/credential.js
+++ b/web/src/locales/en-US/credential.js
@@ -18,5 +18,10 @@ export default {
"credential.manage.form.name": "Name",
"credential.manage.form.username": "Username",
"credential.manage.form.password": "Password",
+ "credential.manage.form.token": "Token",
+ "credential.manage.form.token.required": "Please input token!",
+ "credential.manage.form.token.placeholder": "Please input token!",
+ "credential.manage.form.token.placeholder.edit":
+ "Original token is not displayed",
"credential.manage.form.tags": "Tags",
};
diff --git a/web/src/locales/en-US/error.js b/web/src/locales/en-US/error.js
index 88a2cc968..d925ac1e4 100644
--- a/web/src/locales/en-US/error.js
+++ b/web/src/locales/en-US/error.js
@@ -2,4 +2,7 @@ export default {
"error.split": ", ",
"error.unknown": "unknown error, please try again later or contact the support team!",
"error.request_timeout_error": "request timeout, please try again later!",
+ "error.request.connection_refused": "Failed to connect to the server.",
+ "error.request.connection_refused.tip":
+ "Failed to connect to the server. Click the \"Services are limited\" link above to open Dev Tools and check the system cluster status.",
};
diff --git a/web/src/locales/en-US/settings.js b/web/src/locales/en-US/settings.js
index e8486fe23..4e08b8574 100644
--- a/web/src/locales/en-US/settings.js
+++ b/web/src/locales/en-US/settings.js
@@ -1,6 +1,49 @@
export default {
"settings.email.server.empty.label1": "You can add email servers here",
"settings.email.server.empty.label2":
- "The alart center can send a notification to the recipient through the designated mail server",
+ "The alert center can send notifications to recipients through the designated mail server.",
"settings.email.server.empty.button.new": "Add email server",
+ "settings.email.server.form.name": "Name",
+ "settings.email.server.form.host": "Host",
+ "settings.email.server.form.port": "Port",
+ "settings.email.server.form.tls_min_version": "TLS Min Version",
+ "settings.email.server.form.tls": "TLS",
+ "settings.email.server.form.enabled": "Enabled",
+ "settings.email.server.form.recipient": "Recipient",
+ "settings.email.server.form.recipient.placeholder": "Please input recipient",
+ "settings.email.server.form.test.button": "Send a Test Email",
+ "settings.email.server.form.validation.name": "Please input name!",
+ "settings.email.server.form.validation.host":
+ "Please input SMTP server host!",
+ "settings.email.server.form.validation.port":
+ "Please input SMTP server port!",
+ "settings.email.server.form.validation.recipient":
+ "Recipient email is invalid",
+ "settings.email.server.form.temp_name": "New Config Name",
+ "settings.email.server.message.test.success": "Sent successfully",
+ "settings.system.tab.general": "General",
+ "settings.system.tab.email": "Email Server",
+ "settings.system.retention.title": "Data Retention",
+ "settings.system.retention.description":
+ "Update how many days system-managed data is retained before ILM deletes it.",
+ "settings.system.retention.help":
+ "The default retention is 30 days and the default rollover size is 50 GB. Saving this setting updates the system ILM retention policy for managed system indices.",
+ "settings.system.retention.unit": "days",
+ "settings.system.retention.size.label": "Rollover size",
+ "settings.system.retention.size.unit": "GB",
+ "settings.system.retention.save": "Save",
+ "settings.system.retention.update.success":
+ "Data retention updated successfully",
+ "settings.system.retention.validation.days":
+ "Please enter a valid retention days value",
+ "settings.system.retention.validation.max_size":
+ "Please enter a valid rollover size in GB, such as 50",
+ "settings.system.rollup.title": "Rollup",
+ "settings.system.rollup.description":
+ "Enable or stop the system cluster rollup jobs from Console system settings.",
+ "settings.system.rollup.enabled": "On",
+ "settings.system.rollup.disabled": "Off",
+ "settings.system.rollup.help":
+ "Turning Rollup off will stop rollup jobs and disable rollup search in cluster settings.",
+ "settings.system.rollup.update.success": "Rollup setting updated successfully",
};
diff --git a/web/src/locales/zh-CN.js b/web/src/locales/zh-CN.js
index 80441dfb5..3e1cbf910 100644
--- a/web/src/locales/zh-CN.js
+++ b/web/src/locales/zh-CN.js
@@ -18,6 +18,11 @@ import listview from "./zh-CN/listview";
import audit from "./zh-CN/audit";
import error from "./zh-CN/error";
+let migration = {};
+try {
+ migration = require("./zh-CN/migration").default ?? {};
+} catch (e) {}
+
export default {
"navBar.lang": "语言",
"layout.user.appslogon": "专业的开源搜索与实时数据分析企业级管控平台",
@@ -134,8 +139,60 @@ export default {
"form.button.clean.deleted.indices.desc": "确定清除已删除的索引吗?",
"component.refreshGroup.label.title": "自动刷新",
"component.refreshGroup.label.every": "每隔",
+ "data_tools.task.keyword": "按关键词搜索",
+ "table.field.id": "编号",
"table.field.actions": "操作",
+ "table.field.listen_address": "监听地址",
+ "table.field.cmdline": "命令行",
+ "table.field.name": "名称",
+ "table.field.pid": "进程ID",
+ "system.security.tab.user": "用户",
+ "system.security.tab.role": "角色",
+ "system.security.search.placeholder": "输入关键字搜索",
+ "system.security.pagination.total": "{start}-{end} / 共 {total} 条",
+ "system.security.confirm.delete": "确认删除?",
+ "system.role.platform.name.required": "请输入名称!",
+ "system.role.platform.feature_privilege.label": "功能权限",
+ "system.role.platform.feature_privilege.required": "请选择平台功能权限!",
+ "system.role.data.cluster.label": "集群",
+ "system.role.data.cluster.required": "请选择集群!",
+ "system.role.data.cluster_privilege.label": "集群权限",
+ "system.role.data.cluster_privilege.required": "请选择集群权限!",
+ "system.role.data.index_privilege.label": "索引权限",
+ "system.role.data.index_privilege.required": "请选择索引权限!",
+ "system.role.data.api_privilege.category": "分类",
+ "system.role.data.api_privilege.privilege": "权限",
+ "system.role.data.index_privilege.index": "索引",
+ "system.role.data.index_privilege.privilege": "权限",
+ "system.security.user.table.name": "名称",
+ "system.security.user.table.nickname": "昵称",
+ "system.security.user.table.roles": "角色",
+ "system.security.user.table.phone": "电话",
+ "system.security.user.table.email": "邮箱",
+ "system.security.user.table.tags": "标签",
+ "system.security.user.action.reset_password": "重置密码",
+ "system.security.user.form.name": "用户名",
+ "system.security.user.form.nickname": "昵称",
+ "system.security.user.form.phone": "电话",
+ "system.security.user.form.email": "邮箱",
+ "system.security.user.form.roles": "角色",
+ "system.security.user.form.tags": "标签",
+ "system.security.user.form.name.required": "请输入名称!",
+ "system.security.user.form.email.invalid": "邮箱格式不正确!",
+ "system.security.user.form.roles.required": "请选择角色!",
+ "system.security.user.create.success": "用户创建成功!",
+ "system.security.user.create.password": "密码:{password}",
+ "system.security.user.create.copy_password": "复制密码",
+ "system.security.role.table.name": "名称",
+ "system.security.role.table.type": "类型",
+ "system.security.role.table.builtin": "内置",
+ "system.security.role.table.description": "描述",
+ "system.security.role.menu.add_platform": "添加平台角色",
+ "system.security.role.menu.add_data": "添加数据角色",
+ "system.security.role.create.success": "角色创建成功!",
+ "system.security.role.create.button.view_list": "返回角色列表",
+ "system.security.role.create.button.continue": "继续创建",
"component.globalHeader.search": "站内搜索",
"component.globalHeader.search.example1": "搜索提示一",
@@ -159,6 +216,29 @@ export default {
"menu.insight": "数据分析",
"menu.insight.dashboard": "数据看板",
"menu.insight.discover": "数据探索",
+ "insight.config.title": "数据探索配置",
+ "insight.config.tab.search": "搜索",
+ "insight.config.search.track_total_hits": "跟踪总命中数",
+ "insight.config.search.timeout": "超时时间",
+ "insight.config.search.field_summary": "字段摘要",
+ "insight.config.search.whether_to_sample": "是否采样",
+ "insight.config.search.sample_records": "采样记录",
+ "insight.config.search.sample_records.all": "全部记录",
+ "insight.config.search.sample_records.manual": "手动设置",
+ "insight.config.search.top_number": "Top 数量",
+ "insight.config.search.time_interval.seconds": "秒",
+ "insight.config.search.time_interval.minutes": "分钟",
+ "insight.config.search.time_interval.hours": "小时",
+ "insight.config.search.time_interval.days": "天",
+ "insight.export.csv": "导出 CSV",
+ "insight.export.excel": "导出 Excel",
+ "insight.export.empty": "当前没有可导出的数据",
+ "insight.discover.empty.no_indices_or_views": "当前集群没有索引或视图",
+ "insight.discover.empty.create_now": "立即创建",
+ "insight.share.copy": "复制分享链接",
+ "insight.share.success": "分享链接已复制",
+ "insight.share.failed": "复制分享链接失败",
+ "insight.button.updating": "更新中",
"menu.alerting": "告警管理",
"menu.alerting.overview": "概览",
@@ -237,6 +317,10 @@ export default {
"menu.resource.agent.new_instance": "探针注册",
"menu.resource.agent.edit_instance": "编辑探针",
+ "menu.data_tools": "数据工具",
+ "menu.data_tools.migration": "数据迁移",
+ "menu.data_tools.comparison": "数据比对",
+
"menu.search": "搜索管理",
"menu.search.overview": "概览",
"menu.search.template": "搜索模板",
@@ -387,6 +471,8 @@ export default {
"app.message.system-tips.no-available-cluster-data-permission":
"当前没有可用集群,请确保您有集群数据权限",
"app.message.confirm.delete": "确定要删除?",
+ "app.message.confirm.delete.multiple": "确定要删除这 {count} 项吗?",
+ "document.confirm.cancel": "确定要取消?",
"app.message.warning.invalid.params": "无效的参数",
"app.message.warning.table.select-row": "请选择表格行",
@@ -399,6 +485,13 @@ export default {
"app.login.sign-in-with": "使用单点登录方式",
"app.login.signup": "注册账户",
"app.login.login": "登录",
+ "app.login.username.required": "请输入用户名!",
+ "app.login.password.required": "请输入密码!",
+ "app.login.mobile.placeholder": "手机号",
+ "app.login.mobile.required": "请输入手机号!",
+ "app.login.mobile.invalid": "手机号格式错误!",
+ "app.login.captcha.placeholder": "验证码",
+ "app.login.captcha.required": "请输入验证码!",
"app.register.register": "注册",
"app.register.get-verification-code": "获取验证码",
"app.register.sing-in": "使用已有账户登录",
@@ -615,6 +708,7 @@ export default {
...alias,
...guide,
...license,
+ ...migration,
...credential,
...overview,
...dashboard,
diff --git a/web/src/locales/zh-CN/command.js b/web/src/locales/zh-CN/command.js
index 764ac138f..460a0f28d 100644
--- a/web/src/locales/zh-CN/command.js
+++ b/web/src/locales/zh-CN/command.js
@@ -1,7 +1,11 @@
export default {
"command.table.field.name": "名称",
"command.table.field.tag": "标签",
+ "command.table.field.creator": "创建人",
+ "command.table.field.created": "创建时间",
"command.table.field.content": "内容",
+ "command.table.field.summary": "摘要",
+ "command.table.summary.requests": "{count} 个请求",
"command.manage.edit.title": "常用命令",
"command.btn.newtag": "新建标签",
"command.message.invalid.tag": "无效的标签内容",
@@ -9,7 +13,7 @@ export default {
"command.manage.title": "常用命令管理",
"command.manage.description":
"常用命令可以帮助您保存常用的请求,并且在开发工具里面通过 LOAD 命令快速地加载。",
- "console.menu.copy_as_curl": "复制为Curl命令",
+ "console.menu.copy_as_curl": "复制为 cURL 命令",
"console.menu.auto_indent": "自动缩进",
"console.menu.save_as_command": "保存为常用命令",
};
diff --git a/web/src/locales/zh-CN/credential.js b/web/src/locales/zh-CN/credential.js
index 137b64dec..c79f1431e 100644
--- a/web/src/locales/zh-CN/credential.js
+++ b/web/src/locales/zh-CN/credential.js
@@ -18,5 +18,9 @@ export default {
"credential.manage.form.name": "凭据名称",
"credential.manage.form.username": "用户名",
"credential.manage.form.password": "密码",
+ "credential.manage.form.token": "Token",
+ "credential.manage.form.token.required": "请输入 Token!",
+ "credential.manage.form.token.placeholder": "请输入 Token!",
+ "credential.manage.form.token.placeholder.edit": "原始 Token 不会显示",
"credential.manage.form.tags": "标签",
};
diff --git a/web/src/locales/zh-CN/error.js b/web/src/locales/zh-CN/error.js
index 0cc8cc810..c8a4d07c0 100644
--- a/web/src/locales/zh-CN/error.js
+++ b/web/src/locales/zh-CN/error.js
@@ -2,4 +2,7 @@ export default {
"error.split": ",",
"error.unknown": "未知错误,请稍后重试或者联系支持团队!",
"error.request_timeout_error": "请求超时,请稍后重试!",
+ "error.request.connection_refused": "连接服务器失败。",
+ "error.request.connection_refused.tip":
+ "连接服务器失败。请点击上方“服务受限”链接进入开发者工具,检查系统集群状态。",
}
diff --git a/web/src/locales/zh-CN/settings.js b/web/src/locales/zh-CN/settings.js
index b05b08ea5..f12080acc 100644
--- a/web/src/locales/zh-CN/settings.js
+++ b/web/src/locales/zh-CN/settings.js
@@ -1,6 +1,44 @@
export default {
"settings.email.server.empty.label1": "您可以在此添加邮件服务器",
"settings.email.server.empty.label2":
- "告警中心可通指定的邮件服务器向收件人发送通知",
+ "告警中心可通过指定的邮件服务器向收件人发送通知。",
"settings.email.server.empty.button.new": "添加邮件服务器",
+ "settings.email.server.form.name": "名称",
+ "settings.email.server.form.host": "主机",
+ "settings.email.server.form.port": "端口",
+ "settings.email.server.form.tls_min_version": "TLS 最低版本",
+ "settings.email.server.form.tls": "TLS",
+ "settings.email.server.form.enabled": "启用",
+ "settings.email.server.form.recipient": "收件人",
+ "settings.email.server.form.recipient.placeholder": "请输入收件人",
+ "settings.email.server.form.test.button": "发送测试邮件",
+ "settings.email.server.form.validation.name": "请输入名称!",
+ "settings.email.server.form.validation.host": "请输入 SMTP 服务器主机!",
+ "settings.email.server.form.validation.port": "请输入 SMTP 服务器端口!",
+ "settings.email.server.form.validation.recipient": "收件人邮箱格式不正确",
+ "settings.email.server.form.temp_name": "新建配置名称",
+ "settings.email.server.message.test.success": "发送成功",
+ "settings.system.tab.general": "通用设置",
+ "settings.system.tab.email": "邮件服务器",
+ "settings.system.retention.title": "数据保留天数",
+ "settings.system.retention.description":
+ "设置系统托管数据在被 ILM 删除前保留多少天。",
+ "settings.system.retention.help":
+ "默认保留 30 天,默认滚动存储大小为 50 GB。保存后会更新系统托管索引对应的 ILM 保留策略。",
+ "settings.system.retention.unit": "天",
+ "settings.system.retention.size.label": "滚动存储大小",
+ "settings.system.retention.size.unit": "GB",
+ "settings.system.retention.save": "保存",
+ "settings.system.retention.update.success": "数据保留天数已更新",
+ "settings.system.retention.validation.days": "请输入有效的保留天数",
+ "settings.system.retention.validation.max_size":
+ "请输入有效的滚动存储大小,单位为 GB,例如 50",
+ "settings.system.rollup.title": "数据汇聚",
+ "settings.system.rollup.description":
+ "可在系统设置中启用或停止系统集群的数据汇聚任务。",
+ "settings.system.rollup.enabled": "开启",
+ "settings.system.rollup.disabled": "关闭",
+ "settings.system.rollup.help":
+ "关闭数据汇聚会逐个停止现有的数据汇聚任务,并同步关闭集群设置中的 rollup search。",
+ "settings.system.rollup.update.success": "数据汇聚设置已更新",
};
diff --git a/web/src/models/global.js b/web/src/models/global.js
index a7933ad7c..27d2cdad2 100644
--- a/web/src/models/global.js
+++ b/web/src/models/global.js
@@ -14,12 +14,38 @@ import router from "umi/router";
import _ from "lodash";
import { getAuthEnabled, hasAuthority } from "@/utils/authority";
import { formatMessage } from "umi/locale";
+import { getPreferredCluster } from "@/utils/setup";
// import ReactGA from "react-ga";
// ReactGA.initialize("G-L0XH1C4CVP");
const MENU_COLLAPSED_KEY = "search-center:menu:collapsed";
const COUSOLE_VERSION_KEY = "console:version";
+const CLUSTER_STATUS_CACHE_TTL = 60 * 1000;
+const CONSOLE_WELCOME_BANNER_STYLE =
+ "color:#1677ff;font-size:14px;font-weight:700;font-family:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;";
+
+const formatBuildDate = (value) => {
+ if (!value) {
+ return value;
+ }
+
+ const buildDate = new Date(value);
+ if (Number.isNaN(buildDate.getTime())) {
+ return value;
+ }
+
+ return buildDate.toLocaleString(undefined, {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ hour12: false,
+ timeZoneName: "short",
+ });
+};
export default {
namespace: "global",
@@ -30,6 +56,8 @@ export default {
notices: [],
clusterVisible: true,
clusterList: [],
+ clusterStatus: {},
+ clusterStatusFetchedAt: 0,
selectedCluster: {},
selectedClusterID: null,
search: {
@@ -86,97 +114,104 @@ export default {
});
},
*fetchClusterList({ payload }, { call, put, select, take }) {
- let res = yield call(searchClusterConfig, payload);
- if (res.error) {
- message.error(res.error);
- return false;
- }
- res = formatESSearchResult(res);
- let { clusterList, search, selectedClusterID } = yield select(
- (state) => state.global
- );
- let data = res.data
- .filter((item) => item.enabled)
- .map((item) => {
- return {
+ try {
+ let res = yield call(searchClusterConfig, payload);
+
+ if (!res) {
+ message.error("No response from cluster service");
+ return false;
+ }
+
+ if (res.error) {
+ message.error(String("Error: " + (res.error.reason || res.error.message || res.error)));
+ return false;
+ }
+
+ res = formatESSearchResult(res);
+
+ let { clusterList, search, selectedClusterID } = yield select(
+ (state) => state.global
+ );
+
+ let data = res.data
+ .filter((item) => item.enabled)
+ .map((item) => ({
...item,
distribution: item.distribution || "elasticsearch",
cluster_uuid: item.cluster_uuid || "",
- };
- });
+ }));
- if (clusterList.length === 0 && !payload.name) {
- if (data.length === 0 && location.href.indexOf("user/login") === -1) {
- if (getAuthEnabled() && !hasAuthority("system.cluster:all")) {
+ if (clusterList.length === 0 && !payload.name) {
+ if (data.length === 0 && location.href.indexOf("user/login") === -1) {
+ if (getAuthEnabled() && !hasAuthority("system.cluster:all")) {
+ Modal.info({
+ title: formatMessage({ id: "app.message.system-tips" }),
+ content: formatMessage({
+ id:
+ "app.message.system-tips.no-available-cluster-data-permission",
+ }),
+ okText: formatMessage({ id: "form.button.ok" }),
+ });
+ return;
+ }
Modal.info({
title: formatMessage({ id: "app.message.system-tips" }),
content: formatMessage({
id:
- "app.message.system-tips.no-available-cluster-data-permission",
+ "app.message.system-tips.no-available-cluster-redirect-setting",
}),
okText: formatMessage({ id: "form.button.ok" }),
- onOk() {},
+ onOk() {
+ router.push("/resource/cluster");
+ },
});
- return;
}
- Modal.info({
- title: formatMessage({ id: "app.message.system-tips" }),
- content: formatMessage({
- id:
- "app.message.system-tips.no-available-cluster-redirect-setting",
- }),
- okText: formatMessage({ id: "form.button.ok" }),
- onOk() {
- router.push("/resource/cluster");
- },
- });
}
- }
- if (!selectedClusterID) {
- const targetID = extractClusterIDFromURL();
- let idx = data.findIndex((item) => {
- return item.id == targetID;
- });
- // idx = idx > -1 ? idx : 0;
- if (idx == -1) {
- let cstatus = yield put({
- type: "fetchClusterStatus",
- });
- yield take("fetchClusterStatus/@@end");
- let { clusterStatus } = yield select((state) => state.global);
- idx = data.findIndex((item) => {
- return clusterStatus[item.id]?.available;
+
+ if (!selectedClusterID) {
+ const targetID = extractClusterIDFromURL();
+ let nextSelectedCluster = getPreferredCluster(data, {
+ targetClusterID: targetID,
});
- if (idx == -1) {
- idx = 0;
+
+ if (!nextSelectedCluster || (targetID && nextSelectedCluster.id !== targetID)) {
+ yield put({ type: "fetchClusterStatus" });
+ yield take("fetchClusterStatus/@@end");
+ let { clusterStatus } = yield select((state) => state.global);
+ const availableCluster = data.find(
+ (item) => clusterStatus[item.id]?.available
+ );
+ nextSelectedCluster =
+ nextSelectedCluster || availableCluster || data[0];
}
+
+ yield put({
+ type: "saveData",
+ payload: {
+ selectedCluster: nextSelectedCluster,
+ selectedClusterID: nextSelectedCluster?.id,
+ },
+ });
}
+
+ let newClusterList = search.name !== payload.name
+ ? data
+ : clusterList.concat(data);
+
yield put({
type: "saveData",
payload: {
- selectedCluster: data[idx],
- selectedClusterID: (data[idx] || {}).id,
+ clusterList: newClusterList,
+ clusterTotal: res.total,
+ search: { ...search, cluster: payload },
},
});
+
+ return data;
+ } catch (err) {
+ message.error(err.message || "Unknown error occurred while fetching clusters");
+ return false;
}
- let newClusterList = [];
- if (search.name != payload.name) {
- newClusterList = data;
- } else {
- newClusterList = clusterList.concat(data);
- }
- yield put({
- type: "saveData",
- payload: {
- clusterList: newClusterList,
- clusterTotal: res.total,
- search: {
- ...search,
- cluster: payload,
- },
- },
- });
- return data;
},
*reloadClusterList({ payload }, { call, put, select }) {
yield put({
@@ -203,6 +238,17 @@ export default {
if (pathname.startsWith("/exception")) {
return;
}
+
+ const dataToolsNewMatch = pathname.match(
+ /^\/data_tools\/(migration|comparison)\/new(?:\/elasticsearch\/[^/]+\/?)?$/
+ );
+ if (dataToolsNewMatch) {
+ const normalizedPath = `/data_tools/${dataToolsNewMatch[1]}/new`;
+ if (pathname !== normalizedPath) {
+ history.replace(normalizedPath + (search || ""));
+ }
+ return;
+ }
const global = yield select((state) => state.global);
if (pathname && global.selectedClusterID) {
@@ -234,21 +280,38 @@ export default {
if (location.href.indexOf("#/user/login") > -1) {
return false;
}
- let res = yield call(getClusterStatus, payload);
+ const options = payload || {};
+ const { force = false, maxAge = CLUSTER_STATUS_CACHE_TTL } = options;
+ const { clusterStatus, clusterStatusFetchedAt } = yield select(
+ (state) => state.global
+ );
+
+ if (
+ !force &&
+ clusterStatusFetchedAt > 0 &&
+ Date.now() - clusterStatusFetchedAt < maxAge
+ ) {
+ return clusterStatus;
+ }
+
+ let res = yield call(getClusterStatus);
if (!res) {
return false;
}
- const { clusterStatus } = yield select((state) => state.global);
if (res.error) {
console.log(res.error);
return false;
}
+ const nextPayload = {
+ clusterStatusFetchedAt: Date.now(),
+ };
if (!_.isEqual(res, clusterStatus)) {
+ nextPayload.clusterStatus = res;
+ }
+ if (Object.keys(nextPayload).length > 0) {
yield put({
type: "saveData",
- payload: {
- clusterStatus: res,
- },
+ payload: nextPayload,
});
}
return res;
@@ -271,11 +334,13 @@ export default {
}
//please do not delete
- console.log(`Welcome to ${APP_TITLE}!`);
- console.log("version.number:", data?.application?.version?.number);
- console.log("version.build_number:", data?.application?.version?.build_number);
- console.log("version.build_hash:", data?.application?.version?.build_hash);
- console.log("version.build_date:", data?.application?.version?.build_date);
+ console.log(`%cWelcome to ${APP_TITLE}!`, CONSOLE_WELCOME_BANNER_STYLE);
+ console.log("version:", data?.application?.version?.number);
+ console.log("build_number:", data?.application?.version?.build_number);
+ console.log("build_hash:", data?.application?.version?.build_hash);
+ console.log("build_date:",
+ formatBuildDate(data?.application?.version?.build_date)
+ );
} else {
console.log("fetch console info failed, ", data);
return false;
@@ -416,6 +481,8 @@ export default {
"/guide",
"/resource",
"/platform/notification",
+ "/data_tools/migration",
+ "/data_tools/comparison",
];
if (clusterHiddenPath.some((p) => pathname.startsWith(p))) {
clusterVisible = false;
diff --git a/web/src/pages/Account/Settings/BaseView.js b/web/src/pages/Account/Settings/BaseView.js
index f191d3318..04f0b7d48 100644
--- a/web/src/pages/Account/Settings/BaseView.js
+++ b/web/src/pages/Account/Settings/BaseView.js
@@ -12,7 +12,7 @@ const { Option } = Select;
// 头像组件 方便以后独立,增加裁剪之类的功能
const AvatarView = ({ avatar }) => (
-
+
+
+
+
+
{formatMessage({ id: "app.settings.security.password-description" })}
:{passwordStrength.strong}
@@ -94,7 +94,7 @@ class SecurityView extends Component {
render() {
return (
-
+
+
@@ -90,7 +90,7 @@ const tabList = [
const desc1 = (
-
+
曲丽丽
@@ -100,7 +100,7 @@ const desc1 = (
const desc2 = (
-
+
周毛毛
diff --git a/web/src/pages/System/Audit/index.jsx b/web/src/pages/System/Audit/index.jsx
index 18af8c585..a9659f503 100644
--- a/web/src/pages/System/Audit/index.jsx
+++ b/web/src/pages/System/Audit/index.jsx
@@ -160,7 +160,7 @@ export default (props) => {
isRefreshPaused={true}
sortEnable={true}
sideEnable={true}
- sideVisible={true}
+ sideVisible={false}
sidePlacement="left"
rowSelectionExtra={{
getExtra: (props) => [
diff --git a/web/src/pages/System/Cluster/AgentCredentialForm.jsx b/web/src/pages/System/Cluster/AgentCredentialForm.jsx
index 06a2ed73e..80c9b9f9b 100644
--- a/web/src/pages/System/Cluster/AgentCredentialForm.jsx
+++ b/web/src/pages/System/Cluster/AgentCredentialForm.jsx
@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useState } from "react";
-import { Button, Divider, Form, Input, Select, Row, Col } from "antd";
+import { Button, Form, Icon, Input, Select, Tooltip } from "antd";
import { formatMessage } from "umi/locale";
import useFetch from "@/lib/hooks/use_fetch";
import { formatESSearchResult } from "@/lib/elasticsearch/util";
@@ -18,6 +18,13 @@ export default (props) => {
} = props;
useEffect(() => {}, [credentialRequired]);
+ const getInitialAgentCredentialValue = (value) =>
+ value?.agent_credential_id
+ ? value.agent_credential_id
+ : value?.username
+ ? MANUAL_VALUE
+ : undefined;
+
const onTryConnect = async () => {
const values = await props.form.validateFields((errors, values) => {
if (errors?.credential_id) {
@@ -54,9 +61,17 @@ export default (props) => {
tryConnect(values);
};
- const [isManual, setIsManual] = useState();
+ const [selectedCredential, setSelectedCredential] = useState(
+ getInitialAgentCredentialValue(initialValue)
+ );
+ const [isManual, setIsManual] = useState(
+ getInitialAgentCredentialValue(initialValue) === MANUAL_VALUE
+ );
+ const canReadCredential =
+ hasAuthority("system.credential:all") ||
+ hasAuthority("system.credential:read");
- const { loading, error, value, run } = useFetch(
+ const { loading, value, run } = useFetch(
"/credential/_search",
{
queryParams: {
@@ -69,26 +84,80 @@ export default (props) => {
);
const onCredentialChange = (value) => {
- if (value === "manual") {
- setIsManual(true);
- } else {
- setIsManual(false);
- }
+ setSelectedCredential(value);
+ setIsManual(value === MANUAL_VALUE);
};
- const { data, total } = useMemo(() => {
+ const { data } = useMemo(() => {
return formatESSearchResult(value);
}, [value]);
+ const credentialActionsStyle = {
+ display: "flex",
+ alignItems: "center",
+ gap: 12,
+ };
+ const credentialGroupStyle = {
+ display: "flex",
+ alignItems: "stretch",
+ flex: 1,
+ minWidth: 0,
+ };
+ const credentialSelectWrapStyle = {
+ flex: 1,
+ minWidth: 0,
+ };
+ const refreshButtonStyle = {
+ width: 28,
+ minWidth: 28,
+ height: "100%",
+ padding: 0,
+ marginLeft: -1,
+ borderTopLeftRadius: 0,
+ borderBottomLeftRadius: 0,
+ zIndex: 1,
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ };
+ const refreshButtonWrapStyle = {
+ display: "flex",
+ alignItems: "stretch",
+ };
+
+ const credentialOptions = useMemo(() => {
+ const options = data.map((item) => ({
+ id: item.id,
+ name: item.name,
+ }));
+ if (
+ initialValue?.agent_credential_id &&
+ !options.find((item) => item.id === initialValue.agent_credential_id)
+ ) {
+ options.unshift({
+ id: initialValue.agent_credential_id,
+ name: initialValue.agent_credential_id,
+ });
+ }
+ return options;
+ }, [data, initialValue?.agent_credential_id]);
useEffect(() => {
- setIsManual(props.isManual);
- }, [props.isManual]);
+ const nextValue = getInitialAgentCredentialValue(initialValue);
+ setSelectedCredential(nextValue);
+ setIsManual(nextValue === MANUAL_VALUE);
+ }, [initialValue?.agent_credential_id, initialValue?.username]);
+
+ useEffect(() => {
+ if (canReadCredential) {
+ run();
+ }
+ }, [canReadCredential, run]);
useEffect(() => {
- if (hasAuthority('system.credential:all') || hasAuthority('system.credential:read')) {
- run()
+ if (canReadCredential && initialValue?.agent_credential_id) {
+ run();
}
- }, [])
+ }, [canReadCredential, initialValue?.agent_credential_id, run]);
if (!needAuth) {
return null;
@@ -97,44 +166,74 @@ export default (props) => {
return (
<>
+ {formatMessage({
+ id: "cluster.regist.step.connect.label.agent_credential",
+ })}
+
+
+
+
+ }
>
-
-
- {getFieldDecorator("agent_credential_id", {
- initialValue: initialValue?.agent_credential_id
- ? initialValue?.agent_credential_id
- : initialValue?.username
- ? MANUAL_VALUE
- : undefined,
- rules: [
- {
- required: credentialRequired,
- message: formatMessage({
- id: "cluster.regist.form.verify.required.agent_credential",
- }),
- },
- ],
- })(
-
{isManual && (
<>
diff --git a/web/src/pages/System/Cluster/CollectMode.jsx b/web/src/pages/System/Cluster/CollectMode.jsx
index 0b172de84..3062edbbe 100644
--- a/web/src/pages/System/Cluster/CollectMode.jsx
+++ b/web/src/pages/System/Cluster/CollectMode.jsx
@@ -30,7 +30,17 @@ export default (props) => {
content: (
<>
- {formatMessage({id: "cluster.manage.metric_collection_mode.confirm.message"}, { mode: value === "agent" ? "Agent" : "Agentless" })}
+ {formatMessage(
+ { id: "cluster.manage.metric_collection_mode.confirm.message" },
+ {
+ mode: formatMessage({
+ id:
+ value === "agent"
+ ? "cluster.manage.metric_collection_mode.option.agent"
+ : "cluster.manage.metric_collection_mode.option.agentless",
+ }),
+ }
+ )}
{isAgentless && isLargeCluster && (
@@ -51,11 +61,19 @@ export default (props) => {
});
}}
>
-
Agentless
-
Agent
+
+ {formatMessage({
+ id: "cluster.manage.metric_collection_mode.option.agentless",
+ })}
+
+
+ {formatMessage({
+ id: "cluster.manage.metric_collection_mode.option.agent",
+ })}
+
)}
>
);
-};
\ No newline at end of file
+};
diff --git a/web/src/pages/System/Cluster/Form.js b/web/src/pages/System/Cluster/Form.js
index f5dd857b3..715884706 100644
--- a/web/src/pages/System/Cluster/Form.js
+++ b/web/src/pages/System/Cluster/Form.js
@@ -20,7 +20,11 @@ import { formatMessage } from "umi/locale";
import TagEditor from "@/components/infini/TagEditor";
import MonitorConfigsForm from "./MonitorConfigsForm";
import MetadataConfigsForm from "./MetadataConfigsForm";
-import { formatConfigsValues } from "./utils";
+import {
+ formatConfigsValues,
+ getClusterProbePath,
+ getClusterConnectErrorMessageFromResponse,
+} from "./utils";
import CredentialForm from "./CredentialForm";
import AgentCredentialForm from "./AgentCredentialForm";
import { MANUAL_VALUE } from "./steps";
@@ -47,6 +51,7 @@ class ClusterForm extends React.Component {
btnLoading: false,
btnLoadingAgent: false,
submitLoading: false,
+ showProbePath: !!getClusterProbePath(props.clusterConfig?.editValue),
};
}
@@ -99,6 +104,7 @@ class ClusterForm extends React.Component {
isManual,
collectMode,
monitored: editValue?.hasOwnProperty("monitored") ? editValue?.monitored : false,
+ showProbePath: !!getClusterProbePath(editValue),
});
}
});
@@ -158,6 +164,7 @@ class ClusterForm extends React.Component {
name: values.name,
host: values.host,
hosts: values.hosts,
+ probe_path: values.probe_path,
credential_id:
values.credential_id !== MANUAL_VALUE
? values.credential_id
@@ -171,10 +178,12 @@ class ClusterForm extends React.Component {
values.agent_credential_id !== MANUAL_VALUE && isAgentMode
? values.agent_credential_id
: undefined,
- agent_basic_auth: {
- username: values.agent_username,
- password: values.agent_password,
- },
+ agent_basic_auth: isAgentMode
+ ? {
+ username: values.agent_username,
+ password: values.agent_password,
+ }
+ : undefined,
metric_collection_mode: values.metric_collection_mode || 'agentless',
description: values.description,
@@ -273,6 +282,7 @@ class ClusterForm extends React.Component {
hosts: values.hosts,
schema: values.isTLS === true ? "https" : "http",
+ probe_path: values.probe_path,
};
if (type === "agent") {
newVals = {
@@ -310,7 +320,7 @@ class ClusterForm extends React.Component {
type: "clusterConfig/doTryConnect",
payload: newVals,
});
- if (res) {
+ if (res && !res.error) {
message.success(
formatMessage({
id: "app.message.connect.success",
@@ -320,6 +330,13 @@ class ClusterForm extends React.Component {
version: res.version,
});
this.clusterUUID = res.cluster_uuid;
+ } else if (res?.error) {
+ message.error(
+ getClusterConnectErrorMessageFromResponse(
+ res,
+ "cluster.regist.try_connect.failed"
+ )
+ );
}
if (type === "agent") {
this.setState({ btnLoadingAgent: false });
@@ -341,6 +358,17 @@ class ClusterForm extends React.Component {
// validation passed
callback();
};
+ validateProbePathRule = (rule, value, callback) => {
+ if (!value) {
+ callback();
+ return;
+ }
+ if (!String(value).trim().startsWith("/")) {
+ callback(formatMessage({ id: "cluster.regist.form.verify.valid.probe_path" }));
+ return;
+ }
+ callback();
+ };
render() {
const { getFieldDecorator } = this.props.form;
@@ -367,6 +395,15 @@ class ClusterForm extends React.Component {
},
};
const { editValue, editMode } = this.props.clusterConfig;
+ const breadcrumbList = [
+ { title: "home", locale: "menu.home", href: "/" },
+ { title: "resource", locale: "menu.resource" },
+ { title: "cluster", locale: "menu.resource.cluster", href: "/resource/cluster" },
+ {
+ title: "editCluster",
+ locale: editMode === "NEW" ? "menu.resource.registCluster" : "menu.resource.editCluster",
+ },
+ ];
//add host value to hosts field if it's empty
if(editValue.host){
if(!editValue.hosts){
@@ -378,7 +415,7 @@ class ClusterForm extends React.Component {
}
}
return (
-
+
)}
-
+
{getFieldDecorator("isTLS", {
initialValue: editValue?.schema === "https",
valuePropName: "checked",
@@ -491,6 +532,47 @@ class ClusterForm extends React.Component {
/>
)}
+
+
+
+ {this.state.showProbePath ? (
+
+ {getFieldDecorator("probe_path", {
+ initialValue: getClusterProbePath(editValue),
+ normalize: (value) => (value || "").trim(),
+ rules: [
+ {
+ validator: this.validateProbePathRule,
+ },
+ ],
+ })(
+
+ )}
+
+ ) : null}
{
this.state.collectMode === 'agent' && (
-
+ <>
+
+ >
)
}
{
@@ -618,7 +702,11 @@ class ClusterForm extends React.Component {
form={this.props.form}
editValue={editValue}
/>
-
+
{getFieldDecorator("tags", {
initialValue: editValue.tags,
rules: [],
diff --git a/web/src/pages/System/Cluster/Step.js b/web/src/pages/System/Cluster/Step.js
index b134be21a..1e356d269 100644
--- a/web/src/pages/System/Cluster/Step.js
+++ b/web/src/pages/System/Cluster/Step.js
@@ -1,4 +1,4 @@
-import { Form, Steps, Button, message, Spin, Card, Row, Col } from "antd";
+import { Alert, Form, Steps, Button, message, Spin, Card, Row, Col } from "antd";
import { connect } from "dva";
import { useState, useRef, useEffect } from "react";
import { InitialStep, ExtraStep, ResultStep, MANUAL_VALUE } from "./steps";
@@ -6,6 +6,10 @@ import PageHeaderWrapper from "@/components/PageHeaderWrapper";
import styles from "./step.less";
import { formatMessage } from "umi/locale";
import { formatConfigsValues } from "./utils";
+import {
+ getClusterConnectErrorMessageFromError,
+ getClusterConnectErrorMessageFromResponse,
+} from "./utils";
import { Link } from "umi";
import { SearchEngines } from "@/lib/search_engines";
@@ -42,6 +46,7 @@ const ClusterStep = ({ dispatch, history, query }) => {
isLoading: false,
current: 0,
});
+ const [connectError, setConnectError] = useState();
const changeStep = (step) => {
setState((st) => {
return {
@@ -65,6 +70,7 @@ const ClusterStep = ({ dispatch, history, query }) => {
const createFormPromise = (type, formatPayload, callback) => {
return new Promise((resolve, reject) => {
setIsLoading(true);
+ setConnectError(undefined);
formRef.current.validateFields((errors, values) => {
if (errors) {
resolve(false);
@@ -82,12 +88,25 @@ const ClusterStep = ({ dispatch, history, query }) => {
}
resolve(true);
} else {
+ setConnectError(
+ getClusterConnectErrorMessageFromResponse(
+ res,
+ "cluster.regist.try_connect.failed"
+ )
+ );
resolve(false);
setIsLoading(false);
}
})
- .catch((err) => {
+ .catch(async (err) => {
+ setConnectError(
+ await getClusterConnectErrorMessageFromError(
+ err,
+ "cluster.regist.try_connect.failed"
+ )
+ );
setIsLoading(false);
+ resolve(false);
});
});
});
@@ -109,6 +128,7 @@ const ClusterStep = ({ dispatch, history, query }) => {
? values.credential_id
: undefined,
schema: values.isTLS === true ? "https" : "http",
+ probe_path: values.probe_path,
}),
(values, res) => {
setClusterConfig({
@@ -135,37 +155,42 @@ const ClusterStep = ({ dispatch, history, query }) => {
const metadata_configs_new = formatConfigsValues(
values.metadata_configs
);
+ const isAgentMode = values.metric_collection_mode === "agent";
clusterConfig.location.region =
clusterConfig.location.region || "default";
- const newVals = {
- name: values.name,
- version: clusterConfig.version,
- distribution: clusterConfig.distribution,
+ const newVals = {
+ name: values.name,
+ version: clusterConfig.version,
+ distribution: clusterConfig.distribution,
host: clusterConfig.host,
hosts: clusterConfig.hosts,
+ probe_path: clusterConfig.probe_path,
location: clusterConfig.location,
credential_id:
clusterConfig.credential_id !== MANUAL_VALUE
? clusterConfig.credential_id
: undefined,
- basic_auth: {
- username: clusterConfig.username || "",
- password: clusterConfig.password || "",
- },
- agent_credential_id:
- values.agent_credential_id !== MANUAL_VALUE
- ? values.agent_credential_id
+ basic_auth: {
+ username: clusterConfig.username || "",
+ password: clusterConfig.password || "",
+ },
+ agent_credential_id:
+ values.agent_credential_id !== MANUAL_VALUE && isAgentMode
+ ? values.agent_credential_id
+ : undefined,
+ agent_basic_auth: isAgentMode
+ ? {
+ username: values.agent_username,
+ password: values.agent_password,
+ }
: undefined,
- agent_basic_auth: {
- username: values.agent_username,
- password: values.agent_password,
- },
- description: values.description,
- enabled: true,
- monitored: values.monitored,
- monitor_configs: monitor_configs_new,
- metadata_configs: metadata_configs_new,
- discovery: {
+ description: values.description,
+ enabled: true,
+ monitored: values.monitored,
+ metric_collection_mode: values.metric_collection_mode || "agentless",
+ monitor_configs: monitor_configs_new,
+ metadata_configs: metadata_configs_new,
+ discovery: {
enabled: values.discovery.enabled,
},
schema: clusterConfig.isTLS ? "https" : "http",
@@ -241,6 +266,14 @@ const ClusterStep = ({ dispatch, history, query }) => {
))}
{renderContent(current)}
+ {current === 0 && connectError ? (
+
+ ) : null}
- {
+ this.setState({ collectMode: mode, agentCredentialRequired: false }, () => {
+ const monitor_configs = this.props.form.getFieldValue("monitor_configs") || {};
+ if (mode === "agent") {
+ monitor_configs.node_stats = { ...(monitor_configs.node_stats || {}), enabled: false };
+ monitor_configs.index_stats = { ...(monitor_configs.index_stats || {}), enabled: false };
+ } else {
+ monitor_configs.node_stats = { ...(monitor_configs.node_stats || {}), enabled: true };
+ monitor_configs.index_stats = { ...(monitor_configs.index_stats || {}), enabled: true };
+ }
+ this.props.form.setFieldsValue({
+ metric_collection_mode: mode,
+ monitor_configs,
+ });
+ });
}}
- isManual={this.state.isManual}
- isEdit={true}
- tryConnect={this.tryConnect}
- credentialRequired={this.state.agentCredentialRequired}
/>
+ {this.state.collectMode === "agent" ? (
+ <>
+
+ >
+ ) : null}
-
+
{getFieldDecorator("tags", {
initialValue: initialValue?.tags || ["default"],
rules: [],
diff --git a/web/src/pages/System/Cluster/steps/initial_step.js b/web/src/pages/System/Cluster/steps/initial_step.js
index 129b3ea8f..ead6745d0 100644
--- a/web/src/pages/System/Cluster/steps/initial_step.js
+++ b/web/src/pages/System/Cluster/steps/initial_step.js
@@ -1,4 +1,4 @@
-import { Form, Input, Switch, Icon, Select } from "antd";
+import { Form, Input, Switch, Icon, Select, Button } from "antd";
import { formatMessage } from "umi/locale";
import CredentialForm from "../CredentialForm";
import "../Form.scss";
@@ -15,7 +15,8 @@ export class InitialStep extends React.Component {
this.state = {
needAuth: props.initialValue?.isAuth !== undefined,
isManual: props.initialValue?.credential_id === MANUAL_VALUE,
- isPageTLS: isTLS(props.initialValue?.host)
+ isPageTLS: isTLS(props.initialValue?.host),
+ showProbePath: !!props.initialValue?.probe_path,
};
}
handleAuthChange = (val) => {
@@ -50,6 +51,17 @@ export class InitialStep extends React.Component {
// validation passed
callback();
};
+ validateProbePathRule = (rule, value, callback) => {
+ if (!value) {
+ callback();
+ return;
+ }
+ if (!String(value).trim().startsWith("/")) {
+ callback(formatMessage({ id: "cluster.regist.form.verify.valid.probe_path" }));
+ return;
+ }
+ callback();
+ };
render() {
const {
form: { getFieldDecorator },
@@ -123,7 +135,11 @@ export class InitialStep extends React.Component {
],
})()}
-
+
{getFieldDecorator("isTLS", {
initialValue: initialValue?.isTLS || false,
})(
@@ -136,6 +152,47 @@ export class InitialStep extends React.Component {
/>
)}
+
+
+
+ {this.state.showProbePath ? (
+
+ {getFieldDecorator("probe_path", {
+ initialValue: initialValue?.probe_path || "",
+ normalize: (value) => (value || "").trim(),
+ rules: [
+ {
+ validator: this.validateProbePathRule,
+ },
+ ],
+ })(
+
+ )}
+
+ ) : null}
{
- TLS:
+ {formatMessage({
+ id: "cluster.manage.field.tls.label",
+ })}
+ :
{formatMessage({
@@ -57,7 +60,7 @@ export const ResultStep = (props) => {
);
const actions = (
-
+
,
+
+ {formatMessage({
+ id: "system.security.role.create.button.continue",
+ })}
+ ,
+ ]}
+ />
+ ) : (
+
+
+ {getFieldDecorator("name", {
+ initialValue: editValue.name,
+ rules: [
+ {
+ required: true,
+ message: formatMessage({
+ id: "system.role.platform.name.required",
+ }),
+ },
+ ],
+ })()}
+
+
+ {getFieldDecorator("privilege.elasticsearch.cluster.resources", {
initialValue: editValue.privilege?.elasticsearch?.cluster
- ?.permissions || [{ "*": ["*"] }],
+ ?.resources || [{ id: "*", name: "*" }],
rules: [
{
required: true,
- message: "Please select cluster privilege!",
+ message: formatMessage({
+ id: "system.role.data.cluster.required",
+ }),
},
],
- }
- )()}
-
-
- {getFieldDecorator("privilege.elasticsearch.index", {
- initialValue: editValue.privilege?.elasticsearch?.index || [
- { name: ["*"], permissions: ["*"] },
- ],
- rules: [
+ })(
+
+ )}
+
+
+ {getFieldDecorator(
+ "privilege.elasticsearch.cluster.permissions",
{
- required: true,
- message: "Please select index privilege!",
- },
- ],
- })(
-
- )}
-
-
- {getFieldDecorator("description", {
- initialValue: editValue.description,
- rules: [],
- })()}
-
-
-
- Save
-
-
-
-
+ initialValue: editValue.privilege?.elasticsearch?.cluster
+ ?.permissions || [{ "*": ["*"] }],
+ rules: [
+ {
+ required: true,
+ message: formatMessage({
+ id: "system.role.data.cluster_privilege.required",
+ }),
+ },
+ ],
+ }
+ )()}
+
+
+ {getFieldDecorator("privilege.elasticsearch.index", {
+ initialValue: editValue.privilege?.elasticsearch?.index || [
+ { name: ["*"], permissions: ["*"] },
+ ],
+ rules: [
+ {
+ required: true,
+ message: formatMessage({
+ id: "system.role.data.index_privilege.required",
+ }),
+ },
+ ],
+ })(
+
+ )}
+
+
+ {getFieldDecorator("description", {
+ initialValue: editValue.description,
+ rules: [],
+ })()}
+
+
+
+ {formatMessage({ id: "form.button.save" })}
+
+
+
+
+ )}
);
diff --git a/web/src/pages/System/Role/Data/index_privilege_field.jsx b/web/src/pages/System/Role/Data/index_privilege_field.jsx
index b984ff4d2..3916f46ea 100644
--- a/web/src/pages/System/Role/Data/index_privilege_field.jsx
+++ b/web/src/pages/System/Role/Data/index_privilege_field.jsx
@@ -4,6 +4,7 @@ import { Button, Input, Select, Icon } from "antd";
import { useCallback, useEffect, useMemo, useState } from "react";
import { DataRoleFromContext } from "./context";
import { ESPrefix } from "@/services/common";
+import { formatMessage } from "umi/locale";
const InputGroup = Input.Group;
const Option = Select.Option;
@@ -76,12 +77,16 @@ const IndexPrivilegeField = ({ privileges = [], value = [], onChange }) => {
marginTop: 10,
}}
>
- Index
- Privilege
+
+ {formatMessage({ id: "system.role.data.index_privilege.index" })}
+
+
+ {formatMessage({ id: "system.role.data.index_privilege.privilege" })}
+
{indexPrivilegeEl}
- Add
+ {formatMessage({ id: "form.button.add" })}
);
diff --git a/web/src/pages/System/Role/Data/new.jsx b/web/src/pages/System/Role/Data/new.jsx
index e43c97a5a..fd3ee8e1a 100644
--- a/web/src/pages/System/Role/Data/new.jsx
+++ b/web/src/pages/System/Role/Data/new.jsx
@@ -1,12 +1,12 @@
import DataRoleForm from "./form";
import { Form } from "antd";
-import { useCallback } from "react";
+import { useCallback, useState } from "react";
import request from "@/utils/request";
-import { message } from "antd";
import { router } from "umi";
import { formatMessage } from "umi/locale";
export default Form.create({ name: "data_role_form_new" })((props) => {
+ const [createResult, setCreateResult] = useState(null);
const onSaveClick = useCallback(async (values) => {
const saveRes = await request(`/role/elasticsearch`, {
method: "POST",
@@ -15,17 +15,18 @@ export default Form.create({ name: "data_role_form_new" })((props) => {
},
});
if (saveRes && saveRes.result == "created") {
- message.success(
- formatMessage({
- id: "app.message.save.success",
- })
- );
- props.form.resetFields();
+ setCreateResult(saveRes);
}
}, []);
return (
{
+ props.form.resetFields();
+ setCreateResult(null);
+ }}
onSaveClick={onSaveClick}
title="Create Data Role"
/>
diff --git a/web/src/pages/System/Role/Platform/api_permission.jsx b/web/src/pages/System/Role/Platform/api_permission.jsx
index b8c9aa55c..9ebc8afa6 100644
--- a/web/src/pages/System/Role/Platform/api_permission.jsx
+++ b/web/src/pages/System/Role/Platform/api_permission.jsx
@@ -1,6 +1,8 @@
import { Transfer } from "antd";
+import React from "react";
-const ApiPermission = ({ value = [], onChange, permissions = [] }) => {
+const ApiPermission = React.forwardRef(
+ ({ value = [], onChange, permissions = [] }, ref) => {
const filterOption = (inputValue, option) =>
option.description.indexOf(inputValue) > -1;
const dataSource = permissions.map((p) => {
@@ -10,14 +12,17 @@ const ApiPermission = ({ value = [], onChange, permissions = [] }) => {
};
});
return (
- item.title}
- />
+
+ item.title}
+ />
+
);
-};
+ }
+);
export default ApiPermission;
diff --git a/web/src/pages/System/Role/Platform/form.jsx b/web/src/pages/System/Role/Platform/form.jsx
index 817d74e3e..09f3aed33 100644
--- a/web/src/pages/System/Role/Platform/form.jsx
+++ b/web/src/pages/System/Role/Platform/form.jsx
@@ -6,6 +6,7 @@ import {
InputNumber,
Card,
Button,
+ Result,
Row,
Col,
Transfer,
@@ -19,8 +20,9 @@ import TagEditor from "@/components/infini/TagEditor";
import Permission from "./permission";
import ApiPermission from "./api_permission";
import request from "@/utils/request";
-import { menuData } from "./menu";
+import { getMenuData } from "./menu";
import { formatMessage } from "umi/locale";
+import { refreshApplicationSettings } from "@/utils/authority";
const formItemLayout = {
labelCol: {
@@ -47,6 +49,16 @@ const tailFormItemLayout = {
const PlatformRoleForm = (props) => {
const { getFieldDecorator } = props.form;
const [isLoading, setIsLoading] = useState(false);
+ const [menuData, setMenuData] = useState(() => getMenuData());
+ const breadcrumbList = [
+ { title: "home", locale: "menu.home", href: "/" },
+ { title: "system", locale: "menu.system" },
+ { title: "security", locale: "menu.system.security" },
+ {
+ title: props.mode === "edit" ? "edit_role" : "new_role",
+ locale: props.mode === "edit" ? "menu.system.edit_role" : "menu.system.new_role",
+ },
+ ];
const handleSubmit = useCallback(
(e) => {
@@ -70,8 +82,20 @@ const PlatformRoleForm = (props) => {
const editValue = props.value || {};
+ useEffect(() => {
+ let isMounted = true;
+ refreshApplicationSettings().finally(() => {
+ if (isMounted) {
+ setMenuData(getMenuData());
+ }
+ });
+ return () => {
+ isMounted = false;
+ };
+ }, []);
+
return (
-
+
@@ -81,41 +105,90 @@ const PlatformRoleForm = (props) => {
}
>
-
- {getFieldDecorator("name", {
- initialValue: editValue.name,
- rules: [
- {
- required: true,
- message: "Please input name!",
- },
- ],
- })()}
-
-
- {getFieldDecorator("privilege.platform", {
- initialValue: editValue.privilege?.platform || [],
- rules: [
- {
- required: true,
- message: "Please select platform feature privilege!",
- },
- ],
- })()}
-
-
- {getFieldDecorator("description", {
- initialValue: editValue.description,
- rules: [],
- })()}
-
-
-
- Save
-
-
-
+ {props.createResult ? (
+ {
+ props.history.push(
+ `/system/security?_g=${encodeURIComponent(
+ JSON.stringify({ tab: "role" })
+ )}`
+ );
+ }}
+ >
+ {formatMessage({
+ id: "system.security.role.create.button.view_list",
+ })}
+ ,
+
+ {formatMessage({
+ id: "system.security.role.create.button.continue",
+ })}
+ ,
+ ]}
+ />
+ ) : (
+
+ {getFieldDecorator("name", {
+ initialValue: editValue.name,
+ rules: [
+ {
+ required: true,
+ message: formatMessage({
+ id: "system.role.platform.name.required",
+ }),
+ },
+ ],
+ })()}
+
+
+ {getFieldDecorator("privilege.platform", {
+ initialValue: editValue.privilege?.platform || [],
+ rules: [
+ {
+ validator: (rule, value, callback) => {
+ if (Array.isArray(value) && value.length > 0) {
+ callback();
+ return;
+ }
+ callback(
+ formatMessage({
+ id: "system.role.platform.feature_privilege.required",
+ })
+ );
+ },
+ },
+ ],
+ })()}
+
+
+ {getFieldDecorator("description", {
+ initialValue: editValue.description,
+ rules: [],
+ })()}
+
+
+
+ {formatMessage({ id: "form.button.save" })}
+
+
+
+ )}
);
diff --git a/web/src/pages/System/Role/Platform/menu.js b/web/src/pages/System/Role/Platform/menu.js
index 7d0d019f6..dade82b3f 100644
--- a/web/src/pages/System/Role/Platform/menu.js
+++ b/web/src/pages/System/Role/Platform/menu.js
@@ -1,4 +1,6 @@
-export const menuData = [
+import { getEnterpriseTaskManagerEnabled } from "@/utils/authority";
+
+const baseMenuData = [
{ key: "workbench", menuKey: "overview" },
{
key: "cluster",
@@ -91,12 +93,12 @@ export const menuData = [
},
],
},
-
{
key: "system",
children: [
{
key: "system.smtp_server",
+ menuKey: "system.settings",
},
{
key: "system.security",
@@ -110,3 +112,23 @@ export const menuData = [
],
},
];
+
+const enterpriseTaskMenu = {
+ key: "data_tools",
+ children: [
+ {
+ key: "data_tools.migration",
+ },
+ {
+ key: "data_tools.comparison",
+ },
+ ],
+};
+
+export const getMenuData = () => {
+ const menuData = [...baseMenuData];
+ if (getEnterpriseTaskManagerEnabled() === "true") {
+ menuData.splice(menuData.length - 1, 0, enterpriseTaskMenu);
+ }
+ return menuData;
+};
diff --git a/web/src/pages/System/Role/Platform/new.jsx b/web/src/pages/System/Role/Platform/new.jsx
index d9fe70ae4..e029b904b 100644
--- a/web/src/pages/System/Role/Platform/new.jsx
+++ b/web/src/pages/System/Role/Platform/new.jsx
@@ -1,12 +1,12 @@
import PlatformRoleForm from "./form";
import { Form } from "antd";
-import { useCallback } from "react";
+import { useCallback, useState } from "react";
import request from "@/utils/request";
-import { message } from "antd";
import { router } from "umi";
import { formatMessage } from "umi/locale";
export default Form.create({ name: "platform_role_form_new" })((props) => {
+ const [createResult, setCreateResult] = useState(null);
const onSaveClick = useCallback(async (values) => {
const saveRes = await request(`/role/platform`, {
method: "POST",
@@ -15,17 +15,18 @@ export default Form.create({ name: "platform_role_form_new" })((props) => {
},
});
if (saveRes && saveRes.result == "created") {
- message.success(
- formatMessage({
- id: "app.message.save.success",
- })
- );
- props.form.resetFields();
+ setCreateResult(saveRes);
}
}, []);
return (
{
+ props.form.resetFields();
+ setCreateResult(null);
+ }}
onSaveClick={onSaveClick}
title="Create Platform Role"
/>
diff --git a/web/src/pages/System/Role/Platform/permission.jsx b/web/src/pages/System/Role/Platform/permission.jsx
index 62240f732..6dad86364 100644
--- a/web/src/pages/System/Role/Platform/permission.jsx
+++ b/web/src/pages/System/Role/Platform/permission.jsx
@@ -46,9 +46,9 @@ const renderTree = ({ key, children, menuKey }, onValueChange) => {
);
};
-export default ({ data, value, onChange }) => {
+const Permission = React.forwardRef(({ data, value, onChange }, ref) => {
const onValueChange = (pitem) => {
- let permissions = value || [];
+ const permissions = [...(value || [])];
const newTemps = (pitem || "").split(":");
const srcIndex = permissions.findIndex((p) => {
return (
@@ -62,22 +62,26 @@ export default ({ data, value, onChange }) => {
} else {
permissions.push(pitem);
}
- permissions = permissions.filter((p) => !p.endsWith(":none"));
+ const nextPermissions = permissions.filter((p) => !p.endsWith(":none"));
if (typeof onChange == "function") {
- onChange(permissions);
+ onChange(nextPermissions);
}
};
data = data || [];
return (
-
- }>
- {data.map((item) => {
- return renderTree(item, onValueChange);
- })}
-
-
+
+
+ }>
+ {data.map((item) => {
+ return renderTree(item, onValueChange);
+ })}
+
+
+
);
-};
+});
+
+export default Permission;
const PermissionTitle = ({ id, title, onChange, showOptions }) => {
const { value } = React.useContext(PermissionContext);
@@ -112,7 +116,7 @@ const PermissionTitle = ({ id, title, onChange, showOptions }) => {
{enumValues.map((item) => {
diff --git a/web/src/pages/System/Role/index.jsx b/web/src/pages/System/Role/index.jsx
index b9ecfb682..ab0872a9f 100644
--- a/web/src/pages/System/Role/index.jsx
+++ b/web/src/pages/System/Role/index.jsx
@@ -11,6 +11,7 @@ import {
message,
Menu,
Dropdown,
+ Icon,
} from "antd";
import { formatMessage } from "umi/locale";
import useFetch from "@/lib/hooks/use_fetch";
@@ -27,7 +28,13 @@ import moment from "moment";
import { formatter } from "@/lib/format";
import { hasAuthority } from "@/utils/authority";
-const { Search } = Input;
+import SearchInput from "@/components/infini/SearchInput";
+
+const firstColumnIconStyle = {
+ marginRight: 8,
+ color: "#999",
+ fontSize: 12,
+};
const RoleList = (props) => {
const [queryParams, setQueryParams] = React.useState({});
@@ -61,22 +68,28 @@ const RoleList = (props) => {
const columns = useMemo(
() => [
{
- title: "Name",
+ title: formatMessage({ id: "system.security.role.table.name" }),
dataIndex: "name",
+ render: (text) => (
+
+
+ {text}
+
+ ),
},
{
- title: "Type",
+ title: formatMessage({ id: "system.security.role.table.type" }),
dataIndex: "type",
},
{
- title: "Builtin",
+ title: formatMessage({ id: "system.security.role.table.builtin" }),
dataIndex: "builtin",
render: (val) => {
return val === true ? "true" : "false";
},
},
{
- title: "Description",
+ title: formatMessage({ id: "system.security.role.table.description" }),
dataIndex: "description",
},
{
@@ -93,14 +106,16 @@ const RoleList = (props) => {
: `/system/security/role/data/edit/${record.id}`
}
>
- Edit
+ {formatMessage({ id: "form.button.edit" })}
onDeleteClick(record.id)}
>
- Delete
+ {formatMessage({ id: "form.button.delete" })}
>
) : null}
@@ -160,8 +175,12 @@ const RoleList = (props) => {
};
const menu = (
);
@@ -176,10 +195,12 @@ const RoleList = (props) => {
}}
>
-
{
onSearchClick(value);
}}
@@ -229,7 +250,10 @@ const RoleList = (props) => {
total: total?.value || total,
showSizeChanger: true,
showTotal: (total, range) =>
- `${range[0]}-${range[1]} of ${total} items`,
+ formatMessage(
+ { id: "system.security.pagination.total" },
+ { start: range[0], end: range[1], total }
+ ),
}}
columns={columns}
onChange={handleTableChange}
diff --git a/web/src/pages/System/Security/index.jsx b/web/src/pages/System/Security/index.jsx
index a0cb02baa..8e9a45f4a 100644
--- a/web/src/pages/System/Security/index.jsx
+++ b/web/src/pages/System/Security/index.jsx
@@ -37,10 +37,16 @@ const Security = (props) => {
setParam({ ...param, tab: key });
}}
>
- User} key="user">
+ {formatMessage({ id: "system.security.tab.user" })}}
+ key="user"
+ >
- Role} key="role">
+ {formatMessage({ id: "system.security.tab.role" })}}
+ key="role"
+ >
diff --git a/web/src/pages/System/Settings/index.jsx b/web/src/pages/System/Settings/index.jsx
new file mode 100644
index 000000000..e63890167
--- /dev/null
+++ b/web/src/pages/System/Settings/index.jsx
@@ -0,0 +1,352 @@
+import PageHeaderWrapper from "@/components/PageHeaderWrapper";
+import {
+ Alert,
+ Button,
+ Card,
+ Empty,
+ InputNumber,
+ Spin,
+ Switch,
+ Tabs,
+ message,
+} from "antd";
+import { formatMessage } from "umi/locale";
+import router from "umi/router";
+import { useEffect, useMemo, useState } from "react";
+import request from "@/utils/request";
+import ServerList from "../Email/Server";
+import { hasAuthority, refreshApplicationSettings } from "@/utils/authority";
+
+const { TabPane } = Tabs;
+
+const SystemSettings = (props) => {
+ const query = new URLSearchParams(props.location?.search || "");
+ const pathDefaultTab = props.location?.pathname?.includes("/email_server")
+ ? "email"
+ : "general";
+ const canReadCluster =
+ hasAuthority("system.cluster:all") || hasAuthority("system.cluster:read");
+ const canWriteCluster = hasAuthority("system.cluster:all");
+ const canReadEmail =
+ hasAuthority("system.smtp_server:all") ||
+ hasAuthority("system.smtp_server:read");
+ const tabs = useMemo(() => {
+ const nextTabs = [];
+ if (canReadCluster) {
+ nextTabs.push("general");
+ }
+ if (canReadEmail) {
+ nextTabs.push("email");
+ }
+ return nextTabs;
+ }, [canReadCluster, canReadEmail]);
+ const [activeTab, setActiveTab] = useState(
+ query.get("tab") || pathDefaultTab || tabs[0] || "general"
+ );
+ const defaultRetentionMaxSize = 50;
+ const [retentionDays, setRetentionDays] = useState(30);
+ const [retentionDraftDays, setRetentionDraftDays] = useState(30);
+ const [retentionMaxSize, setRetentionMaxSize] = useState(defaultRetentionMaxSize);
+ const [retentionDraftMaxSize, setRetentionDraftMaxSize] = useState(
+ defaultRetentionMaxSize
+ );
+ const [retentionLoading, setRetentionLoading] = useState(false);
+ const [rollupEnabled, setRollupEnabled] = useState(false);
+ const [rollupLoading, setRollupLoading] = useState(false);
+
+ const normalizeRetentionSize = (value) =>
+ `${value || ""}`.replace(/\s+/g, "").toLowerCase();
+
+ const parseRetentionSizeToGb = (value) => {
+ const normalizedValue = normalizeRetentionSize(value);
+ const matches = normalizedValue.match(/^(\d+)(b|kb|mb|gb|tb|k|m|g|t)$/i);
+ if (!matches) {
+ return defaultRetentionMaxSize;
+ }
+ const size = Number(matches[1]);
+ const unit = matches[2].toLowerCase();
+ const multiplier = {
+ b: 1 / 1024 / 1024 / 1024,
+ kb: 1 / 1024 / 1024,
+ k: 1 / 1024 / 1024,
+ mb: 1 / 1024,
+ m: 1 / 1024,
+ gb: 1,
+ g: 1,
+ tb: 1024,
+ t: 1024,
+ }[unit];
+ if (!multiplier) {
+ return defaultRetentionMaxSize;
+ }
+ return Math.max(1, Math.ceil(size * multiplier));
+ };
+
+ const isValidRetentionSize = (value) =>
+ Number.isInteger(value) && value > 0;
+
+ useEffect(() => {
+ if (tabs.length > 0 && !tabs.includes(activeTab)) {
+ setActiveTab(tabs[0]);
+ }
+ }, [tabs, activeTab]);
+
+ useEffect(() => {
+ if (!canReadCluster) {
+ return;
+ }
+ const fetchRetentionSetting = async () => {
+ setRetentionLoading(true);
+ const res = await request("/setting/system/retention", {
+ method: "GET",
+ });
+ if (!res?.error && Number.isInteger(res.days) && res.days > 0) {
+ setRetentionDays(res.days);
+ setRetentionDraftDays(res.days);
+ if (res.max_size) {
+ const normalizedSize = parseRetentionSizeToGb(res.max_size);
+ setRetentionMaxSize(normalizedSize);
+ setRetentionDraftMaxSize(normalizedSize);
+ }
+ }
+ setRetentionLoading(false);
+ };
+ const fetchRollupSetting = async () => {
+ setRollupLoading(true);
+ const res = await request("/setting/system/rollup", {
+ method: "GET",
+ });
+ if (!res?.error) {
+ setRollupEnabled(!!res.enabled);
+ }
+ setRollupLoading(false);
+ };
+ fetchRetentionSetting();
+ fetchRollupSetting();
+ }, [canReadCluster]);
+
+ const onTabChange = (key) => {
+ setActiveTab(key);
+ router.replace(`/system/settings?tab=${key}`);
+ };
+
+ const onRollupToggle = async (checked) => {
+ const previousValue = rollupEnabled;
+ setRollupEnabled(checked);
+ setRollupLoading(true);
+ const res = await request("/setting/system/rollup", {
+ method: "PUT",
+ body: {
+ enabled: checked,
+ },
+ });
+ if (res?.error) {
+ setRollupEnabled(previousValue);
+ setRollupLoading(false);
+ return;
+ }
+ await refreshApplicationSettings(true);
+ setRollupLoading(false);
+ message.success(
+ formatMessage({ id: "settings.system.rollup.update.success" })
+ );
+ };
+
+ const onRetentionSave = async () => {
+ if (!Number.isInteger(retentionDraftDays) || retentionDraftDays <= 0) {
+ message.warning(
+ formatMessage({ id: "settings.system.retention.validation.days" })
+ );
+ return;
+ }
+ if (!isValidRetentionSize(retentionDraftMaxSize)) {
+ message.warning(
+ formatMessage({ id: "settings.system.retention.validation.max_size" })
+ );
+ return;
+ }
+ setRetentionLoading(true);
+ const normalizedMaxSize = normalizeRetentionSize(`${retentionDraftMaxSize}gb`);
+ const res = await request("/setting/system/retention", {
+ method: "PUT",
+ body: {
+ days: retentionDraftDays,
+ max_size: normalizedMaxSize,
+ },
+ });
+ if (res?.error) {
+ setRetentionLoading(false);
+ return;
+ }
+ setRetentionDays(res.days);
+ setRetentionDraftDays(res.days);
+ setRetentionMaxSize(parseRetentionSizeToGb(res.max_size || normalizedMaxSize));
+ setRetentionDraftMaxSize(
+ parseRetentionSizeToGb(res.max_size || normalizedMaxSize)
+ );
+ setRetentionLoading(false);
+ message.success(
+ formatMessage({ id: "settings.system.retention.update.success" })
+ );
+ };
+
+ const renderRetentionSettings = () => {
+ if (!canReadCluster) {
+ return null;
+ }
+ return (
+
+
+
+
+
+ {formatMessage({ id: "settings.system.retention.title" })}
+
+
+ {formatMessage({
+ id: "settings.system.retention.description",
+ })}
+
+
+
+ {
+ setRetentionDraftDays(
+ Number.isInteger(value) ? value : retentionDraftDays
+ );
+ }}
+ />
+ {formatMessage({ id: "settings.system.retention.unit" })}
+
+ {formatMessage({ id: "settings.system.retention.size.label" })}
+
+ {
+ setRetentionDraftMaxSize(
+ Number.isInteger(value) ? value : retentionDraftMaxSize
+ );
+ }}
+ />
+ {formatMessage({ id: "settings.system.retention.size.unit" })}
+
+ {formatMessage({ id: "settings.system.retention.save" })}
+
+
+
+ {formatMessage({ id: "settings.system.retention.help" })} }
+ />
+
+
+ );
+ };
+
+ const renderRollupSettings = () => {
+ if (!canReadCluster) {
+ return ;
+ }
+ return (
+
+
+
+
+
+ {formatMessage({ id: "settings.system.rollup.title" })}
+
+
+ {formatMessage({ id: "settings.system.rollup.description" })}
+
+
+
+
+
+
+
+ );
+ };
+
+ const renderGeneralSettings = () => {
+ if (!canReadCluster) {
+ return ;
+ }
+ return (
+ <>
+ {renderRetentionSettings()}
+ {renderRollupSettings()}
+ >
+ );
+ };
+
+ return (
+
+
+
+ {canReadCluster ? (
+
+ {renderGeneralSettings()}
+
+ ) : null}
+ {canReadEmail ? (
+
+
+
+ ) : null}
+
+
+
+ );
+};
+
+export default SystemSettings;
diff --git a/web/src/pages/System/User/form.jsx b/web/src/pages/System/User/form.jsx
index 960dedc89..7590f862c 100644
--- a/web/src/pages/System/User/form.jsx
+++ b/web/src/pages/System/User/form.jsx
@@ -9,6 +9,7 @@ import {
Row,
Col,
Result,
+ message,
} from "antd";
import PageHeaderWrapper from "@/components/PageHeaderWrapper";
// import "./form.scss";
@@ -45,8 +46,18 @@ const tailFormItemLayout = {
const UserForm = (props) => {
const { getFieldDecorator } = props.form;
const [isLoading, setIsLoading] = useState(false);
+ const breadcrumbList = [
+ { title: "home", locale: "menu.home", href: "/" },
+ { title: "system", locale: "menu.system" },
+ { title: "security", locale: "menu.system.security" },
+ {
+ title: props.mode === "edit" ? "edit_user" : "new_user",
+ locale:
+ props.mode === "edit" ? "menu.system.edit_user" : "menu.system.new_user",
+ },
+ ];
- const { value: roleRes } = useFetch(
+ const { loading: rolesLoading, error: rolesError, value: roleRes } = useFetch(
`/role/_search`,
{ queryParams: { size: 10000 } },
[]
@@ -67,24 +78,35 @@ const UserForm = (props) => {
e.preventDefault();
setIsLoading(true);
props.form.validateFields(async (err, values) => {
- if (err) {
- return false;
- }
- if (typeof props.onSaveClick == "function") {
- let newVals = {
- ...values,
- };
- if (newVals.roles) {
- newVals.roles = newVals.roles.map((rid) => {
- return roles.find((role) => role.id == rid);
- });
+ try {
+ if (err || rolesLoading || rolesError) {
+ return;
+ }
+ if (typeof props.onSaveClick == "function") {
+ let newVals = {
+ ...values,
+ };
+ if (newVals.roles) {
+ newVals.roles = newVals.roles
+ .map((rid) => {
+ return roles.find((role) => role.id == rid);
+ })
+ .filter(Boolean);
+ }
+ await props.onSaveClick(newVals);
}
- await props.onSaveClick(newVals);
+ } catch (error) {
+ message.error(
+ formatMessage({
+ id: "app.message.save.failed",
+ })
+ );
+ } finally {
setIsLoading(false);
}
});
},
- [props.form, roles]
+ [props.form, props.onSaveClick, roles, rolesError, rolesLoading]
);
const editValue = props.value || {};
if (editValue.roles && editValue.roles.length && editValue.roles[0]?.name) {
@@ -95,7 +117,7 @@ const UserForm = (props) => {
};
return (
-
+
@@ -107,18 +129,24 @@ const UserForm = (props) => {
>
{!props.createResult ? (
+
{getFieldDecorator("name", {
initialValue: editValue.name,
rules: [
{
required: true,
- message: "Please input name!",
+ message: formatMessage({
+ id: "system.security.user.form.name.required",
+ }),
},
],
})()}
-
+
{getFieldDecorator("nick_name", {
initialValue: editValue.nick_name,
rules: [
@@ -136,7 +164,9 @@ const UserForm = (props) => {
rules: [],
})()}
*/}
-
+
{getFieldDecorator("phone", {
initialValue: editValue.phone,
rules: [
@@ -152,7 +182,9 @@ const UserForm = (props) => {
],
})()}
-
+
{getFieldDecorator("email", {
initialValue: editValue.email,
rules: [
@@ -162,24 +194,31 @@ const UserForm = (props) => {
// },
{
type: "email",
- message: "The input is not valid email!",
+ message: formatMessage({
+ id: "system.security.user.form.email.invalid",
+ }),
},
],
})()}
-
+
{getFieldDecorator("roles", {
initialValue: editValue.roles,
rules: [
{
required: true,
- message: "Please select roles!",
+ message: formatMessage({
+ id: "system.security.user.form.roles.required",
+ }),
},
],
})(
option.props.children
.toLowerCase()
@@ -196,15 +235,22 @@ const UserForm = (props) => {
)}
-
+
{getFieldDecorator("tags", {
initialValue: editValue.tags || [],
rules: [],
})()}
-
- Save
+
+ {formatMessage({ id: "form.button.save" })}
@@ -221,13 +267,16 @@ export default UserForm;
const CreateResult = ({ password }) => (
+
{(copy) => (
- Copy Password
+ {formatMessage({ id: "system.security.user.create.copy_password" })}
)}
,
diff --git a/web/src/pages/System/User/index.jsx b/web/src/pages/System/User/index.jsx
index edfe1bb9c..0f726bd9c 100644
--- a/web/src/pages/System/User/index.jsx
+++ b/web/src/pages/System/User/index.jsx
@@ -9,6 +9,7 @@ import {
Button,
Input,
message,
+ Icon,
} from "antd";
import { formatMessage } from "umi/locale";
import useFetch from "@/lib/hooks/use_fetch";
@@ -25,7 +26,13 @@ import moment from "moment";
import { formatter } from "@/lib/format";
import { hasAuthority } from "@/utils/authority";
-const { Search } = Input;
+import SearchInput from "@/components/infini/SearchInput";
+
+const firstColumnIconStyle = {
+ marginRight: 8,
+ color: "#999",
+ fontSize: 12,
+};
const UserList = (props) => {
const [queryParams, setQueryParams] = React.useState({});
@@ -61,30 +68,36 @@ const UserList = (props) => {
const columns = useMemo(
() => [
{
- title: "Name",
+ title: formatMessage({ id: "system.security.user.table.name" }),
dataIndex: "name",
+ render: (text) => (
+
+
+ {text}
+
+ ),
},
{
- title: "Nickname",
+ title: formatMessage({ id: "system.security.user.table.nickname" }),
dataIndex: "nick_name",
},
{
- title: "Roles",
+ title: formatMessage({ id: "system.security.user.table.roles" }),
dataIndex: "roles",
render: (val) => {
return (val || []).map((role) => role.name).join(",");
},
},
{
- title: "Phone",
+ title: formatMessage({ id: "system.security.user.table.phone" }),
dataIndex: "phone",
},
{
- title: "Email",
+ title: formatMessage({ id: "system.security.user.table.email" }),
dataIndex: "email",
},
{
- title: "Tags",
+ title: formatMessage({ id: "system.security.user.table.tags" }),
dataIndex: "tags",
render: (text) => {
return text;
@@ -92,6 +105,7 @@ const UserList = (props) => {
},
{
title: formatMessage({ id: "table.field.actions" }),
+ width: 180,
render: (text, record) => (
{hasAuthority("system.security:all") ? (
@@ -100,21 +114,25 @@ const UserList = (props) => {
key="permission"
to={`/system/security/user/edit/${record.id}`}
>
- Edit
+ {formatMessage({ id: "form.button.edit" })}
onDeleteClick(record.id)}
>
- Delete
+ {formatMessage({ id: "form.button.delete" })}
- Reset Password
+ {formatMessage({
+ id: "system.security.user.action.reset_password",
+ })}
>
) : null}
@@ -174,10 +192,12 @@ const UserList = (props) => {
}}
>
-
{
onSearchClick(value);
}}
@@ -224,7 +244,10 @@ const UserList = (props) => {
total: total?.value || total,
showSizeChanger: true,
showTotal: (total, range) =>
- `${range[0]}-${range[1]} of ${total} items`,
+ formatMessage(
+ { id: "system.security.pagination.total" },
+ { start: range[0], end: range[1], total }
+ ),
}}
columns={columns}
onChange={handleTableChange}
diff --git a/web/src/pages/System/User/new.jsx b/web/src/pages/System/User/new.jsx
index 606e1e714..d02f8a041 100644
--- a/web/src/pages/System/User/new.jsx
+++ b/web/src/pages/System/User/new.jsx
@@ -22,12 +22,21 @@ export default Form.create({ name: "user_form_new" })((props) => {
})
);
setCreateResult(saveRes);
+ return;
+ }
+ if (saveRes && !saveRes.error) {
+ message.error(
+ formatMessage({
+ id: "app.message.save.failed",
+ })
+ );
}
}, []);
return (
{
const { form, match } = props;
const { getFieldDecorator } = form;
const [passwordHelp, setPasswordHelp] = useState(null);
+ const breadcrumbList = [
+ { title: "home", locale: "menu.home", href: "/" },
+ { title: "system", locale: "menu.system" },
+ { title: "security", locale: "menu.system.security" },
+ { title: "reset_password", locale: "menu.system.reset_password" },
+ ];
const onCancelClick = () => {
props.history.go(-1);
@@ -118,7 +124,7 @@ export default Form.create({ name: "user_form_new" })((props) => {
});
};
return (
-
+
diff --git a/web/src/pages/User/Login.js b/web/src/pages/User/Login.js
index 48f89c6f3..d4c0f16dd 100644
--- a/web/src/pages/User/Login.js
+++ b/web/src/pages/User/Login.js
@@ -3,7 +3,10 @@ import { connect } from "dva";
import { formatMessage, FormattedMessage } from "umi/locale";
import Link from "umi/link";
import { Checkbox, Alert, Icon,Button } from "antd";
+import router from "umi/router";
import Login from "@/components/Login";
+import { getHealth } from "@/services/system";
+import { getSetupRequired, setSetupRequired } from "@/utils/setup";
import styles from "./Login.less";
import "./LoginPage.scss";
@@ -19,6 +22,27 @@ class LoginPage extends Component {
autoLogin: true,
};
+ componentDidMount() {
+ if (getSetupRequired() === "true") {
+ router.replace("/guide/initialization");
+ return;
+ }
+
+ this.syncSetupState();
+ }
+
+ syncSetupState = async () => {
+ try {
+ const res = await getHealth();
+ setSetupRequired(`${!!res?.setup_required}`);
+ if (res?.setup_required) {
+ router.replace("/guide/initialization");
+ }
+ } catch (error) {
+ console.log(error);
+ }
+ };
+
onTabChange = (type) => {
this.setState({ type });
};
diff --git a/web/src/pages/User/SSOSuccess.js b/web/src/pages/User/SSOSuccess.js
index ebeb09e13..593c4299f 100644
--- a/web/src/pages/User/SSOSuccess.js
+++ b/web/src/pages/User/SSOSuccess.js
@@ -6,6 +6,10 @@ import Result from "@/components/Result";
import { router } from "umi";
import { setAuthority } from "@/utils/authority";
import { reloadAuthorized } from "@/utils/Authorized";
+import {
+ clearStoredLoginResponse,
+ storeLoginResponse,
+} from "@/utils/auth_session";
const actions = (
@@ -20,9 +24,9 @@ const actions = (
const SSOSuccess = ({ location }) => {
useEffect(() => {
if (location?.query?.payload) {
- localStorage.setItem("login-response", location.query.payload);
+ const loginResponse = storeLoginResponse(location.query.payload);
try {
- const query = JSON.parse(location.query.payload);
+ const query = loginResponse || JSON.parse(location.query.payload);
if (query?.privilege) {
setAuthority(query.privilege);
reloadAuthorized();
@@ -33,7 +37,7 @@ const SSOSuccess = ({ location }) => {
localStorage.setItem("infini-console-authority", "");
}
} else {
- localStorage.setItem("login-response", "");
+ clearStoredLoginResponse();
}
setTimeout(() => {
router.push("/");
diff --git a/web/src/utils/auth_session.js b/web/src/utils/auth_session.js
new file mode 100644
index 000000000..2e036ea60
--- /dev/null
+++ b/web/src/utils/auth_session.js
@@ -0,0 +1,432 @@
+const LOGIN_RESPONSE_KEY = "login-response";
+const LOGIN_ACTIVITY_KEY = "login-last-activity-at";
+const REFRESH_ENDPOINT = "/account/refresh";
+const MIN_REFRESH_THRESHOLD_MS = 5 * 60 * 1000;
+const ACTIVITY_IDLE_TIMEOUT_MS = 15 * 60 * 1000;
+const ACTIVITY_PERSIST_INTERVAL_MS = 30 * 1000;
+const REFRESH_RETRY_INTERVAL_MS = 30 * 1000;
+
+let refreshPromise = null;
+let refreshTimer = null;
+let managerStarted = false;
+let lastRecordedActivityAt = 0;
+let lastRefreshAttemptAt = 0;
+
+function canUseWindow() {
+ return typeof window !== "undefined";
+}
+
+function parseJSON(value) {
+ if (!value) {
+ return null;
+ }
+ try {
+ return JSON.parse(value);
+ } catch (e) {
+ return null;
+ }
+}
+
+function decodeJwtPayload(token) {
+ if (!token || !canUseWindow()) {
+ return null;
+ }
+ const segments = token.split(".");
+ if (segments.length < 2) {
+ return null;
+ }
+ try {
+ const base64 = segments[1].replace(/-/g, "+").replace(/_/g, "/");
+ const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
+ return JSON.parse(window.atob(padded));
+ } catch (e) {
+ return null;
+ }
+}
+
+function toUnixMs(value) {
+ const numericValue = Number(value);
+ if (!Number.isFinite(numericValue) || numericValue <= 0) {
+ return 0;
+ }
+ return numericValue > 1e12 ? numericValue : numericValue * 1000;
+}
+
+function normalizeLoginResponse(response, now = Date.now()) {
+ if (!response || typeof response !== "object") {
+ return null;
+ }
+ const normalized = {
+ ...response,
+ };
+ let expiresAt = toUnixMs(normalized.expires_at);
+
+ if (!expiresAt && normalized.access_token) {
+ const payload = decodeJwtPayload(normalized.access_token);
+ expiresAt = toUnixMs(payload?.exp);
+ }
+
+ if (!expiresAt) {
+ const expireInSeconds = Number(normalized.expire_in);
+ if (Number.isFinite(expireInSeconds) && expireInSeconds > 0) {
+ expiresAt = now + expireInSeconds * 1000;
+ }
+ }
+
+ if (expiresAt) {
+ normalized.expires_at = expiresAt;
+ }
+
+ return normalized;
+}
+
+function getStoredActivityAt() {
+ if (!canUseWindow()) {
+ return 0;
+ }
+ return Number(window.localStorage.getItem(LOGIN_ACTIVITY_KEY) || 0);
+}
+
+function setStoredActivityAt(activityAt) {
+ if (!canUseWindow()) {
+ return;
+ }
+ window.localStorage.setItem(LOGIN_ACTIVITY_KEY, `${activityAt}`);
+}
+
+function clearRefreshTimer() {
+ if (refreshTimer) {
+ window.clearTimeout(refreshTimer);
+ refreshTimer = null;
+ }
+}
+
+function getRefreshThresholdMs(loginResponse) {
+ const tokenLifetimeMs = Number(loginResponse?.expire_in || 0) * 1000;
+ if (tokenLifetimeMs > 0) {
+ return Math.max(MIN_REFRESH_THRESHOLD_MS, tokenLifetimeMs / 2);
+ }
+ return MIN_REFRESH_THRESHOLD_MS;
+}
+
+function getNormalizedRequestPath(requestUrl) {
+ if (!canUseWindow()) {
+ return requestUrl;
+ }
+
+ const resolvedUrl = new URL(requestUrl, window.location.origin);
+ let pathname = resolvedUrl.pathname;
+ const basePath =
+ window.routerBase && window.routerBase !== "/"
+ ? window.routerBase.replace(/\/$/, "")
+ : "";
+
+ if (basePath && pathname.startsWith(`${basePath}/`)) {
+ pathname = pathname.slice(basePath.length);
+ } else if (basePath && pathname === basePath) {
+ pathname = "/";
+ }
+
+ return pathname;
+}
+
+function buildUrlWithBasePath(relativeUrl) {
+ if (!canUseWindow() || /^(https?:)?\/\//.test(relativeUrl)) {
+ return relativeUrl;
+ }
+ const basePath =
+ window.routerBase && window.routerBase !== "/"
+ ? window.routerBase.replace(/\/$/, "")
+ : "";
+ const cleanUrl = relativeUrl.replace(/^\//, "");
+ return `${basePath}/${cleanUrl}`;
+}
+
+function isRefreshPath(requestUrl) {
+ const path = getNormalizedRequestPath(requestUrl);
+ return (
+ path === REFRESH_ENDPOINT ||
+ path === "/account/login" ||
+ path === "/account/login/challenge" ||
+ path === "/account/logout"
+ );
+}
+
+function hasRecentUserActivity(now = Date.now()) {
+ const lastActivityAt = getStoredActivityAt();
+ return lastActivityAt > 0 && now-lastActivityAt <= ACTIVITY_IDLE_TIMEOUT_MS;
+}
+
+function dispatchLogout() {
+ if (
+ !canUseWindow() ||
+ window.location.href.indexOf("user/login") !== -1 ||
+ !window.g_app?._store
+ ) {
+ return;
+ }
+ window.g_app._store.dispatch({
+ type: "login/logout",
+ payload: {
+ skipServerLogout: true,
+ },
+ });
+}
+
+function scheduleNextRefresh() {
+ if (!canUseWindow()) {
+ return;
+ }
+ clearRefreshTimer();
+ const loginResponse = getStoredLoginResponse();
+ const expiresAt = Number(loginResponse?.expires_at || 0);
+ if (!loginResponse?.access_token || !expiresAt) {
+ return;
+ }
+
+ const now = Date.now();
+ const remainingMs = expiresAt - now;
+ if (remainingMs <= 0) {
+ return;
+ }
+ const refreshThresholdMs = getRefreshThresholdMs(loginResponse);
+
+ const delayMs =
+ remainingMs > refreshThresholdMs
+ ? remainingMs - refreshThresholdMs
+ : Math.min(REFRESH_RETRY_INTERVAL_MS, remainingMs);
+
+ refreshTimer = window.setTimeout(() => {
+ refreshTimer = null;
+ if (!hasRecentUserActivity()) {
+ scheduleNextRefresh();
+ return;
+ }
+ refreshAccessToken().catch(() => {
+ scheduleNextRefresh();
+ });
+ }, Math.max(1000, delayMs));
+}
+
+async function requestTokenRefresh(currentToken) {
+ if (!canUseWindow() || !currentToken) {
+ return null;
+ }
+
+ const refreshUrl = buildUrlWithBasePath(REFRESH_ENDPOINT);
+ if (new URL(refreshUrl, window.location.origin).protocol !== "https:") {
+ return null;
+ }
+
+ const response = await window.fetch(refreshUrl, {
+ method: "POST",
+ credentials: "include",
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json; charset=utf-8",
+ Authorization: `Bearer ${currentToken}`,
+ },
+ });
+
+ let payload = null;
+ try {
+ payload = await response.json();
+ } catch (e) {
+ payload = null;
+ }
+
+ if (!response.ok) {
+ const error = new Error(
+ payload?.error?.reason || response.statusText || "failed to refresh token"
+ );
+ error.status = response.status;
+ throw error;
+ }
+
+ return payload;
+}
+
+export function getStoredLoginResponse() {
+ if (!canUseWindow()) {
+ return null;
+ }
+ const parsed = parseJSON(window.localStorage.getItem(LOGIN_RESPONSE_KEY));
+ const normalized = normalizeLoginResponse(parsed);
+ if (!normalized?.access_token) {
+ return null;
+ }
+
+ if (parsed?.expires_at !== normalized.expires_at) {
+ window.localStorage.setItem(LOGIN_RESPONSE_KEY, JSON.stringify(normalized));
+ }
+
+ return normalized;
+}
+
+export function storeLoginResponse(response, { recordActivity = true } = {}) {
+ if (!canUseWindow()) {
+ return null;
+ }
+ const parsed =
+ typeof response === "string"
+ ? parseJSON(response)
+ : response;
+
+ if (!parsed?.access_token) {
+ clearStoredLoginResponse();
+ return null;
+ }
+
+ const normalized = normalizeLoginResponse(parsed);
+ if (!normalized) {
+ clearStoredLoginResponse();
+ return null;
+ }
+
+ window.localStorage.setItem(LOGIN_RESPONSE_KEY, JSON.stringify(normalized));
+
+ if (recordActivity) {
+ setStoredActivityAt(Date.now());
+ }
+
+ scheduleNextRefresh();
+ return normalized;
+}
+
+export function clearStoredLoginResponse() {
+ if (!canUseWindow()) {
+ return;
+ }
+ window.localStorage.removeItem(LOGIN_RESPONSE_KEY);
+ window.localStorage.removeItem(LOGIN_ACTIVITY_KEY);
+ clearRefreshTimer();
+}
+
+export function getAuthorizationToken() {
+ return getStoredLoginResponse()?.access_token || "";
+}
+
+export function recordUserActivity({ force = false } = {}) {
+ if (!canUseWindow() || !getStoredLoginResponse()?.access_token) {
+ return;
+ }
+
+ const now = Date.now();
+ if (!force && now - lastRecordedActivityAt < ACTIVITY_PERSIST_INTERVAL_MS) {
+ return;
+ }
+
+ lastRecordedActivityAt = now;
+ setStoredActivityAt(now);
+
+ const loginResponse = getStoredLoginResponse();
+ const refreshThresholdMs = getRefreshThresholdMs(loginResponse);
+ if (
+ loginResponse?.expires_at &&
+ loginResponse.expires_at - now <= refreshThresholdMs
+ ) {
+ refreshAccessToken().catch(() => {});
+ return;
+ }
+
+ scheduleNextRefresh();
+}
+
+export async function refreshAccessToken({ force = false } = {}) {
+ const loginResponse = getStoredLoginResponse();
+ if (!loginResponse?.access_token) {
+ return null;
+ }
+
+ const now = Date.now();
+ const expiresAt = Number(loginResponse.expires_at || 0);
+ const refreshThresholdMs = getRefreshThresholdMs(loginResponse);
+ if (
+ !force &&
+ (
+ !expiresAt ||
+ expiresAt - now > refreshThresholdMs ||
+ !hasRecentUserActivity(now) ||
+ now - lastRefreshAttemptAt < REFRESH_RETRY_INTERVAL_MS
+ )
+ ) {
+ return loginResponse;
+ }
+
+ if (refreshPromise) {
+ return refreshPromise;
+ }
+
+ lastRefreshAttemptAt = now;
+ const previousToken = loginResponse.access_token;
+
+ refreshPromise = requestTokenRefresh(previousToken)
+ .then((response) => {
+ if (response?.status === "ok" && response.access_token) {
+ return storeLoginResponse(response, { recordActivity: false });
+ }
+ return getStoredLoginResponse();
+ })
+ .catch((error) => {
+ const latest = getStoredLoginResponse();
+ if (latest?.access_token && latest.access_token !== previousToken) {
+ return latest;
+ }
+ if (Number(error?.status) === 401) {
+ dispatchLogout();
+ return null;
+ }
+ return latest;
+ })
+ .finally(() => {
+ refreshPromise = null;
+ scheduleNextRefresh();
+ });
+
+ return refreshPromise;
+}
+
+export function ensureFreshAccessToken(requestUrl) {
+ if (!getStoredLoginResponse()?.access_token || isRefreshPath(requestUrl)) {
+ return Promise.resolve(getStoredLoginResponse());
+ }
+ return refreshAccessToken();
+}
+
+export function startActivityAwareTokenRefresh() {
+ if (!canUseWindow() || managerStarted) {
+ return;
+ }
+
+ managerStarted = true;
+
+ const handleActivity = () => {
+ recordUserActivity();
+ };
+
+ ["mousedown", "keydown", "scroll", "touchstart", "mousemove"].forEach(
+ (eventName) => {
+ window.addEventListener(eventName, handleActivity, { passive: true });
+ }
+ );
+
+ document.addEventListener("visibilitychange", () => {
+ if (document.visibilityState === "visible") {
+ recordUserActivity({ force: true });
+ refreshAccessToken().catch(() => {});
+ } else {
+ scheduleNextRefresh();
+ }
+ });
+
+ window.addEventListener("storage", (event) => {
+ if (
+ event.key === LOGIN_RESPONSE_KEY ||
+ event.key === LOGIN_ACTIVITY_KEY
+ ) {
+ scheduleNextRefresh();
+ }
+ });
+
+ scheduleNextRefresh();
+}
diff --git a/web/src/utils/authority.js b/web/src/utils/authority.js
index 73f07abd9..14dd4a068 100644
--- a/web/src/utils/authority.js
+++ b/web/src/utils/authority.js
@@ -1,4 +1,28 @@
import request from "./request";
+import {
+ getAuthorizationToken,
+ getStoredLoginResponse,
+} from "./auth_session";
+
+const APPLICATION_AUTH_KEY = "infini-auth";
+const APPLICATION_ROLLUP_KEY = "infini-rollup-enabled";
+const ENTERPRISE_TASK_MANAGER_KEY = "infini-enterprise-task-manager-enabled";
+export const APPLICATION_SETTINGS_UPDATED_EVENT =
+ "console:application-settings-updated";
+let applicationSettingsPromise = null;
+let applicationSettingsCache = null;
+
+function persistApplicationSettings(res) {
+ localStorage.setItem(APPLICATION_AUTH_KEY, `${res.auth_enabled}`);
+ localStorage.setItem(
+ APPLICATION_ROLLUP_KEY,
+ `${!!res.system_cluster?.rollup_enabled}`
+ );
+ localStorage.setItem(
+ ENTERPRISE_TASK_MANAGER_KEY,
+ `${!!res.enterprise_plugins?.task_manager}`
+ );
+}
// use localStorage to store the authority info, which might be sent from server in actual project.
export function getAuthority(str) {
@@ -37,49 +61,64 @@ export function hasAuthority(authority) {
}
export function getAuthEnabled() {
- return localStorage.getItem("infini-auth");
+ return localStorage.getItem(APPLICATION_AUTH_KEY);
}
export function isLogin() {
- const responseStr = localStorage.getItem("login-response");
- if (responseStr) {
- let loginResponse = null;
- try {
- loginResponse = JSON.parse(responseStr);
- if (loginResponse?.username && loginResponse?.status == "ok") {
- return true;
- }
- } catch (err) {
- console.error(err);
- }
+ const loginResponse = getStoredLoginResponse();
+ if (loginResponse?.username && loginResponse?.status == "ok") {
+ return true;
}
return false;
}
export function getAuthorizationHeader() {
- const responseStr = localStorage.getItem("login-response");
- if (responseStr) {
- let loginResponse = null;
- try {
- loginResponse = JSON.parse(responseStr);
- } catch (err) {
- console.error(err);
- }
- if (loginResponse) {
- return "Bearer " + loginResponse.access_token;
- }
+ const accessToken = getAuthorizationToken();
+ if (accessToken) {
+ return "Bearer " + accessToken;
}
return "";
}
export function getRollupEnabled() {
- return localStorage.getItem("infini-rollup-enabled");
+ return localStorage.getItem(APPLICATION_ROLLUP_KEY);
}
-(async function() {
- const res = await request("/setting/application");
- if (res && !res.error) {
- localStorage.setItem("infini-auth", res.auth_enabled);
- localStorage.setItem('infini-rollup-enabled', res.system_cluster?.rollup_enabled || false)
+export function getEnterpriseTaskManagerEnabled() {
+ return localStorage.getItem(ENTERPRISE_TASK_MANAGER_KEY);
+}
+
+export async function refreshApplicationSettings(force = false) {
+ if (applicationSettingsPromise) {
+ return applicationSettingsPromise;
}
+
+ if (!force && applicationSettingsCache) {
+ return applicationSettingsCache;
+ }
+
+ applicationSettingsPromise = request("/setting/application")
+ .then((res) => {
+ if (res && !res.error) {
+ persistApplicationSettings(res);
+ applicationSettingsCache = res;
+ if (typeof window !== "undefined") {
+ window.dispatchEvent(
+ new CustomEvent(APPLICATION_SETTINGS_UPDATED_EVENT, {
+ detail: res,
+ })
+ );
+ }
+ }
+ return res;
+ })
+ .finally(() => {
+ applicationSettingsPromise = null;
+ });
+
+ return applicationSettingsPromise;
+}
+
+(async function() {
+ await refreshApplicationSettings();
})();
diff --git a/web/src/utils/request.js b/web/src/utils/request.js
index 876aaaea7..c1162e272 100644
--- a/web/src/utils/request.js
+++ b/web/src/utils/request.js
@@ -5,6 +5,7 @@ import hash from "hash.js";
import { isAntdPro } from "./utils";
import { formatMessage } from "umi/locale";
import { getAuthorizationHeader } from "./authority";
+import { ensureFreshAccessToken } from "./auth_session";
import * as uuid from 'uuid';
export const formatResponse = (response) => {
@@ -31,6 +32,187 @@ export const formatResponse = (response) => {
}
}
+const secureTransportErrorReason =
+ "Sensitive requests require HTTPS. Enable Console HTTPS or put Console behind an HTTPS reverse proxy.";
+const ERROR_NOTIFICATION_DEDUPE_MS = 4000;
+const DATATOOLS_LICENSE_REQUIRED_REASON =
+ "a valid license is required to use DataTools";
+const DATATOOLS_LICENSE_MODAL_EVENT = "console:datatools-license-required";
+const DATATOOLS_LICENSE_MODAL_DEDUPE_MS = 3000;
+const recentErrorNotifications = new Map();
+let lastDataToolsLicenseModalAt = 0;
+
+const sensitiveRequestRules = [
+ { method: "POST", pattern: /^\/account\/login\/challenge$/ },
+ { method: "POST", pattern: /^\/account\/login$/ },
+ { method: "POST", pattern: /^\/account\/refresh$/ },
+ { method: "PUT", pattern: /^\/account\/password$/ },
+ { method: "POST", pattern: /^\/user$/ },
+ { method: "PUT", pattern: /^\/user\/[^/]+\/password$/ },
+ { method: "POST", pattern: /^\/credential$/ },
+ { method: "PUT", pattern: /^\/credential\/[^/]+$/ },
+ { method: "POST", pattern: /^\/setup\/_validate$/ },
+ { method: "POST", pattern: /^\/setup\/_initialize$/ },
+ { method: "POST", pattern: /^\/setup\/_validate_secret$/ },
+ { method: "POST", pattern: /^\/elasticsearch\/$/ },
+ { method: "PUT", pattern: /^\/elasticsearch\/[^/]+$/ },
+ { method: "POST", pattern: /^\/elasticsearch\/try_connect$/ },
+ { method: "POST", pattern: /^\/email\/server$/ },
+ { method: "POST", pattern: /^\/email\/server\/_test$/ },
+ { method: "PUT", pattern: /^\/email\/server\/[^/]+$/ },
+ { method: "PUT", pattern: /^\/setting\/system\/rollup$/ },
+ { method: "PUT", pattern: /^\/setting\/system\/retention$/ },
+];
+
+const getNormalizedRequestPath = (requestUrl) => {
+ if (typeof window === "undefined") {
+ return requestUrl;
+ }
+
+ const resolvedUrl = new URL(requestUrl, window.location.origin);
+ let pathname = resolvedUrl.pathname;
+ const basePath =
+ window.routerBase && window.routerBase !== "/"
+ ? window.routerBase.replace(/\/$/, "")
+ : "";
+
+ if (basePath && pathname.startsWith(`${basePath}/`)) {
+ pathname = pathname.slice(basePath.length);
+ } else if (basePath && pathname === basePath) {
+ pathname = "/";
+ }
+
+ return pathname;
+};
+
+const requestUsesSecureTransport = (requestUrl) => {
+ if (typeof window === "undefined") {
+ return true;
+ }
+
+ return new URL(requestUrl, window.location.origin).protocol === "https:";
+};
+
+const getCurrentRoutePath = () => {
+ if (typeof window === "undefined") {
+ return "/";
+ }
+
+ const hash = window.location.hash || "";
+ const hashPath = hash.startsWith("#") ? hash.slice(1) : hash;
+ if (hashPath.startsWith("/")) {
+ return getNormalizedRequestPath(hashPath);
+ }
+
+ return getNormalizedRequestPath(window.location.pathname || "/");
+};
+
+const isDataToolsRoute = () => {
+ return /^\/data_tools(\/|$)/.test(getCurrentRoutePath());
+};
+
+const openDataToolsLicenseModal = () => {
+ if (typeof window === "undefined") {
+ return;
+ }
+
+ const now = Date.now();
+ if (now - lastDataToolsLicenseModalAt < DATATOOLS_LICENSE_MODAL_DEDUPE_MS) {
+ return;
+ }
+
+ lastDataToolsLicenseModalAt = now;
+ window.dispatchEvent(new CustomEvent(DATATOOLS_LICENSE_MODAL_EVENT));
+};
+
+const requestRequiresSecureTransport = (requestUrl, method = "GET") => {
+ const normalizedMethod = method.toUpperCase();
+ const normalizedPath = getNormalizedRequestPath(requestUrl);
+
+ return sensitiveRequestRules.some(
+ ({ method: sensitiveMethod, pattern }) =>
+ sensitiveMethod === normalizedMethod && pattern.test(normalizedPath)
+ );
+};
+
+const getInsecureTransportResponse = () => ({
+ status: "error",
+ success: false,
+ currentAuthority: "guest",
+ error: {
+ reason: secureTransportErrorReason,
+ },
+});
+
+const cleanupRecentErrorNotifications = (now = Date.now()) => {
+ recentErrorNotifications.forEach((timestamp, key) => {
+ if (now - timestamp >= ERROR_NOTIFICATION_DEDUPE_MS) {
+ recentErrorNotifications.delete(key);
+ }
+ });
+};
+
+const showErrorNotification = ({
+ message,
+ description,
+ style,
+ dedupeKey,
+}) => {
+ const now = Date.now();
+ cleanupRecentErrorNotifications(now);
+ const key = dedupeKey || `${message}`;
+ const lastShownAt = recentErrorNotifications.get(key);
+ if (lastShownAt && now - lastShownAt < ERROR_NOTIFICATION_DEDUPE_MS) {
+ return;
+ }
+ recentErrorNotifications.set(key, now);
+ notification.error({
+ key,
+ placement: "topRight",
+ message,
+ description,
+ style,
+ });
+};
+
+const fetchReplayNonce = async (requestUrl, requestMethod, authorizationHeader) => {
+ const nonceEndpoint = buildUrlWithBasePath("/account/replay_nonce");
+ const headers = {
+ Accept: "application/json",
+ "Content-Type": "application/json; charset=utf-8",
+ };
+ if (authorizationHeader) {
+ headers.Authorization = authorizationHeader;
+ }
+
+ const response = await fetch(nonceEndpoint, {
+ method: "POST",
+ credentials: "include",
+ headers,
+ body: JSON.stringify({
+ method: requestMethod,
+ path: getNormalizedRequestPath(requestUrl),
+ }),
+ });
+
+ let payload = null;
+ try {
+ payload = await response.json();
+ } catch (error) {
+ payload = null;
+ }
+
+ if (!response.ok || !payload?.nonce) {
+ const reason =
+ payload?.error?.reason ||
+ payload?.message ||
+ "failed to fetch replay nonce";
+ throw new Error(reason);
+ }
+
+ return payload.nonce;
+};
+
const checkStatus = async (response, noticeable, option={}) => {
const codeMessage = {
200: formatMessage({ id: "app.message.http.status.200" }),
@@ -53,25 +235,31 @@ const checkStatus = async (response, noticeable, option={}) => {
if (response.status >= 200 && response.status < 300) {
return response;
}
+ if (response.status === 403 && isDataToolsRoute()) {
+ let jsonRes = null;
+ try {
+ jsonRes = await response.clone().json();
+ } catch (error) {
+ jsonRes = null;
+ }
+ if (jsonRes?.error?.reason === DATATOOLS_LICENSE_REQUIRED_REASON) {
+ openDataToolsLicenseModal();
+ return response;
+ }
+ }
if (response.status == 500) {
- const jsonRes = await response.clone().json();
- if (jsonRes.error && !jsonRes.stack) {
+ let jsonRes = null;
+ try {
+ jsonRes = await response.clone().json();
+ } catch (error) {
+ jsonRes = null;
+ }
+ if (jsonRes?.error && !jsonRes.stack) {
if (noticeable) {
- let desc = "";
- if (typeof jsonRes.error == "string") {
- desc = jsonRes.error;
- } else {
- if (jsonRes.error?.reason) {
- desc = (
-
- {jsonRes.error.reason}
-
- );
- } else {
- desc = JSON.stringify(jsonRes.error);
- }
- }
- desc = (
+ const friendlyMessage = jsonRes.error?.key
+ ? formatMessage({ id: jsonRes.error.key })
+ : formatMessage({ id: "app.message.http.status.500" });
+ const desc = (
{
{response.status}
- {desc}
+
+ {friendlyMessage}
+
);
- notification.error({
+ showErrorNotification({
message: formatMessage({ id: "app.message.http.request.error" }),
description: desc,
style: { wordBreak: "break-all" },
+ dedupeKey: `http-500:${jsonRes.error?.key || jsonRes.error?.reason || friendlyMessage}`,
});
}
return response;
@@ -104,10 +295,11 @@ const checkStatus = async (response, noticeable, option={}) => {
option.hasOwnProperty("showErrorInner") &&
option.showErrorInner === true
) {
- notification.error({
+ showErrorNotification({
message: response.statusText,
description: errortext,
style: { wordBreak: "break-all" },
+ dedupeKey: `http-inner:${response.status}:${response.statusText}:${errortext}`,
});
return response;
}
@@ -136,10 +328,11 @@ const checkStatus = async (response, noticeable, option={}) => {
{errortext}
);
- notification.error({
+ showErrorNotification({
message: `${formatMessage({ id: "app.message.http.request.error" })}`,
description: desc,
style: { wordBreak: "break-all" },
+ dedupeKey: `http-status:${response.status}:${errortext}`,
});
}
}
@@ -264,10 +457,11 @@ export default function request(
signal,
};
const newOptions = { ...defaultOptions, ...options };
+ const requestMethod = (newOptions.method || "GET").toUpperCase();
if (
- newOptions.method === "POST" ||
- newOptions.method === "PUT" ||
- newOptions.method === "DELETE"
+ requestMethod === "POST" ||
+ requestMethod === "PUT" ||
+ requestMethod === "DELETE"
) {
if (!(newOptions.body instanceof FormData)) {
newOptions.headers = {
@@ -290,9 +484,17 @@ export default function request(
"Accept-Encoding": "gzip, deflate, br",
...newOptions.headers,
};
- const authorizationHeader = getAuthorizationHeader();
- if (authorizationHeader) {
- newOptions.headers["Authorization"] = authorizationHeader;
+ const requiresSecureTransport = requestRequiresSecureTransport(url, requestMethod);
+ if (requiresSecureTransport && !requestUsesSecureTransport(url)) {
+ if (noticeable) {
+ showErrorNotification({
+ message: "HTTPS required",
+ description: secureTransportErrorReason,
+ style: { wordBreak: "break-all" },
+ dedupeKey: `https-required:${getNormalizedRequestPath(url)}`,
+ });
+ }
+ return Promise.resolve(getInsecureTransportResponse());
}
const expirys = options.expirys && 60;
@@ -311,8 +513,11 @@ export default function request(
}
}
- return (
- fetch(url, newOptions)
+ const sendRequest = (nonce) => {
+ if (nonce) {
+ newOptions.headers["X-Request-Nonce"] = nonce;
+ }
+ return fetch(url, newOptions)
.then((res) => checkStatus(res, noticeable, option))
// .then(response => cachedSave(response, hashcode))
.then((response) => {
@@ -332,11 +537,15 @@ export default function request(
//connection refused
const err = new Error();
err.name = "ERR_CONNECTION_REFUSED";
- err.message = "Failed to connnect server";
+ err.message = formatMessage({
+ id: "error.request.connection_refused",
+ });
if (typeof setGlobalHealth === "function") {
setGlobalHealth({
error: err.name,
- desc: err.message,
+ desc: formatMessage({
+ id: "error.request.connection_refused.tip",
+ }),
});
}
return err;
@@ -344,7 +553,7 @@ export default function request(
if (status === "AbortError") {
if (noticeable) {
- notification.error({
+ showErrorNotification({
message: formatMessage({
id: "app.message.http.request.timeout",
}),
@@ -355,6 +564,7 @@ export default function request(
"\r\nURL:" +
url,
style: { wordBreak: "break-all" },
+ dedupeKey: `request-timeout:${url}`,
});
}
return;
@@ -364,9 +574,15 @@ export default function request(
if (status === 401) {
// @HACK
/* eslint-disable no-underscore-dangle */
- if (location.href.indexOf("user/login") === -1) {
+ if (
+ option?.skipAuthRedirect !== true &&
+ location.href.indexOf("user/login") === -1
+ ) {
window.g_app._store.dispatch({
type: "login/logout",
+ payload: {
+ skipServerLogout: true,
+ },
});
}
}
@@ -392,6 +608,37 @@ export default function request(
return e.rawResponse;
}
return e.response;
- })
- );
+ });
+ };
+
+ const executeRequest = () => {
+ const authorizationHeader = getAuthorizationHeader();
+ if (authorizationHeader) {
+ newOptions.headers["Authorization"] = authorizationHeader;
+ } else {
+ delete newOptions.headers["Authorization"];
+ }
+
+ const noncePromise = requiresSecureTransport
+ ? fetchReplayNonce(url, requestMethod, authorizationHeader)
+ : Promise.resolve(null);
+
+ const handleNonceError = (error) => {
+ if (noticeable) {
+ showErrorNotification({
+ message: "Request rejected",
+ description: error?.message || "failed to fetch replay nonce",
+ style: { wordBreak: "break-all" },
+ dedupeKey: `request-rejected:${getNormalizedRequestPath(url)}:${error?.message || "failed to fetch replay nonce"}`,
+ });
+ }
+ return getInsecureTransportResponse();
+ };
+
+ return noncePromise.then((nonce) => sendRequest(nonce), handleNonceError);
+ };
+
+ return Promise.resolve(
+ option?.skipAuthRefresh === true ? null : ensureFreshAccessToken(url)
+ ).then(executeRequest);
}
diff --git a/web/src/utils/setup.js b/web/src/utils/setup.js
index 79807e346..165bba0e0 100644
--- a/web/src/utils/setup.js
+++ b/web/src/utils/setup.js
@@ -12,4 +12,22 @@ export function isSystemCluster(clusterID){
export function getSystemClusterID() {
return 'infini_default_system_cluster';
-}
\ No newline at end of file
+}
+
+export function getPreferredCluster(clusters = [], options = {}) {
+ if (!Array.isArray(clusters) || clusters.length === 0) {
+ return null;
+ }
+
+ const { selectedClusterID, targetClusterID } = options;
+ const findCluster = (clusterID) =>
+ clusterID ? clusters.find((item) => item.id === clusterID) : null;
+
+ return (
+ findCluster(selectedClusterID) ||
+ findCluster(targetClusterID) ||
+ findCluster(getSystemClusterID()) ||
+ clusters[0] ||
+ null
+ );
+}
diff --git a/web/src/utils/treemap_title.js b/web/src/utils/treemap_title.js
new file mode 100644
index 000000000..008113eed
--- /dev/null
+++ b/web/src/utils/treemap_title.js
@@ -0,0 +1,23 @@
+import { formatMessage } from "umi/locale";
+
+const TREEMAP_TITLE_MAP = {
+ "Search latency by index": "cluster.monitor.treemap.search_latency_by_index",
+ "Avg search latency by index":
+ "cluster.monitor.treemap.search_latency_by_index",
+};
+
+export const getLocalizedTreemapTitle = (title) => {
+ if (!title) {
+ return title;
+ }
+
+ const localeId = TREEMAP_TITLE_MAP[title];
+ if (!localeId) {
+ return title;
+ }
+
+ return formatMessage({
+ id: localeId,
+ defaultMessage: title,
+ });
+};