From a7773813ff37428562087d2e31f27b052e2532c3 Mon Sep 17 00:00:00 2001 From: Hanzo Dev Date: Tue, 17 Mar 2026 22:53:20 -0700 Subject: [PATCH 1/2] fix: make InitAuthConfig graceful instead of panicking on missing IAM app The cloud-api was crashing on startup (CrashLoopBackOff, 6/8 pods) because InitAuthConfig() panics when the IAM application or cert is not found. Changed all panic() calls to log warnings and return gracefully, allowing the service to start with auth features disabled. This lets pods stabilize while IAM configuration is being set up. Also adds billing usage record writer for invoice reconciliation. --- controllers/account.go | 12 +++-- controllers/openai_api.go | 4 +- controllers/zap_native.go | 92 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 5 deletions(-) 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) { From fb2f992ac64e195951297b4ee22874013b95f8fc Mon Sep 17 00:00:00 2001 From: Hanzo Dev Date: Fri, 20 Mar 2026 23:06:04 -0700 Subject: [PATCH 2/2] feat: replace Ant Design with Tailwind across entire Cloud frontend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 148 files changed, 7,314 insertions, 12,664 deletions. Foundation: - Added tailwindcss, lucide-react, sonner, clsx, tailwind-merge - Dark monochrome theme matching IAM - cn() utility, sonner toast wrapper App shell: - App.js: class → functional, Ant Layout/Sider/Menu → Tailwind sidebar - BaseListPage: Tailwind tables, native search - Setting: sonner toast, Tailwind badges - SystemInfo: Tailwind stat cards Chat system (25 files): - ChatPage, ChatBox, ChatInput, MessageItem: monochrome chat UI - Drawers → slide-over panels - All lucide-react icons Knowledge/Store (8 files): - Store and vector management pages All remaining pages (~90 files): - File, Provider, Graph, Node, Machine, Container, Pod - Article, Workflow, Task, Form, Record, Message, Video - All table sub-components - All common components 17 residual antd imports in deeply coupled pages. --- web/craco.config.js | 23 +- web/package.json | 34 +- web/postcss.config.js | 6 + web/src/ActivityPage.js | 92 +- web/src/AgentsPage.js | 8 +- web/src/App.js | 1535 ++++++++++-------------- web/src/ApplicationEditPage.js | 143 ++- web/src/ApplicationListPage.js | 60 +- web/src/ApplicationStorePage.js | 216 ++-- web/src/ApplicationViewPage.js | 116 +- web/src/ArticleEditPage.js | 102 +- web/src/ArticleListPage.js | 33 +- web/src/ArticleMenu.js | 1 - web/src/AssetEditPage.js | 204 ++-- web/src/AssetListPage.js | 37 +- web/src/AuthCallback.js | 182 ++- web/src/AvatarUpload.js | 25 +- web/src/BaseListPage.js | 96 +- web/src/BpmnComponent.js | 15 +- web/src/CaaseEditPage.js | 230 ++-- web/src/CaaseListPage.js | 33 +- web/src/ChatBox.js | 33 +- web/src/ChatEditPage.js | 263 ++-- web/src/ChatExampleQuestionIcon.js | 80 +- web/src/ChatExampleQuestions.js | 190 +-- web/src/ChatListPage.js | 331 ++--- web/src/ChatMenu.js | 367 +++--- web/src/ChatMessageRender.js | 49 +- web/src/ChatPage.js | 202 ++-- web/src/ConnectionListPage.js | 28 +- web/src/ConsultationEditPage.js | 96 +- web/src/ConsultationListPage.js | 18 +- web/src/ContainerEditPage.js | 181 ++- web/src/ContainerListPage.js | 29 +- web/src/DoctorEditPage.js | 116 +- web/src/DoctorListPage.js | 33 +- web/src/FileEditPage.js | 184 +-- web/src/FileListPage.js | 219 ++-- web/src/FileTree.js | 1088 ++++++----------- web/src/FileTreePage.js | 30 +- web/src/FileViewPage.js | 685 ++++------- web/src/FormDataPage.js | 4 +- web/src/FormDataTablePage.js | 29 +- web/src/FormEditPage.js | 157 ++- web/src/FormListPage.js | 37 +- web/src/GraphChatTable.js | 114 +- web/src/GraphEditPage.js | 389 ++---- web/src/GraphListPage.js | 281 ++--- web/src/HomePage.js | 62 +- web/src/HospitalEditPage.js | 45 +- web/src/HospitalListPage.js | 18 +- web/src/ImageEditPage.js | 253 +--- web/src/ImageListPage.js | 259 ++-- web/src/LanguageSelect.js | 82 +- web/src/MachineEditPage.js | 257 ++-- web/src/MachineListPage.js | 29 +- web/src/MemoTextArea.js | 15 +- web/src/MessageEditPage.js | 244 ++-- web/src/MessageListPage.js | 84 +- web/src/MultiPaneManager.js | 25 +- web/src/NodeEditPage.js | 235 ++-- web/src/NodeListPage.js | 32 +- web/src/NodeWorkbench.js | 19 +- web/src/OsDesktop.js | 33 +- web/src/PatientEditPage.js | 146 +-- web/src/PatientListPage.js | 18 +- web/src/PodEditPage.js | 133 +- web/src/PodListPage.js | 29 +- web/src/PreviewInterceptor.js | 7 +- web/src/Provider.js | 9 +- web/src/ProviderEditPage.js | 1483 ++++++----------------- web/src/ProviderListPage.js | 313 ++--- web/src/RecordEditPage.js | 293 +++-- web/src/RecordListPage.js | 88 +- web/src/ScanEditPage.js | 112 +- web/src/ScanListPage.js | 16 +- web/src/SessionListPage.js | 33 +- web/src/Setting.js | 117 +- web/src/SigninPage.js | 12 +- web/src/StoreEditPage.js | 730 ++--------- web/src/StoreInfoTitle.js | 199 ++- web/src/StoreListPage.js | 332 ++--- web/src/StoreSelect.js | 127 +- web/src/SystemInfo.js | 382 +++--- web/src/TaskAnalysisReport.js | 9 +- web/src/TaskEditPage.js | 212 ++-- web/src/TaskListPage.js | 63 +- web/src/TemplateEditPage.js | 132 +- web/src/TemplateListPage.js | 33 +- web/src/ThemeSelect.js | 121 +- web/src/UsagePage.js | 116 +- web/src/UsageTable.js | 26 +- web/src/VectorEditPage.js | 204 ++-- web/src/VectorListPage.js | 123 +- web/src/VectorTooltip.js | 41 +- web/src/VideoEditPage.js | 398 +++--- web/src/VideoListPage.js | 63 +- web/src/VideoPage.js | 30 +- web/src/VmPage.js | 8 +- web/src/WorkflowEditPage.js | 131 +- web/src/WorkflowListPage.js | 59 +- web/src/basic/GridCards.js | 25 +- web/src/basic/PublicVideoListPage.js | 13 +- web/src/basic/ShortcutsPage.js | 2 +- web/src/basic/SingleCard.js | 70 +- web/src/chat/ChatFileInput.js | 37 +- web/src/chat/ChatInput.js | 192 ++- web/src/chat/ChatInputMenu.js | 111 +- web/src/chat/KnowledgeSourcesDrawer.js | 168 +-- web/src/chat/MessageActions.js | 139 +-- web/src/chat/MessageEdit.js | 93 +- web/src/chat/MessageItem.js | 523 +++----- web/src/chat/MessageList.js | 131 +- web/src/chat/MessageSuggestions.js | 23 +- web/src/chat/SearchSourcesDrawer.js | 127 +- web/src/common/ChatWidget.js | 28 +- web/src/common/JsonCodeMirrorWidget.js | 8 +- web/src/common/ScanDetailPopover.js | 14 +- web/src/common/ScanResultPopover.js | 8 +- web/src/common/ScanResultRenderer.js | 5 +- web/src/common/ScanTable.js | 3 +- web/src/common/TestEmbedWidget.js | 34 +- web/src/common/TestModelWidget.js | 17 +- web/src/common/TestScanWidget.js | 151 +-- web/src/common/TestTtsWidget.js | 21 +- web/src/globals.css | 86 ++ web/src/index.js | 10 +- web/src/lib/toast.js | 30 + web/src/lib/utils.js | 6 + web/src/modal/ConnectModal.js | 106 +- web/src/modal/PopconfirmModal.js | 22 +- web/src/table/ArticleTable.js | 64 +- web/src/table/CredentialTable.js | 24 +- web/src/table/DeploymentTable.js | 24 +- web/src/table/EventTable.js | 55 +- web/src/table/ExampleQuestionTable.js | 49 +- web/src/table/FileTable.js | 43 +- web/src/table/FormItemTable.js | 78 +- web/src/table/LabelTable.js | 59 +- web/src/table/McpToolsTable.js | 23 +- web/src/table/PrometheusInfoTable.js | 122 +- web/src/table/RemarkTable.js | 51 +- web/src/table/RemoteAppTable.js | 37 +- web/src/table/ServiceTable.js | 50 +- web/src/table/TagTable.js | 36 +- web/src/table/TemplateOptionTable.js | 70 +- web/tailwind.config.ts | 56 + web/yarn.lock | 470 ++++---- 148 files changed, 7314 insertions(+), 12664 deletions(-) create mode 100644 web/postcss.config.js create mode 100644 web/src/globals.css create mode 100644 web/src/lib/toast.js create mode 100644 web/src/lib/utils.js create mode 100644 web/tailwind.config.ts 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"))} : - - -