diff --git a/controllers/account.go b/controllers/account.go index 06201e1b..e1c177c8 100644 --- a/controllers/account.go +++ b/controllers/account.go @@ -44,18 +44,22 @@ func InitAuthConfig() { iamsdk.InitConfig(iamEndpoint, clientId, clientSecret, "", iamOrganization, iamApplication) application, err := iamsdk.GetApplication(iamApplication) if err != nil { - panic(err) + fmt.Printf("[WARN] Failed to get IAM application %q: %v (auth features disabled)\n", iamApplication, err) + return } if application == nil { - panic(fmt.Errorf("The application: %s does not exist", iamApplication)) + fmt.Printf("[WARN] IAM application %q does not exist (auth features disabled)\n", iamApplication) + return } cert, err := iamsdk.GetCert(application.Cert) if err != nil { - panic(err) + fmt.Printf("[WARN] Failed to get cert %q for application %q: %v (auth features disabled)\n", application.Cert, iamApplication, err) + return } if cert == nil { - panic(fmt.Errorf("The cert: %s does not exist", application.Cert)) + fmt.Printf("[WARN] Cert %q for application %q does not exist (auth features disabled)\n", application.Cert, iamApplication) + return } iamsdk.InitConfig(iamEndpoint, clientId, clientSecret, cert.Certificate, iamOrganization, iamApplication) diff --git a/controllers/openai_api.go b/controllers/openai_api.go index ed232134..33dc1c43 100644 --- a/controllers/openai_api.go +++ b/controllers/openai_api.go @@ -470,7 +470,9 @@ func resolveConsoleKeys(org string) (publicKey, secretKey string) { // (console-pk-{org} / console-sk-{org}), enabling each org to see their own usage // in console.hanzo.ai. This is fire-and-forget — failures are silently ignored. func recordTrace(record *usageRecord, startTime time.Time) { - // Write to ClickHouse via native ZAP if datastore is connected. + // Write billing record to ClickHouse for invoice reconciliation. + go zapWriteUsage(record, startTime) + // Write observability trace to ClickHouse via native ZAP. go zapWriteTrace(record, startTime) go func() { diff --git a/controllers/zap_native.go b/controllers/zap_native.go index 2834d93b..d29e5348 100644 --- a/controllers/zap_native.go +++ b/controllers/zap_native.go @@ -169,6 +169,98 @@ func zapWriteTrace(record *usageRecord, startTime time.Time) { } } +// ── ZAP billing record writer (datastore → ClickHouse) ────────────────── +// +// Writes billing/usage records to hanzo.cloud_usage for invoice reconciliation. +// Both Commerce and Console can query this table for unified billing views. + +var usageTableCreated bool + +func zapEnsureUsageTable() { + if usageTableCreated { + return + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + err := object.ZapDatastoreExec(ctx, ` + CREATE TABLE IF NOT EXISTS hanzo.cloud_usage ( + id String, + timestamp DateTime, + owner String, + user_id String, + organization String, + model String, + provider String, + request_id String, + prompt_tokens UInt32, + completion_tokens UInt32, + total_tokens UInt32, + cache_read_tokens UInt32, + cache_write_tokens UInt32, + cost_cents UInt64, + currency String, + status String, + error_msg String, + is_premium UInt8, + is_stream UInt8, + client_ip String + ) ENGINE = MergeTree() + ORDER BY (timestamp, organization, user_id) + TTL timestamp + INTERVAL 2 YEAR + `) + if err != nil { + logs.Warn("ZAP: failed to create cloud_usage table: %v", err) + return + } + usageTableCreated = true +} + +func zapWriteUsage(record *usageRecord, startTime time.Time) { + if !object.DatastoreEnabled() { + return + } + + zapEnsureUsageTable() + + org := record.Organization + if org == "" { + org = record.Owner + } + + costCents := calculateCostCentsWithCache( + record.Model, record.PromptTokens, record.CompletionTokens, + record.CacheReadTokens, record.CacheWriteTokens, + ) + + premium := uint8(0) + if record.Premium { + premium = 1 + } + stream := uint8(0) + if record.Stream { + stream = 1 + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err := object.ZapDatastoreExec(ctx, + `INSERT INTO hanzo.cloud_usage (id, timestamp, owner, user_id, organization, model, provider, request_id, prompt_tokens, completion_tokens, total_tokens, cache_read_tokens, cache_write_tokens, cost_cents, currency, status, error_msg, is_premium, is_stream, client_ip) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + record.RequestID, startTime.UTC(), + record.Owner, record.User, org, + record.Model, record.Provider, record.RequestID, + record.PromptTokens, record.CompletionTokens, record.TotalTokens, + record.CacheReadTokens, record.CacheWriteTokens, + costCents, "usd", + record.Status, record.ErrorMsg, + premium, stream, record.ClientIP, + ) + if err != nil { + logs.Warn("ZAP: usage write failed: %v", err) + } +} + // ── models.list ───────────────────────────────────────────────────────── func zapListModelsHandler() (*zap.Message, error) { diff --git a/web/craco.config.js b/web/craco.config.js index 1d60efda..53572b1a 100644 --- a/web/craco.config.js +++ b/web/craco.config.js @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -const CracoLessPlugin = require("craco-less"); const path = require("path"); module.exports = { @@ -28,19 +27,19 @@ module.exports = { }, }, }, - plugins: [ - { - plugin: CracoLessPlugin, - options: { - lessLoaderOptions: { - lessOptions: { - modifyVars: {"@primary-color": "rgb(89,54,213)", "@border-radius-base": "5px"}, - javascriptEnabled: true, - }, - }, + style: { + postcss: { + loaderOptions: (postcssLoaderOptions) => { + postcssLoaderOptions.postcssOptions = { + plugins: [ + require("tailwindcss"), + require("autoprefixer"), + ], + }; + return postcssLoaderOptions; }, }, - ], + }, webpack: { configure: (webpackConfig, {env, paths}) => { paths.appBuild = path.resolve(__dirname, "build-temp"); diff --git a/web/package.json b/web/package.json index 389987b6..82d62dad 100644 --- a/web/package.json +++ b/web/package.json @@ -19,6 +19,7 @@ "aliplayer-react": "^0.7.0", "antd": "5.24.0", "antd-token-previewer": "^2.0.8", + "autoprefixer": "^10.4.20", "bpmn-font": "^0.12.1", "bpmn-js": "^18.4.0", "bpmn-js-color-picker": "^0.7.2", @@ -26,9 +27,11 @@ "bpmn-js-element-templates": "^2.11.0", "bpmn-js-properties-panel": "^5.35.0", "camunda-bpmn-moddle": "^7.0.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "codemirror": "^6.0.1", + "cookie": "^1.1.1", "copy-to-clipboard": "^3.3.1", - "craco-less": "2.0.0", "d3-force": "^3.0.0", "dayjs": "^1.11.19", "dompurify": "^3.0.9", @@ -43,17 +46,20 @@ "identicon.js": "^2.3.3", "js-base64": "^3.7.7", "katex": "^0.16.9", + "lodash": "4.17.21", + "lucide-react": "^0.468.0", "marked": "^12.0.1", "md5": "^2.3.0", "moment": "^2.29.1", "papaparse": "^5.4.1", + "postcss": "^8.4.49", "rc-bullets": "^1.5.16", "react": "^18.2.0", "react-bpmn": "^0.2.0", "react-device-detect": "1.17.0", "react-dom": "^18.2.0", "react-draggable": "^4.4.6", - "react-file-viewer": "^1.2.1", + "react-file-viewer": "1.2.1", "react-force-graph": "^1.48.1", "react-force-graph-2d": "^1.29.0", "react-github-corner": "^2.5.0", @@ -67,6 +73,9 @@ "remark-frontmatter": "^4.0.1", "remark-gfm": "^3.0.1", "remark-math": "^6.0.0", + "sonner": "^1.7.1", + "tailwind-merge": "^2.6.0", + "tailwindcss": "^3.4.17", "xlsx": "^0.16.9" }, "scripts": { @@ -76,10 +85,9 @@ "test": "craco test", "eject": "craco eject", "analyze": "source-map-explorer 'build/static/js/*.js'", - "preinstall": "node -e \"if (process.env.npm_execpath.indexOf('yarn') === -1) throw new Error('Use yarn for installing: https://yarnpkg.com/en/docs/install')\"", "fix": "eslint --fix src/ --ext .js", "lint:js": "eslint --fix src/ --ext .js", - "lint:css": "stylelint src/**/*.{css,less} --fix", + "lint:css": "stylelint src/**/*.css --fix", "lint": "yarn lint:js && yarn lint:css" }, "eslintConfig": { @@ -89,14 +97,12 @@ "production": [ ">0.2%", "not dead", - "not op_mini all", - "ie 9, ie 10, ie 11" + "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", - "last 1 safari version", - "ie 9, ie 10, ie 11" + "last 1 safari version" ] }, "devDependencies": { @@ -107,23 +113,17 @@ "cross-env": "^7.0.3", "eslint": "8.22.0", "eslint-plugin-react": "^7.31.1", - "husky": "^4.3.8", + "eslint-plugin-unused-imports": "^2.0.0", "lint-staged": "^13.0.3", "stylelint": "^14.11.0", - "stylelint-config-recommended-less": "^1.0.4", "stylelint-config-standard": "^28.0.0" }, "lint-staged": { - "src/**/*.{css,less}": [ + "src/**/*.css": [ "stylelint --fix" ], - "src/**/*.{js,jsx,ts,tsx}": [ + "src/**/*.{js,jsx}": [ "eslint --fix" ] - }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } } } diff --git a/web/postcss.config.js b/web/postcss.config.js new file mode 100644 index 00000000..33ad091d --- /dev/null +++ b/web/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/web/src/ActivityPage.js b/web/src/ActivityPage.js index 00e55435..6381318d 100644 --- a/web/src/ActivityPage.js +++ b/web/src/ActivityPage.js @@ -13,7 +13,6 @@ // limitations under the License. import React from "react"; -import {Col, Row, Select, Statistic} from "antd"; import BaseListPage from "./BaseListPage"; import * as Setting from "./Setting"; import * as ActivityBackend from "./backend/ActivityBackend"; @@ -21,7 +20,6 @@ import ReactEcharts from "echarts-for-react"; import i18next from "i18next"; import * as UsageBackend from "./backend/UsageBackend"; -const {Option} = Select; class ActivityPage extends BaseListPage { constructor(props) { @@ -190,22 +188,22 @@ class ActivityPage extends BaseListPage { const isLoading = activityResponse === undefined; return ( - - +
+
- - +
+
- - +
+
); } @@ -238,20 +236,15 @@ class ActivityPage extends BaseListPage { return (
{this.renderDropdown()} - this.setState({selectedOps})} allowClear maxTagCount="responsive" maxTagTextLength={10} > {this.state.allOps.map(op => ( - + ))} - +
); } @@ -286,14 +279,11 @@ class ActivityPage extends BaseListPage { return (
{i18next.t("general:User")}: - handleChange(value))} style={{width: "100%"}} > {users_options} - +
); } @@ -337,12 +327,12 @@ class ActivityPage extends BaseListPage { grouped.push(this.subPieCharts.slice(i, i + 2)); } return ( - +
{ grouped.map((r, rowIndex) => ( - +
{r.map((dataName, colIndex) => ( - +
- +
))} - +
)) } - +
); } @@ -374,9 +364,9 @@ class ActivityPage extends BaseListPage { const activitiesAction = this.state[fieldName]; return ( - - - +
+
+
- - +
+
- - - - - +
+
+
+
+
{this.renderSubPieCharts()} - - +
+
); } @@ -429,21 +419,21 @@ class ActivityPage extends BaseListPage { render() { return (
- - - +
+
+
{this.renderStatistic(this.state["activitiesresponse"])} - - +
+
{this.renderRadio()} - - - - - +
+
+
+
+
{this.renderChart()} - - +
+
); } diff --git a/web/src/AgentsPage.js b/web/src/AgentsPage.js index f8ab5749..7aa42fa0 100644 --- a/web/src/AgentsPage.js +++ b/web/src/AgentsPage.js @@ -13,9 +13,9 @@ // limitations under the License. import React from "react"; -import {Card, Spin} from "antd"; import * as AgentsBackend from "./backend/AgentsBackend"; import i18next from "i18next"; +import {Loader2} from "lucide-react"; class AgentsPage extends React.Component { constructor(props) { @@ -59,21 +59,21 @@ class AgentsPage extends React.Component { if (this.state.loading) { return (
- +
); } if (this.state.error) { return ( - +

{i18next.t("general:Failed to load agents dashboard")}: {this.state.error}

{i18next.t("general:Configure AGENTS_DASHBOARD_URL environment variable to set the agents control plane URL")}

- +
); } diff --git a/web/src/App.js b/web/src/App.js index a964d2fd..dd17bf2a 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -12,17 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React, {Component} from "react"; -import {Link, Redirect, Route, Switch, withRouter} from "react-router-dom"; -import {StyleProvider, legacyLogicalPropertiesTransformer} from "@ant-design/cssinjs"; -import {Avatar, Button, Card, ConfigProvider, Drawer, Dropdown, FloatButton, Layout, Menu, Result} from "antd"; -import {AppstoreTwoTone, BarsOutlined, BulbTwoTone, CloudTwoTone, CommentOutlined, DesktopOutlined, DownOutlined, HomeTwoTone, LockTwoTone, LoginOutlined, LogoutOutlined, RobotOutlined, SettingOutlined, SettingTwoTone, VideoCameraTwoTone, WalletTwoTone} from "@ant-design/icons"; -import "./App.less"; +import React, {useCallback, useEffect, useMemo, useRef, useState} from "react"; +import {Link, Redirect, Route, Switch, useHistory, useLocation} from "react-router-dom"; import {Helmet} from "react-helmet"; +import {Toaster} from "sonner"; +import {useTranslation} from "react-i18next"; +import i18next from "i18next"; +import {Bot, ChevronDown, ChevronLeft, Cloud, Home, LayoutGrid, Lightbulb, Lock, LogIn, LogOut, Menu, MessageSquare, Monitor, Settings, User, Video, Wallet, X} from "lucide-react"; import * as Setting from "./Setting"; import * as AccountBackend from "./backend/AccountBackend"; -import AuthCallback from "./AuthCallback"; import * as Conf from "./Conf"; +import * as FormBackend from "./backend/FormBackend"; +import * as StoreBackend from "./backend/StoreBackend"; +import * as FetchFilter from "./backend/FetchFilter"; +import {PreviewInterceptor} from "./PreviewInterceptor"; +import {cn} from "./lib/utils"; + +// Page imports +import AuthCallback from "./AuthCallback"; import HomePage from "./HomePage"; import StoreListPage from "./StoreListPage"; import StoreEditPage from "./StoreEditPage"; @@ -38,10 +45,6 @@ import ProviderEditPage from "./ProviderEditPage"; import VectorListPage from "./VectorListPage"; import VectorEditPage from "./VectorEditPage"; import SigninPage from "./SigninPage"; -import i18next from "i18next"; -import {withTranslation} from "react-i18next"; -import LanguageSelect from "./LanguageSelect"; -import ThemeSelect from "./ThemeSelect"; import ChatEditPage from "./ChatEditPage"; import ChatListPage from "./ChatListPage"; import MessageListPage from "./MessageListPage"; @@ -73,7 +76,6 @@ import TaskEditPage from "./TaskEditPage"; import FormListPage from "./FormListPage"; import FormEditPage from "./FormEditPage"; import FormDataPage from "./FormDataPage"; -import * as FormBackend from "./backend/FormBackend"; import ArticleListPage from "./ArticleListPage"; import ArticleEditPage from "./ArticleEditPage"; import ChatPage from "./ChatPage"; @@ -81,15 +83,12 @@ import CustomGithubCorner from "./CustomGithubCorner"; import ShortcutsPage from "./basic/ShortcutsPage"; import UsagePage from "./UsagePage"; import ActivityPage from "./ActivityPage"; -import * as StoreBackend from "./backend/StoreBackend"; import NodeWorkbench from "./NodeWorkbench"; import AccessPage from "./component/access/AccessPage"; -import {PreviewInterceptor} from "./PreviewInterceptor"; import AuditPage from "./frame/AuditPage"; import PythonYolov8miPage from "./frame/PythonYolov8miPage"; import PythonSrPage from "./frame/PythonSrPage"; import SystemInfo from "./SystemInfo"; -import * as FetchFilter from "./backend/FetchFilter"; import OsDesktop from "./OsDesktop"; import TemplateListPage from "./TemplateListPage"; import TemplateEditPage from "./TemplateEditPage"; @@ -110,34 +109,93 @@ import ConsultationListPage from "./ConsultationListPage"; import ConsultationEditPage from "./ConsultationEditPage"; import AgentsPage from "./AgentsPage"; import VmPage from "./VmPage"; +import LanguageSelect from "./LanguageSelect"; +import ThemeSelect from "./ThemeSelect"; -const {Header, Footer, Content} = Layout; +// Sidebar nav group component +function NavGroup({icon: Icon, label, children, defaultOpen = false}) { + const [open, setOpen] = useState(defaultOpen); + const location = useLocation(); -class App extends Component { - constructor(props) { - super(props); - this.setThemeAlgorithm(); - let storageThemeAlgorithm = []; - try { - storageThemeAlgorithm = localStorage.getItem("themeAlgorithm") ? JSON.parse(localStorage.getItem("themeAlgorithm")) : ["default"]; - } catch { - storageThemeAlgorithm = ["default"]; + // Auto-open if any child matches current path + const isActive = useMemo(() => { + return React.Children.toArray(children).some(child => { + return child?.props?.to && location.pathname.startsWith(child.props.to); + }); + }, [children, location.pathname]); + + useEffect(() => { + if (isActive) { + setOpen(true); } - this.state = { - classes: props, - selectedMenuKey: 0, - account: undefined, - uri: null, - themeAlgorithm: storageThemeAlgorithm, - themeData: Conf.ThemeDefault, - menuVisible: false, - forms: [], - store: undefined, - }; - this.initConfig(); + }, [isActive]); + + return ( +
+ + {open && ( +
+ {children} +
+ )} +
+ ); +} + +function NavItem({to, children, external}) { + const location = useLocation(); + const active = location.pathname === to || (to !== "/" && location.pathname.startsWith(to)); + + if (external) { + return ( + + {children} + + + + + ); } - initConfig() { + return ( + + {children} + + ); +} + +function App() { + const history = useHistory(); + const location = useLocation(); + const {t} = useTranslation(); + + const [account, setAccount] = useState(undefined); + const [forms, setForms] = useState([]); + const [store, setStore] = useState(undefined); + const [sidebarOpen, setSidebarOpen] = useState(true); + const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); + const previewInterceptorRef = useRef(null); + + // Initialize config + useEffect(() => { Setting.initServerUrl(); Setting.initWebConfig(); @@ -148,19 +206,39 @@ class App extends Component { FetchFilter.initDemoMode(); Setting.initIamSdk(Conf.AuthConfig); + if (!Conf.DisablePreviewMode) { - this.previewInterceptor = new PreviewInterceptor(() => this.state.account, this.props.history); // add interceptor + previewInterceptorRef.current = new PreviewInterceptor(() => account, history); } - } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + // Fetch account + const getAccount = useCallback(() => { + AccountBackend.getAccount().then((res) => { + const acc = res.data; + if (acc !== null) { + const language = localStorage.getItem("language"); + if (language !== "" && language !== i18next.language) { + Setting.setLanguage(language); + } + } + setAccount(acc); + }); + }, []); - UNSAFE_componentWillMount() { - this.updateMenuKey(); - this.getAccount(); - this.setTheme(); - this.getForms(); - } + // Fetch forms + const getForms = useCallback(() => { + FormBackend.getForms("admin").then((res) => { + if (res.status === "ok") { + setForms(res.data); + } else { + Setting.showMessage("error", `${i18next.t("general:Failed to get")}: ${res.msg}`); + } + }); + }, []); - setTheme() { + // Fetch store theme + const getStoreTheme = useCallback(() => { StoreBackend.getStore("admin", "_cloud_default_store_").then((res) => { if (res.status === "ok" && res.data) { const color = res.data.themeColor ? res.data.themeColor : Conf.ThemeDefault.colorPrimary; @@ -169,939 +247,566 @@ class App extends Component { Setting.setThemeColor(color); localStorage.setItem("themeColor", color); } - this.setState({store: res.data}); + setStore(res.data); } else { Setting.setThemeColor(Conf.ThemeDefault.colorPrimary); - Setting.showMessage("error", `${i18next.t("general:Failed to get")}: ${res.msg}`); } }); - } - - componentDidUpdate() { - // eslint-disable-next-line no-restricted-globals - const uri = location.pathname; - if (this.state.uri !== uri) { - this.updateMenuKey(); - } - } - - updateMenuKeyForm(forms) { - // eslint-disable-next-line no-restricted-globals - const uri = location.pathname; - this.setState({ - uri: uri, - }); - - forms.forEach(form => { - const path = `/forms/${form.name}/data`; - if (uri.includes(path)) { - this.setState({selectedMenuKey: path}); + }, []); + + useEffect(() => { + getAccount(); + getForms(); + getStoreTheme(); + }, [getAccount, getForms, getStoreTheme]); + + // Close mobile sidebar on navigation + useEffect(() => { + setMobileSidebarOpen(false); + }, [location.pathname]); + + const signout = useCallback(() => { + AccountBackend.signout().then((res) => { + if (res.status === "ok") { + setAccount(null); + Setting.showMessage("success", i18next.t("account:Successfully signed out, redirected to homepage")); + Setting.goToLink("/"); + } else { + Setting.showMessage("error", `${i18next.t("account:Signout failed")}: ${res.msg}`); } }); - } + }, []); - updateMenuKey() { - // eslint-disable-next-line no-restricted-globals - const uri = location.pathname; - this.setState({ - uri: uri, - }); - if (uri === "/" || uri === "/home") { - this.setState({selectedMenuKey: "/"}); - } else if (uri.includes("/stores")) { - this.setState({selectedMenuKey: "/stores"}); - } else if (uri.includes("/providers")) { - this.setState({selectedMenuKey: "/providers"}); - } else if (uri.includes("/vectors")) { - this.setState({selectedMenuKey: "/vectors"}); - } else if (uri.includes("/chats")) { - this.setState({selectedMenuKey: "/chats"}); - } else if (uri.includes("/messages")) { - this.setState({selectedMenuKey: "/messages"}); - } else if (uri.includes("/graphs")) { - this.setState({selectedMenuKey: "/graphs"}); - } else if (uri.includes("/scans")) { - this.setState({selectedMenuKey: "/scans"}); - } else if (uri.includes("/usages")) { - this.setState({selectedMenuKey: "/usages"}); - } else if (uri.includes("/activities")) { - this.setState({selectedMenuKey: "/activities"}); - } else if (uri.includes("/nodes")) { - this.setState({selectedMenuKey: "/nodes"}); - } else if (uri.includes("/machines")) { - this.setState({selectedMenuKey: "/machines"}); - } else if (uri.includes("/assets")) { - this.setState({selectedMenuKey: "/assets"}); - } else if (uri.includes("/images")) { - this.setState({selectedMenuKey: "/images"}); - } else if (uri.includes("/containers")) { - this.setState({selectedMenuKey: "/containers"}); - } else if (uri.includes("/pods")) { - this.setState({selectedMenuKey: "/pods"}); - } else if (uri.includes("/templates")) { - this.setState({selectedMenuKey: "/templates"}); - } else if (uri.includes("/applications")) { - this.setState({selectedMenuKey: "/applications"}); - } else if (uri.includes("/sessions")) { - this.setState({selectedMenuKey: "/sessions"}); - } else if (uri.includes("/connections")) { - this.setState({selectedMenuKey: "/connections"}); - } else if (uri.includes("/records")) { - this.setState({selectedMenuKey: "/records"}); - } else if (uri.includes("/workflows")) { - this.setState({selectedMenuKey: "/workflows"}); - } else if (uri.includes("/audit")) { - this.setState({selectedMenuKey: "/audit"}); - } else if (uri.includes("/yolov8mi")) { - this.setState({selectedMenuKey: "/yolov8mi"}); - } else if (uri.includes("/sr")) { - this.setState({selectedMenuKey: "/sr"}); - } else if (uri.includes("/tasks")) { - this.setState({selectedMenuKey: "/tasks"}); - } else if (uri.includes("/forms")) { - this.setState({selectedMenuKey: "/forms"}); - } else if (uri.includes("/articles")) { - this.setState({selectedMenuKey: "/articles"}); - } else if (uri.includes("/hospitals")) { - this.setState({selectedMenuKey: "/hospitals"}); - } else if (uri.includes("/doctors")) { - this.setState({selectedMenuKey: "/doctors"}); - } else if (uri.includes("/patients")) { - this.setState({selectedMenuKey: "/patients"}); - } else if (uri.includes("/caases")) { - this.setState({selectedMenuKey: "/caases"}); - } else if (uri.includes("/consultations")) { - this.setState({selectedMenuKey: "/consultations"}); - } else if (uri.includes("/public-videos")) { - this.setState({selectedMenuKey: "/public-videos"}); - } else if (uri.includes("/videos")) { - this.setState({selectedMenuKey: "/videos"}); - } else if (uri.includes("/chat")) { - this.setState({selectedMenuKey: "/chat"}); - } else if (uri.includes("/agents")) { - this.setState({selectedMenuKey: "/agents"}); - } else if (uri.includes("/vm")) { - this.setState({selectedMenuKey: "/vm"}); - } else if (uri.includes("/sysinfo")) { - this.setState({selectedMenuKey: "/sysinfo"}); - } else if (uri.includes("/swagger")) { - this.setState({selectedMenuKey: "/swagger"}); - } else { - this.setState({selectedMenuKey: "null"}); - } - } - - onUpdateAccount(account) { - this.setState({ - account: account, - }); - } - - setLanguage(account) { - // let language = account?.language; - const language = localStorage.getItem("language"); - if (language !== "" && language !== i18next.language) { - Setting.setLanguage(language); + const renderSigninIfNotSignedIn = useCallback((component) => { + if (account === null) { + const signinUrl = Setting.getSigninUrl(); + if (signinUrl && signinUrl !== "") { + sessionStorage.setItem("from", window.location.pathname); + window.location.replace(signinUrl); + } + return null; + } else if (account === undefined) { + return null; } - } - - getAccount() { - AccountBackend.getAccount() - .then((res) => { - this.initConfig(); - const account = res.data; - if (account !== null) { - this.setLanguage(account); - } - - this.setState({ - account: account, - }); - }); - } - - getForms() { - FormBackend.getForms("admin") - .then((res) => { - if (res.status === "ok") { - this.setState({ - forms: res.data, - }); - - this.updateMenuKeyForm(res.data); - } else { - Setting.showMessage("error", `${i18next.t("general:Failed to get")}: ${res.msg}`); - } - }); - } + return component; + }, [account]); - signout() { - AccountBackend.signout() - .then((res) => { - if (res.status === "ok") { - this.setState({ - account: null, - }); - - Setting.showMessage("success", i18next.t("account:Successfully signed out, redirected to homepage")); - Setting.goToLink("/"); - // this.props.history.push("/"); - } else { - Setting.showMessage("error", `${i18next.t("account:Signout failed")}: ${res.msg}`); - } - }); - } - - handleRightDropdownClick(e) { - if (e.key === "/account") { - Setting.openLink(Setting.getMyProfileUrl(this.state.account)); - } else if (e.key === "/logout") { - this.signout(); + const renderHomeIfSignedIn = useCallback((component) => { + if (account !== null && account !== undefined) { + return ; } - } + return component; + }, [account]); - isStoreSelectEnabled() { - const uri = this.state.uri || window.location.pathname; + const isHiddenHeaderAndFooter = useCallback(() => { + const hiddenPaths = ["/workbench", "/access"]; + return hiddenPaths.some(path => location.pathname.startsWith(path)); + }, [location.pathname]); - if (uri.includes("/chat")) { - return true; - } - const enabledStartsWith = ["/stores", "/providers", "/vectors", "/chats", "/messages", "/usages", "/files"]; - if (enabledStartsWith.some(prefix => uri.startsWith(prefix))) { - return true; - } + const isWithoutCard = useCallback(() => { + return Setting.isMobile() || isHiddenHeaderAndFooter() || + location.pathname === "/chat" || location.pathname.startsWith("/chat/") || location.pathname === "/"; + }, [isHiddenHeaderAndFooter, location.pathname]); + const isStoreSelectEnabled = useCallback(() => { + const uri = location.pathname; + if (uri.includes("/chat")) {return true;} + const enabledPrefixes = ["/stores", "/providers", "/vectors", "/chats", "/messages", "/usages", "/files"]; + if (enabledPrefixes.some(prefix => uri.startsWith(prefix))) {return true;} if (uri === "/" || uri === "/home") { - if ( - Setting.isAnonymousUser(this.state.account) || - Setting.isChatUser(this.state.account) || - Setting.isAdminUser(this.state.account) || - Setting.isChatAdminUser(this.state.account) || - Setting.getUrlParam("isRaw") !== null - ) { + if (Setting.isAnonymousUser(account) || Setting.isChatUser(account) || + Setting.isAdminUser(account) || Setting.isChatAdminUser(account) || + Setting.getUrlParam("isRaw") !== null) { return true; } } return false; - } + }, [location.pathname, account]); - onClose = () => { - this.setState({ - menuVisible: false, - }); - }; + // Build sidebar nav items based on account + const renderSidebar = () => { + if (!account) {return null;} - showMenu = () => { - this.setState({ - menuVisible: true, - }); - }; + const isAdmin = Setting.isAdminUser(account); + const isChatAdmin = Setting.isChatAdminUser(account); - setThemeAlgorithm() { - const currentUrl = window.location.href; - const url = new URL(currentUrl); - const themeType = url.searchParams.get("theme"); - if (themeType === "dark" || themeType === "default") { - localStorage.setItem("themeAlgorithm", JSON.stringify([themeType])); - } - } - - setLogoAndThemeAlgorithm = (nextThemeAlgorithm) => { - this.setState({ - themeAlgorithm: nextThemeAlgorithm, - logo: Setting.getLogo(nextThemeAlgorithm, this.state.store?.logoUrl), - }); - localStorage.setItem("themeAlgorithm", JSON.stringify(nextThemeAlgorithm)); - }; - - renderAvatar() { - if (this.state.account.avatar === "") { + if (account.type?.startsWith("video-")) { return ( - - {Setting.getShortName(this.state.account.name)} - - ); - } else { - return ( - - {Setting.getShortName(this.state.account.name)} - - ); - } - } - - renderRightDropdown() { - if ((Setting.isAnonymousUser(this.state.account) && Conf.DisablePreviewMode) || Setting.getUrlParam("isRaw") !== null) { - return ( -
- { - this.renderAvatar() - } -   -   - {Setting.isMobile() ? null : Setting.getShortName(this.state.account.displayName)}   -   -   -   -
- ); - } - - const items = []; - if (!Setting.isAnonymousUser(this.state.account)) { - items.push(Setting.getItem(<>  {i18next.t("account:My Account")}, - "/account" - )); - items.push(Setting.getItem(<>  {i18next.t("general:Chats & Messages")}, - "/chat" - )); - items.push(Setting.getItem(<>  {i18next.t("account:Sign Out")}, - "/logout" - )); - } else { - items.push(Setting.getItem(<>  {i18next.t("account:Sign In")}, - "/login" - )); - } - const onClick = (e) => { - if (e.key === "/account") { - Setting.openLink(Setting.getMyProfileUrl(this.state.account)); - } else if (e.key === "/logout") { - this.signout(); - } else if (e.key === "/chat") { - this.props.history.push("/chat"); - } else if (e.key === "/login") { - this.props.history.push(window.location.pathname); - Setting.redirectToLogin(); - } - }; - - return ( - -
- { - this.renderAvatar() - } -   -   - {Setting.isMobile() ? null : Setting.getShortName(this.state.account.displayName)}   -   -   -   -
-
- ); - } - - renderAccountMenu() { - if (this.state.account === undefined) { - return null; - } else if (this.state.account === null) { - return ( - - - -
- -
-
- -
-
- ); - } else { - return ( - - {this.renderRightDropdown()} - - - {Setting.isLocalAdminUser(this.state.account) && - - } -
-
-
- + ); } - } - - navItemsIsAll() { - const navItems = this.state.store?.navItems; - return !navItems || navItems.includes("all"); - } - - filterMenuItems(menuItems, navItems) { - if (!navItems || navItems.includes("all")) { - return menuItems; - } - const filteredItems = menuItems.map(item => { - if (!Array.isArray(item.children)) { - return item; + if (!isAdmin && (Setting.isAnonymousUser(account) && !Conf.DisablePreviewMode)) { + if (!isChatAdmin) { + return ( + + ); } - - const filteredChildren = item.children.filter(child => { - return navItems.includes(child.key); - }); - - const newItem = {...item}; - newItem.children = filteredChildren; - return newItem; - }); - - return filteredItems.filter(item => { - return !Array.isArray(item.children) || item.children.length > 0; - }); - } - - getMenuItems() { - const res = []; - - res.push(Setting.getItem({i18next.t("general:Home")}, "/")); - - if (this.state.account === null || this.state.account === undefined) { - return []; } - const navItems = this.state.store?.navItems; - - if (this.state.account.type.startsWith("video-")) { - res.push(Setting.getItem({i18next.t("general:Videos")}, "/videos")); - // res.push(Setting.getItem({i18next.t("general:Public Videos")}, "/public-videos")); - - if (this.state.account.type === "video-admin-user") { - res.push(Setting.getItem( - + if (isChatAdmin && !isAdmin) { + return ( + + ); } - if (!Setting.isAdminUser(this.state.account) && (Setting.isAnonymousUser(this.state.account) && !Conf.DisablePreviewMode)) { // show complete menu for anonymous user in preview mode even not login - if (!Setting.isChatAdminUser(this.state.account)) { - // res.push(Setting.getItem({i18next.t("general:Usages")}, "/usages")); - return res; - } + if (Setting.isTaskUser(account)) { + return ( + + ); } - const domain = Setting.getSubdomain(); - // const domain = "med"; - - if (Conf.ShortcutPageItems.length > 0 && domain === "data") { - res.push(Setting.getItem({i18next.t("general:Stores")}, "/stores")); - res.push(Setting.getItem({i18next.t("general:Providers")}, "/providers")); - res.push(Setting.getItem({i18next.t("general:Nodes")}, "/nodes")); - res.push(Setting.getItem({i18next.t("general:Sessions")}, "/sessions")); - res.push(Setting.getItem({i18next.t("general:Connections")}, "/connections")); - res.push(Setting.getItem({i18next.t("general:Records")}, "/records")); - } else if (Conf.ShortcutPageItems.length > 0 && domain === "ai") { - res.push(Setting.getItem({i18next.t("general:Chat")}, "/chat")); - res.push(Setting.getItem({i18next.t("general:Stores")}, "/stores")); - res.push(Setting.getItem({i18next.t("general:Providers")}, "/providers")); - res.push(Setting.getItem({i18next.t("general:Vectors")}, "/vectors")); - res.push(Setting.getItem({i18next.t("general:Chats")}, "/chats")); - res.push(Setting.getItem({i18next.t("general:Messages")}, "/messages")); - res.push(Setting.getItem({i18next.t("general:Usages")}, "/usages")); - res.push(Setting.getItem({i18next.t("general:Activities")}, "/activities")); - // res.push(Setting.getItem({i18next.t("general:Tasks")}, "/tasks")); - // res.push(Setting.getItem({i18next.t("general:Articles")}, "/articles")); - } else if (Setting.isChatAdminUser(this.state.account)) { - res.push(Setting.getItem({i18next.t("general:Chat")}, "/chat")); - res.push(Setting.getItem({i18next.t("general:Stores")}, "/stores")); - res.push(Setting.getItem({i18next.t("general:Vectors")}, "/vectors")); - res.push(Setting.getItem({i18next.t("general:Chats")}, "/chats")); - res.push(Setting.getItem({i18next.t("general:Messages")}, "/messages")); - res.push(Setting.getItem({i18next.t("general:Usages")}, "/usages")); - res.push(Setting.getItem({i18next.t("general:Activities")}, "/activities")); - - if (window.location.pathname === "/") { - Setting.goToLinkSoft(this, "/chat"); - } - - res.push(Setting.getItem( - - {i18next.t("general:Users")} - {Setting.renderExternalLink()} - , - "#")); - - res.push(Setting.getItem( - - {i18next.t("general:Resources")} - {Setting.renderExternalLink()} - , - "##")); - - res.push(Setting.getItem( - - {i18next.t("general:Permissions")} - {Setting.renderExternalLink()} - , - "###")); - } else if (Setting.isTaskUser(this.state.account)) { - res.push(Setting.getItem({i18next.t("general:Tasks")}, "/tasks")); - - if (window.location.pathname === "/") { - Setting.goToLinkSoft(this, "/tasks"); - } - } else if (Conf.ShortcutPageItems.length > 0 && domain === "video") { - if (Conf.EnableExtraPages) { - res.push(Setting.getItem({i18next.t("general:Videos")}, "/videos")); - // res.push(Setting.getItem({i18next.t("general:Public Videos")}, "/public-videos")); - // res.push(Setting.getItem({i18next.t("general:Tasks")}, "/tasks")); - // res.push(Setting.getItem({i18next.t("general:Articles")}, "/articles")); - } - - if (window.location.pathname === "/") { - Setting.goToLinkSoft(this, "/videos"); - } - } else { - const textColor = this.state.themeAlgorithm.includes("dark") ? "white" : "black"; - const twoToneColor = this.state.themeData.colorPrimary; - - res.pop(); - - res.push(Setting.getItem({i18next.t("general:Home")}, "/home", , [ - Setting.getItem({i18next.t("general:Chat")}, "/chat"), - Setting.getItem({i18next.t("general:Usages")}, "/usages"), - Setting.getItem({i18next.t("general:Activities")}, "/activities"), - Setting.getItem({i18next.t("general:OS Desktop")}, "/desktop"), - ])); - - res.push(Setting.getItem({i18next.t("general:Chats & Messages")}, "/ai-chat", , [ - Setting.getItem({i18next.t("general:Chats")}, "/chats"), - Setting.getItem({i18next.t("general:Messages")}, "/messages"), - ])); - - res.push(Setting.getItem({i18next.t("general:AI Setting")}, "/ai-setting", , [ - Setting.getItem({i18next.t("general:Stores")}, "/stores"), - Setting.getItem({i18next.t("general:Files")}, "/files"), - Setting.getItem({i18next.t("general:Providers")}, "/providers"), - Setting.getItem({i18next.t("general:Vectors")}, "/vectors"), - ])); - - res.push(Setting.getItem({i18next.t("general:Cloud Resources")}, "/cloud", , [ - Setting.getItem({i18next.t("general:Templates")}, "/templates"), - Setting.getItem({i18next.t("general:Application Store")}, "/application-store"), - Setting.getItem({i18next.t("general:Applications")}, "/applications"), - Setting.getItem({i18next.t("general:Nodes")}, "/nodes"), - Setting.getItem({i18next.t("general:Machines")}, "/machines"), - Setting.getItem({i18next.t("general:Assets")}, "/assets"), - Setting.getItem({i18next.t("general:Images")}, "/images"), - Setting.getItem({i18next.t("general:Containers")}, "/containers"), - Setting.getItem({i18next.t("general:Pods")}, "/pods"), - Setting.getItem({i18next.t("general:Workbench")}, "workbench"), - ])); - - res.push(Setting.getItem({i18next.t("general:Multimedia")}, "/multimedia", , [ - Setting.getItem({i18next.t("general:Videos")}, "/videos"), - Setting.getItem({i18next.t("general:Public Videos")}, "/public-videos"), - Setting.getItem({i18next.t("general:Tasks")}, "/tasks"), - Setting.getItem({i18next.t("general:Forms")}, "/forms"), - Setting.getItem({i18next.t("general:Workflows")}, "/workflows"), - Setting.getItem({i18next.t("med:Hospitals")}, "/hospitals"), - Setting.getItem({i18next.t("med:Doctors")}, "/doctors"), - Setting.getItem({i18next.t("med:Patients")}, "/patients"), - Setting.getItem({i18next.t("med:Caases")}, "/caases"), - Setting.getItem({i18next.t("med:Consultations")}, "/consultations"), - Setting.getItem({i18next.t("general:Audit")}, "/audit"), - Setting.getItem({i18next.t("med:Medical Image Analysis")}, "/yolov8mi"), - Setting.getItem({i18next.t("med:Super Resolution")}, "/sr"), - Setting.getItem({i18next.t("general:Articles")}, "/articles"), - Setting.getItem({i18next.t("general:Graphs")}, "/graphs"), - Setting.getItem({i18next.t("general:Scans")}, "/scans"), - ])); - - res.push(Setting.getItem({i18next.t("general:Logging & Auditing")}, "/logs", , [ - Setting.getItem({i18next.t("general:Sessions")}, "/sessions"), - Setting.getItem({i18next.t("general:Connections")}, "/connections"), - Setting.getItem({i18next.t("general:Records")}, "/records"), - ])); - - res.push(Setting.getItem({i18next.t("general:Identity & Access Management")}, "/identity", , [ - Setting.getItem( - + // Full admin nav + return ( + + ); + }; - return this.filterMenuItems(res, navItems); + const renderAvatar = () => { + if (!account) {return null;} + if (account.avatar === "") { + return ( +
+ {Setting.getShortName(account.name)} +
+ ); } + return ( + + ); + }; - const sortedForms = this.state.forms.slice().sort((a, b) => { - return a.position.localeCompare(b.position); - }); - - sortedForms.forEach(form => { - const path = `/forms/${form.name}/data`; - res.push(Setting.getItem({form.displayName}, path)); - }); - - return res; - } + const renderUserMenu = () => { + if (account === undefined) {return null;} - renderHomeIfSignedIn(component) { - if (this.state.account !== null && this.state.account !== undefined) { - return ; - } else { - return component; + if (account === null) { + return ( + + ); } - } - renderSigninIfNotSignedIn(component) { - if (this.state.account === null) { - const signinUrl = Setting.getSigninUrl(); - if (signinUrl && signinUrl !== "") { - sessionStorage.setItem("from", window.location.pathname); - window.location.replace(signinUrl); - } else { - return null; - } - } else if (this.state.account === undefined) { - return null; - } else { - return component; - } - } + return ( +
+ {Setting.isLocalAdminUser(account) && ( + + )} + +
+ +
+ {!Setting.isAnonymousUser(account) && ( + <> + + +
+ + + )} + {Setting.isAnonymousUser(account) && ( + + )} +
+
+
+ ); + }; - renderRouter() { - if (this.state.account?.type.startsWith("video-")) { - if (window.location.pathname === "/") { - return ( - - ); + const renderRouter = () => { + if (account?.type?.startsWith("video-")) { + if (location.pathname === "/") { + return ; } } return ( - this.renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> - this.renderHomeIfSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> + renderHomeIfSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> } /> - } /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - } /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - this.renderSigninIfNotSignedIn()} /> - } />} /> + } /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + } /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + renderSigninIfNotSignedIn()} /> + ( +
+

404

+

{i18next.t("general:Sorry, the page you visited does not exist.")}

+ + {i18next.t("general:Back Home")} + +
+ )} />
); - } - - isWithoutCard() { - return Setting.isMobile() || this.isHiddenHeaderAndFooter() || window.location.pathname === "/chat" || window.location.pathname.startsWith("/chat/") || window.location.pathname === "/"; - } + }; - isHiddenHeaderAndFooter(uri) { - if (uri === undefined) { - uri = this.state.uri; - } - const hiddenPaths = ["/workbench", "/access"]; - for (const path of hiddenPaths) { - if (uri.startsWith(path)) { - return true; - } - } + // Raw mode or portal mode + if (Setting.getUrlParam("isRaw") !== null) { + return ( + <> + + + + ); } - renderContent() { - if (Setting.getUrlParam("isRaw") !== null) { - return ( - - ); - } else if (Setting.getSubdomain() === "portal") { - return ( - - ); - } - + if (Setting.getSubdomain() === "portal") { return ( - - {this.renderHeader()} - - {this.isWithoutCard() ? - this.renderRouter() : - - {this.renderRouter()} - - } - - {this.renderFooter()} - + <> + + + ); } - renderHeader() { - if (this.isHiddenHeaderAndFooter()) { - return null; - } - - const showMenu = () => { - this.setState({ - menuVisible: true, - }); - }; + const showShell = !isHiddenHeaderAndFooter(); + + return ( + <> + + {Setting.getHtmlTitle(store?.htmlTitle)} + + + + + + {showShell ? ( +
+ {/* Mobile sidebar overlay */} + {mobileSidebarOpen && ( +
setMobileSidebarOpen(false)} + /> + )} - const onClick = ({key}) => { - if (Setting.isMobile()) { - this.setState({ - menuVisible: false, - }); - } + {/* Sidebar */} + + + {/* Main content */} +
+ {/* Top bar */} +
+ +
+ {renderUserMenu()} +
+ + {/* Page content */} +
+ {isWithoutCard() ? ( + renderRouter() + ) : ( +
+
+ {renderRouter()} +
+
+ )} +
+
-
- {this.renderAccountMenu()} + ) : ( +
+ {renderRouter()}
- - ); - } - - renderFooter() { - if (this.isHiddenHeaderAndFooter()) { - return null; - } - // How to keep your footer where it belongs ? - // https://www.freecodecamp.org/news/how-to-keep-your-footer-where-it-belongs-59c6aa05c59c - - return ( - -
-
-
-
- ); - } - - renderPage() { - return ( - - {/* { */} - {/* this.renderBanner() */} - {/* } */} - - - { - this.renderContent() - } - - ); - } - - getAntdLocale() { - return { - Table: { - filterConfirm: i18next.t("general:OK"), - filterReset: i18next.t("general:Reset"), - filterEmptyText: i18next.t("general:No data"), - filterSearchPlaceholder: i18next.t("general:Search"), - emptyText: i18next.t("general:No data"), - selectAll: i18next.t("general:Select all"), - selectInvert: i18next.t("general:Invert selection"), - selectionAll: i18next.t("general:Select all data"), - sortTitle: i18next.t("general:Sort"), - expand: i18next.t("general:Expand row"), - collapse: i18next.t("general:Collapse row"), - triggerDesc: i18next.t("general:Click to sort descending"), - triggerAsc: i18next.t("general:Click to sort ascending"), - cancelSort: i18next.t("general:Click to cancel sorting"), - }, - }; - } - - render() { - return ( - - - {Setting.getHtmlTitle(this.state.store?.htmlTitle)} - - - - - { - this.renderPage() - } - - - - ); - } + )} + + ); } -export default withRouter(withTranslation()(App)); +export default App; diff --git a/web/src/ApplicationEditPage.js b/web/src/ApplicationEditPage.js index aeb9b55a..d5f52c84 100644 --- a/web/src/ApplicationEditPage.js +++ b/web/src/ApplicationEditPage.js @@ -13,7 +13,6 @@ // limitations under the License. import React from "react"; -import {Button, Card, Col, Input, Popconfirm, Row, Select} from "antd"; import * as ApplicationBackend from "./backend/ApplicationBackend"; import * as TemplateBackend from "./backend/TemplateBackend"; import * as Setting from "./Setting"; @@ -21,7 +20,6 @@ import i18next from "i18next"; import TemplateOptionTable from "./table/TemplateOptionTable"; import Editor from "./common/Editor"; -const {TextArea} = Input; class ApplicationEditPage extends React.Component { constructor(props) { @@ -143,51 +141,49 @@ class ApplicationEditPage extends React.Component { renderApplication() { return ( - +
{i18next.t("application:Edit Application")}     - - - {this.state.isNewApplication && } + + + {this.state.isNewApplication && }
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner"> - - +
+
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} : - - +
+
{ this.updateApplicationField("name", e.target.value); }} /> - - - - +
+
+
+
{Setting.getLabel(i18next.t("general:Display name"), i18next.t("general:Display name - Tooltip"))} : - - +
+
{ this.updateApplicationField("displayName", e.target.value); }} /> - - - - +
+
+
+
{Setting.getLabel(i18next.t("general:Description"), i18next.t("general:Description - Tooltip"))} : - - -