Initial commit of HAMi-WebUI source code

main
Nimbus318 11 months ago
commit 7b9df19fbc

@ -0,0 +1,2 @@
packages/
packages_back/

@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11

@ -0,0 +1,7 @@
public
dist
packages/web/build/*.js
packages/web/src/assets
packages/web/public
packages/web/dist

@ -0,0 +1,198 @@
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module'
},
env: {
browser: true,
node: true,
es6: true
},
extends: ['plugin:vue/recommended', 'eslint:recommended'],
// add your custom rules here
// it is base on https://github.com/vuejs/eslint-config-vue
rules: {
'vue/max-attributes-per-line': [2, {
'singleline': 10,
'multiline': {
'max': 1,
'allowFirstLine': false
}
}],
'vue/singleline-html-element-content-newline': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/name-property-casing': ['error', 'PascalCase'],
'vue/no-v-html': 'off',
'accessor-pairs': 2,
'arrow-spacing': [2, {
'before': true,
'after': true
}],
'block-spacing': [2, 'always'],
'brace-style': [2, '1tbs', {
'allowSingleLine': true
}],
'camelcase': [0, {
'properties': 'always'
}],
'comma-dangle': [2, 'never'],
'comma-spacing': [2, {
'before': false,
'after': true
}],
'comma-style': [2, 'last'],
'constructor-super': 2,
'curly': [2, 'multi-line'],
'dot-location': [2, 'property'],
'eol-last': 2,
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
'generator-star-spacing': [2, {
'before': true,
'after': true
}],
'handle-callback-err': [2, '^(err|error)$'],
'indent': [2, 2, {
'SwitchCase': 1
}],
'jsx-quotes': [2, 'prefer-single'],
'key-spacing': [2, {
'beforeColon': false,
'afterColon': true
}],
'keyword-spacing': [2, {
'before': true,
'after': true
}],
'new-cap': [2, {
'newIsCap': true,
'capIsNew': false
}],
'new-parens': 2,
'no-array-constructor': 2,
'no-caller': 2,
'no-console': 'off',
'no-class-assign': 2,
'no-cond-assign': 2,
'no-const-assign': 2,
'no-control-regex': 0,
'no-delete-var': 2,
'no-dupe-args': 2,
'no-dupe-class-members': 2,
'no-dupe-keys': 2,
'no-duplicate-case': 2,
'no-empty-character-class': 2,
'no-empty-pattern': 2,
'no-eval': 2,
'no-ex-assign': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-extra-boolean-cast': 2,
'no-extra-parens': [2, 'functions'],
'no-fallthrough': 2,
'no-floating-decimal': 2,
'no-func-assign': 2,
'no-implied-eval': 2,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': [2, {
'allowLoop': false,
'allowSwitch': false
}],
'no-lone-blocks': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-multiple-empty-lines': [2, {
'max': 1
}],
'no-native-reassign': 2,
'no-negated-in-lhs': 2,
'no-new-object': 2,
'no-new-require': 2,
'no-new-symbol': 2,
'no-new-wrappers': 2,
'no-obj-calls': 2,
'no-octal': 2,
'no-octal-escape': 2,
'no-path-concat': 2,
'no-proto': 2,
'no-redeclare': 2,
'no-regex-spaces': 2,
'no-return-assign': [2, 'except-parens'],
'no-self-assign': 2,
'no-self-compare': 2,
'no-sequences': 2,
'no-shadow-restricted-names': 2,
'no-spaced-func': 2,
'no-sparse-arrays': 2,
'no-this-before-super': 2,
'no-throw-literal': 2,
'no-trailing-spaces': 2,
'no-undef': 2,
'no-undef-init': 2,
'no-unexpected-multiline': 2,
'no-unmodified-loop-condition': 2,
'no-unneeded-ternary': [2, {
'defaultAssignment': false
}],
'no-unreachable': 2,
'no-unsafe-finally': 2,
'no-unused-vars': [2, {
'vars': 'all',
'args': 'none'
}],
'no-useless-call': 2,
'no-useless-computed-key': 2,
'no-useless-constructor': 2,
'no-useless-escape': 0,
'no-whitespace-before-property': 2,
'no-with': 2,
'one-var': [2, {
'initialized': 'never'
}],
'operator-linebreak': [2, 'after', {
'overrides': {
'?': 'before',
':': 'before'
}
}],
'padded-blocks': [2, 'never'],
'quotes': [2, 'single', {
'avoidEscape': true,
'allowTemplateLiterals': true
}],
'semi': [2, 'never'],
'semi-spacing': [2, {
'before': false,
'after': true
}],
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [2, {
'words': true,
'nonwords': false
}],
'spaced-comment': [2, 'always', {
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}],
'template-curly-spacing': [2, 'never'],
'use-isnan': 2,
'valid-typeof': 2,
'wrap-iife': [2, 'any'],
'yield-star-spacing': [2, 'both'],
'yoda': [2, 'never'],
'prefer-const': 2,
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'object-curly-spacing': [2, 'always', {
objectsInObjects: false
}],
'array-bracket-spacing': [2, 'never']
}
}

39
.gitignore vendored

@ -0,0 +1,39 @@
# compiled output
/dist
/node_modules
/public
/packages/web/node_modules
/.pnpm-store
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.tgz

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

@ -0,0 +1,51 @@
# Contributing to HAMi-WebUI
Thank you for your interest in contributing to HAMi-WebUI! We welcome all people who want to contribute in a healthy and constructive manner within our community.
This document is a guide to help you through the process of contributing to HAMi-WebUI.
## Become a contributor
You can contribute to HAMi-WebUI in several ways. Here are some examples:
- Contribute to the HAMi-WebUI codebase.
- Report and triage bugs.
- Write technical documentation and blog posts, for users and contributors.
- Help others by answering questions about HAMi-WebUI.
For more ways to contribute, check out the [Open Source Guides](https://opensource.guide/how-to-contribute/).
### Report bugs
Before submitting a new issue, try to make sure someone hasn't already reported the problem. Look through the [existing issues](https://github.com/Project-HAMi/HAMi-UI/issues) for similar issues.
Report a bug by submitting a [bug report](https://github.com/Project-HAMi/HAMi-UI/issues/new?labels=type%3A+bug&template=1-bug_report.md). Make sure that you provide as much information as possible on how to reproduce the bug.
For authentication and alerting HAMi-WebUI server logs are useful.
### Suggest enhancements
If you have an idea of how to improve HAMi-WebUI, submit a [feature request](https://github.com/Project-HAMi/HAMi-UI/issues/new?assignees=&labels=type%2Ffeature-request&projects=&template=1-feature_requests.md).
We want to make HAMi-WebUI accessible to even more people. Submit an [accessibility issue](https://github.com/Project-HAMi/HAMi-UI/issues/new?labels=type%3A+accessibility&template=3-accessibility.md) to help us understand what we can improve.
### Write documentation
We welcome your expertise and input as our body of technical content grows.
### Your first contribution
Unsure where to begin contributing to HAMi-WebUI? Start by browsing issues labeled `beginner friendly` or `help wanted`.
- [Beginner-friendly](https://github.com/Project-HAMi/HAMi-UI/issues?q=is%3Aopen+is%3Aissue+label%3A%22beginner+friendly%22) issues are generally straightforward to complete.
- [Help wanted](https://github.com/Project-HAMi/HAMi-UI/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) issues are problems we would like the community to help us with regardless of complexity.
If you're looking to make a code change, see how to set up your environment for [local development](docs/contribute/developer-guide.md).
When you're ready to contribute, it's time to create a pull request.
## Where do I go from here?
- Set up your [development environment](docs/contribute/developer-guide.md).

@ -0,0 +1,15 @@
FROM node:21.6.2 AS builder
WORKDIR /src
RUN npm install -g pnpm
COPY . .
RUN make build-all
FROM node:21.6.2-slim
COPY --from=builder /src/dist/ /apps/dist/
COPY --from=builder /src/node_modules/ /apps/node_modules/
COPY --from=builder /src/public/ /apps/public/

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@ -0,0 +1,51 @@
VERSION?=latest
DOCKER_IMAGE=projecthami/hami-webui-fe
OUT=./dist
PROJECT_NAME?=test-project
# 按项目最小化构建
ROUTE_FILE=packages/web/src/router/index.js
PROJECT_PATH=packages/web/projects/
DISABLED_PROJECTS?=""
.PHONY: install-modules
install-modules:
pnpm install
.PHONY: build-all
build-all: install-modules build-bff build-web
.PHONY: build-bff
build-bff:
pnpm run build
.PHONY: build-web
build-web:
cd packages/web && pnpm run build
.PHONY: start-dev
start-dev: install-modules start-bff start-web
.PHONY: start-bff
start-bff:
pnpm run start:dev &
.PHONY: start-web
start-web:
cd packages/web && pnpm run start:dev
.PHONY: start-prod
start-prod:
pnpm run start:prod
.PHONY: build-image
build-image:
docker build --platform linux/amd64 -t ${DOCKER_IMAGE}:${VERSION} .
.PHONY: push-image
push-image:
docker push ${DOCKER_IMAGE}:${VERSION}
.PHONY: release
release: build-image push-image

@ -0,0 +1,37 @@
<img src="docs/logo-horizontal.png" alt="HAMi-WebUI Logo (Light)" width="50%">
English | [简体中文](README_ZH.md)
An open-source platform for managing and observing GPU resources, based on the HAMi project
[![License](https://img.shields.io/github/license/Project-HAMi/HAMi)](LICENSE)
HAMi-WebUI is built upon the [HAMi](https://github.com/Project-HAMi/HAMi) open-source project. It extends HAMi by providing an intuitive web interface for visualizing and managing GPU resource allocation and usage across nodes. The platform supports detailed views for tasks and GPUs, enabling teams to monitor resource consumption effectively.
- **Resource Overview:** Provides a comprehensive view of all resources, including node and GPU usage. Quickly assess the status of all nodes and GPUs.
- **Node Management:** Explore detailed node information, including node status, resource usage, and availability.
- **GPU Management:** Visualize the GPU usage on each node, providing detailed insights into the allocation and usage of compute power and memory.
- **Task Management:** Track tasks and their resource consumption. View task creation times, status, GPU allocations, and more.
## Get started
- [Installation guide](docs/installation/helm/index.md)
## Contributing
If you're interested in contributing to HAMi-WebUI:
- Start by reading the [Contributing guide](CONTRIBUTING.md).
- Set up your local environment by following our [Developer guide](docs/contribute/developer-guide.md).
- Explore [beginner-friendly issues](https://github.com/Project-HAMi/HAMi-WebUI/issues?q=is%3Aopen+is%3Aissue+label%3A%22beginner+friendly%22).
## Get involved
- Regular Community Meeting: Friday at 16:00 UTC+8 (Chinese)(weekly). [Convert to your timezone](https://www.thetimezoneconverter.com/?t=14%3A30&tz=GMT%2B8&).
- [Meeting Notes and Agenda](https://docs.google.com/document/d/1YC6hco03_oXbF9IOUPJ29VWEddmITIKIfSmBX8JtGBw/edit#heading=h.g61sgp7w0d0c)
- [Meeting Link](https://meeting.tencent.com/dm/Ntiwq1BICD1P)
- Join our community on [Slack](https://join.slack.com/t/hami-hsf3791/shared_invite/zt-2gcteqiph-Ls8Atnpky6clrspCAQ_eGQ).
## License
HAMi-WebUI is distributed under [Apache-2.0](LICENSE). For details, see [LICENSE](LICENSE).

@ -0,0 +1,38 @@
<img src="docs/logo-horizontal.png" alt="HAMi-WebUI Logo (Light)" width="50%">
[English](README.md) | 简体中文
基于 HAMi 项目的开源 GPU 资源管理与监控平台
[![License](https://img.shields.io/github/license/Project-HAMi/HAMi)](LICENSE)
HAMi-WebUI 是基于 [HAMi](https://github.com/Project-HAMi/HAMi) 开源项目构建的。它通过提供一个直观的 Web 界面来扩展 HAMi 的功能,帮助用户可视化和管理节点上的 GPU 资源分配和使用情况。该平台支持任务和显卡的详细视图,使团队能够高效地监控资源消耗。
- **资源概览:** 提供所有资源的综合视图,包括节点和显卡的资源使用情况。快速评估所有节点和显卡的状态。
- **节点管理:** 浏览详细的节点信息,包括节点状态、资源使用情况。
- **显卡管理:** 可视化各节点的显卡使用情况,详细展示算力与显存的分配与使用。
- **任务管理:** 追踪任务及其资源消耗。查看任务创建时间、状态、显卡分配等信息。
## 开始使用
- [安装指南](docs/installation/helm/index.md)
## 参与贡献
如果你对 HAMi-WebUI 项目感兴趣:
- 首先阅读[贡献指南](CONTRIBUTING.md)。
- 按照我们的[开发者指南](docs/contribute/developer-guide.md)设置本地开发环境。
- 查看[适合初学者的问题](https://github.com/Project-HAMi/HAMi-WebUI/issues?q=is%3Aopen+is%3Aissue+label%3A%22beginner+friendly%22)。
## 参与社区
- 周例会: Friday at 16:00 UTC+8 (Chinese)(weekly).
- [Meeting Notes and Agenda](https://docs.google.com/document/d/1YC6hco03_oXbF9IOUPJ29VWEddmITIKIfSmBX8JtGBw/edit#heading=h.g61sgp7w0d0c)
- [Meeting Link](https://meeting.tencent.com/dm/Ntiwq1BICD1P)
- 加入我们的 [Slack 社区](https://join.slack.com/t/hami-hsf3791/shared_invite/zt-2gcteqiph-Ls8Atnpky6clrspCAQ_eGQ)。
## 许可证
HAMi-WebUI 根据 [Apache-2.0](LICENSE) 许可证分发。如需了解详细情况,请参阅 [LICENSE](LICENSE)。

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

@ -0,0 +1,9 @@
dependencies:
- name: dcgm-exporter
repository: https://nvidia.github.io/dcgm-exporter/helm-charts
version: 3.5.0
- name: kube-prometheus-stack
repository: https://prometheus-community.github.io/helm-charts
version: 62.6.0
digest: sha256:31ae87f1891641569ab12218f9a5b4b6f475c2437ed22a5141f431437afb9abf
generated: "2024-09-11T11:57:17.364742+08:00"

@ -0,0 +1,34 @@
apiVersion: v2
name: hami-webui
description: A Helm chart for Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 1.0.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.0.0"
dependencies:
- condition: dcgm-exporter.enabled
name: dcgm-exporter
repository: https://nvidia.github.io/dcgm-exporter/helm-charts
version: 3.5.0
- condition: kube-prometheus-stack.enabled
name: kube-prometheus-stack
repository: https://prometheus-community.github.io/helm-charts
version: 62.6.0

@ -0,0 +1,184 @@
# HAMi-WebUI
![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.0](https://img.shields.io/badge/AppVersion-1.0.0-informational?style=flat-square)
## Get Repo Info
```console
helm repo add hami-charts https://project-hami.github.io/HAMi/
helm repo update
```
_See [helm repo](https://helm.sh/docs/helm/helm_repo/) for command documentation._
## Installing the Chart
To install the chart with the release name `my-release`:
```console
helm install hami-webui hami-charts/hami-webui
```
## Uninstalling the Chart
To uninstall/delete the my-release deployment:
```console
helm delete hami-webui
```
The command removes all the Kubernetes components associated with the chart and deletes the release.
## Requirements
| Repository | Name | Version |
|------------|------|---------|
| https://nvidia.github.io/dcgm-exporter/helm-charts | dcgm-exporter | 3.5.0 |
| https://prometheus-community.github.io/helm-charts | kube-prometheus-stack | 62.6.0 |
## Values
| Key | Type | Default | Description |
|-----|------|------------------------------------------------------------------------------------|-------------|
| affinity | object | `{}` | |
| dcgm-exporter.enabled | bool | `true` | |
| dcgm-exporter.nodeSelector.gpu | string | `"on"` | |
| dcgm-exporter.serviceMonitor.additionalLabels.jobRelease | string | `"hami-webui-prometheus"` | |
| dcgm-exporter.serviceMonitor.enabled | bool | `true` | |
| dcgm-exporter.serviceMonitor.honorLabels | bool | `false` | |
| dcgm-exporter.serviceMonitor.interval | string | `"15s"` | |
| externalPrometheus.address | string | `"http://prometheus-kube-prometheus-prometheus.prometheus.svc.cluster.local:9090"` | |
| externalPrometheus.enabled | bool | `false` | |
| fullnameOverride | string | `""` | |
| hamiServiceMonitor.additionalLabels.jobRelease | string | `"hami-webui-prometheus"` | |
| hamiServiceMonitor.enabled | bool | `true` | |
| hamiServiceMonitor.honorLabels | bool | `false` | |
| hamiServiceMonitor.interval | string | `"15s"` | |
| hamiServiceMonitor.relabelings | list | `[]` | |
| hamiServiceMonitor.svcNamespace | string | "kube-system" | Default is "kube-system", but it should be set according to the namespace where the HAMi components are installed. || image.backend.pullPolicy | string | `"IfNotPresent"` | |
| image.backend.repository | string | `"projecthami/hami-webui-be-oss"` | |
| image.backend.tag | string | `"v1.0.0"` | |
| image.frontend.pullPolicy | string | `"IfNotPresent"` | |
| image.frontend.repository | string | `"projecthami/hami-webui-fe-oss"` | |
| image.frontend.tag | string | `"v1.0.0"` | |
| imagePullSecrets | list | `[]` | |
| ingress.annotations | object | `{}` | |
| ingress.className | string | `""` | |
| ingress.enabled | bool | `false` | |
| ingress.hosts[0].host | string | `"chart-example.local"` | |
| ingress.hosts[0].paths[0].path | string | `"/"` | |
| ingress.hosts[0].paths[0].pathType | string | `"ImplementationSpecific"` | |
| ingress.tls | list | `[]` | |
| kube-prometheus-stack.alertmanager.enabled | bool | `false` | |
| kube-prometheus-stack.crds.enabled | bool | `false` | |
| kube-prometheus-stack.defaultRules.create | bool | `false` | |
| kube-prometheus-stack.enabled | bool | `true` | |
| kube-prometheus-stack.grafana.enabled | bool | `false` | |
| kube-prometheus-stack.kubernetesServiceMonitors.enabled | bool | `false` | |
| kube-prometheus-stack.nodeExporter.enabled | bool | `false` | |
| kube-prometheus-stack.prometheus.prometheusSpec.serviceMonitorSelector.matchLabels.jobRelease | string | `"hami-webui-prometheus"` | |
| kube-prometheus-stack.prometheusOperator.enabled | bool | `false` | |
| nameOverride | string | `""` | |
| namespaceOverride | string | `""` | |
| nodeSelector | object | `{}` | |
| podAnnotations | object | `{}` | |
| podSecurityContext | object | `{}` | |
| replicaCount | int | `1` | |
| resources.backend.limits.cpu | string | `"50m"` | |
| resources.backend.limits.memory | string | `"250Mi"` | |
| resources.backend.requests.cpu | string | `"50m"` | |
| resources.backend.requests.memory | string | `"250Mi"` | |
| resources.frontend.limits.cpu | string | `"200m"` | |
| resources.frontend.limits.memory | string | `"500Mi"` | |
| resources.frontend.requests.cpu | string | `"200m"` | |
| resources.frontend.requests.memory | string | `"500Mi"` | |
| securityContext | object | `{}` | |
| service.port | int | `3000` | |
| service.type | string | `"ClusterIP"` | |
| serviceAccount.annotations | object | `{}` | |
| serviceAccount.create | bool | `true` | |
| serviceAccount.name | string | `""` | |
| serviceMonitor.additionalLabels.jobRelease | string | `"hami-webui-prometheus"` | |
| serviceMonitor.enabled | bool | `true` | |
| serviceMonitor.honorLabels | bool | `false` | |
| serviceMonitor.interval | string | `"15s"` | |
| serviceMonitor.relabelings | list | `[]` | |
| tolerations | list | `[]` | |
## Configuration Guide for HAMi-WebUI Helm Chart
### 1. About `dcgm-exporter`
If `dcgm-exporter` is already installed in your cluster, you should disable it by modifying the following setting:
```yaml
dcgm-exporter:
enabled: false
```
This ensures that the existing `dcgm-exporter` instance is used, preventing conflicts.
### 2. About `Prometheus`
#### Scenario 1: If an existing Prometheus is available in your cluster
If your cluster already has a working Prometheus instance, you can enable the external Prometheus configuration and provide the correct address:
```yaml
externalPrometheus:
enabled: true
address: "<your-prometheus-address>"
```
Here, replace <your-prometheus-address> with the actual domain or internal Ingress address for your Prometheus instance.
#### Scenario 2: If no Prometheus or Operator is installed in the cluster
If there is no existing Prometheus or Prometheus Operator in your cluster, you can enable the kube-prometheus-stack to deploy Prometheus:
```yaml
kube-prometheus-stack:
enabled: true
crds:
enabled: true
...
prometheusOperator:
enabled: true
...
```
#### Scenario 3: If Prometheus and Operator already exist, but a separate Prometheus instance is needed
If your cluster has Prometheus and Prometheus Operator, but you want to use a separate instance without affecting the existing setup, modify the configuration as follows:
```yaml
kube-prometheus-stack:
enabled: true
...
```
This allows you to reuse the existing Operator and CRDs while deploying a new Prometheus instance.
### 3. About `jobRelease` Labels
If deploying a completely new Prometheus, you can leave the default `jobRelease: hami-webui-prometheus` unchanged.
***However, if you are integrating with an existing Prometheus instance and modifying the `prometheusSpec.serviceMonitorSelector.matchLabels`, ensure that **all** corresponding `...ServiceMonitor.additionalLabels` are updated to reflect the correct label.***
For example, if you modify:
```yaml
prometheus:
prometheusSpec:
serviceMonitorSelector:
matchLabels:
<jobRelease-label-key>: <jobRelease-label-value>
```
You must also modify all ...ServiceMonitor.additionalLabels in your values.yaml file to match:
```yaml
...ServiceMonitor:
additionalLabels:
<jobRelease-label-key>: <jobRelease-label-value>
```
This ensures that Prometheus will correctly discover all the ServiceMonitor configurations based on the updated labels.

@ -0,0 +1,22 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "hami-webui.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "hami-webui.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "hami-webui.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "hami-webui.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:3000 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 3000:$CONTAINER_PORT
{{- end }}

@ -0,0 +1,73 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "hami-webui.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "hami-webui.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Allow the release namespace to be overridden for multi-namespace deployments in combined charts
*/}}
{{- define "hami-webui.namespace" -}}
{{- if .Values.namespaceOverride -}}
{{- .Values.namespaceOverride -}}
{{- else -}}
{{- .Release.Namespace -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "hami-webui.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "hami-webui.labels" -}}
helm.sh/chart: {{ include "hami-webui.chart" . }}
{{ include "hami-webui.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "hami-webui.selectorLabels" -}}
app.kubernetes.io/name: {{ include "hami-webui.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "hami-webui.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "hami-webui.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

@ -0,0 +1,17 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "hami-webui.fullname" . }}-config
namespace: {{ include "hami-webui.namespace" . }}
data:
config.yaml: |
server:
http:
addr: 0.0.0.0:8000
timeout: 1s
grpc:
addr: 0.0.0.0:9000
timeout: 1s
prometheus:
address: {{ ternary .Values.externalPrometheus.address (printf "http://%s-kube-prometh-prometheus.%s.svc.cluster.local:9090" (include "hami-webui.fullname" .) (include "hami-webui.namespace" .)) .Values.externalPrometheus.enabled }}
timeout: 1m

@ -0,0 +1,81 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "hami-webui.fullname" . }}
namespace: {{ include "hami-webui.namespace" . }}
labels:
{{- include "hami-webui.labels" . | nindent 4 }}
app.kubernetes.io/component: "hami-webui"
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "hami-webui.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: "hami-webui"
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "hami-webui.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: "hami-webui"
spec:
serviceAccountName: {{ include "hami-webui.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}-fe-oss
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.frontend.repository }}:{{ .Values.image.frontend.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.frontend.pullPolicy }}
env:
- name: TZ
value: "Asia/Shanghai"
ports:
- name: http
containerPort: 3000
protocol: TCP
command:
- "node"
args:
- "/apps/dist/main"
resources:
{{- toYaml .Values.resources.frontend | nindent 12 }}
- name: {{ .Chart.Name }}-be-oss
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.backend.repository }}:{{ .Values.image.backend.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.backend.pullPolicy }}
ports:
- name: metrics
containerPort: 8000
protocol: TCP
command:
- "/apps/server"
args:
- "--conf"
- "/apps/config/config.yaml"
resources:
{{- toYaml .Values.resources.backend | nindent 12 }}
volumeMounts:
- name: config
mountPath: /apps/config/
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
- name: config
configMap:
name: {{ include "hami-webui.fullname" . }}-config

@ -0,0 +1,27 @@
{{- if .Values.hamiServiceMonitor.enabled }}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ include "hami-webui.fullname" . }}-hami-svc-monitor
namespace: {{ include "hami-webui.namespace" . }}
labels:
{{- include "hami-webui.labels" . | nindent 4 }}
app.kubernetes.io/component: "hami-webui"
{{- if .Values.hamiServiceMonitor.additionalLabels }}
{{- toYaml .Values.hamiServiceMonitor.additionalLabels | nindent 4 }}
{{- end }}
spec:
selector:
matchLabels:
app.kubernetes.io/component: hami-device-plugin
namespaceSelector:
matchNames:
- "{{ .Values.hamiServiceMonitor.svcNamespace }}"
endpoints:
- path: /metrics
port: monitorport
interval: "{{ .Values.hamiServiceMonitor.interval }}"
honorLabels: {{ .Values.hamiServiceMonitor.honorLabels }}
relabelings:
{{ toYaml .Values.hamiServiceMonitor.relabelings | nindent 6 }}
{{- end -}}

@ -0,0 +1,63 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "hami-webui.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
namespace: {{ include "hami-webui.namespace" . }}
labels:
{{- include "hami-webui.labels" . | nindent 4 }}
app.kubernetes.io/component: "hami-webui"
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- else }}
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

@ -0,0 +1,15 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: hami-webui-reader
namespace: {{ include "hami-webui.namespace" . }}
labels:
{{- include "hami-webui.labels" . | nindent 4 }}
app.kubernetes.io/component: "hami-webui"
rules:
- apiGroups: [ "" ]
resources: [ "nodes" ]
verbs: [ "get", "list", "watch" ]
- apiGroups: [ "" ]
resources: [ "pods" ]
verbs: [ "get", "list", "watch" ]

@ -0,0 +1,16 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "hami-webui.fullname" . }}
namespace: {{ include "hami-webui.namespace" . }}
labels:
{{- include "hami-webui.labels" . | nindent 4 }}
app.kubernetes.io/component: "hami-webui"
subjects:
- kind: ServiceAccount
name: {{ include "hami-webui.serviceAccountName" . }}
namespace: {{ include "hami-webui.namespace" . }}
roleRef:
kind: ClusterRole
name: hami-webui-reader
apiGroup: rbac.authorization.k8s.io

@ -0,0 +1,22 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "hami-webui.fullname" . }}
namespace: {{ include "hami-webui.namespace" . }}
labels:
{{- include "hami-webui.labels" . | nindent 4 }}
app.kubernetes.io/component: "hami-webui"
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
- port: 8000
targetPort: metrics
protocol: TCP
name: metrics
selector:
{{- include "hami-webui.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: "hami-webui"

@ -0,0 +1,14 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "hami-webui.serviceAccountName" . }}
namespace: {{ include "hami-webui.namespace" . }}
labels:
{{- include "hami-webui.labels" . | nindent 4 }}
app.kubernetes.io/component: "hami-webui"
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

@ -0,0 +1,42 @@
{{- if .Values.serviceMonitor.enabled }}
# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ include "hami-webui.fullname" . }}-svc-monitor
namespace: {{ include "hami-webui.namespace" . }}
labels:
{{- include "hami-webui.labels" . | nindent 4 }}
app.kubernetes.io/component: "hami-webui"
{{- if .Values.serviceMonitor.additionalLabels }}
{{- toYaml .Values.serviceMonitor.additionalLabels | nindent 4 }}
{{- end }}
spec:
selector:
matchLabels:
{{- include "hami-webui.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: "hami-webui"
namespaceSelector:
matchNames:
- "{{ include "hami-webui.namespace" . }}"
endpoints:
- port: "metrics"
path: "/metrics"
interval: "{{ .Values.serviceMonitor.interval }}"
honorLabels: {{ .Values.serviceMonitor.honorLabels }}
relabelings:
{{ toYaml .Values.serviceMonitor.relabelings | nindent 6 }}
{{- end -}}

@ -0,0 +1,150 @@
# Default values for hami-webui.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
frontend:
repository: projecthami/hami-webui-fe-oss
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: "v1.0.0"
backend:
repository: projecthami/hami-webui-be-oss
pullPolicy: IfNotPresent
tag: "v1.0.0"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
namespaceOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: ClusterIP
port: 3000
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
resources:
frontend:
limits:
cpu: 200m
memory: 500Mi
requests:
cpu: 200m
memory: 500Mi
backend:
limits:
cpu: 50m
memory: 250Mi
requests:
cpu: 50m
memory: 250Mi
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
serviceMonitor:
enabled: true
interval: 15s
honorLabels: false
additionalLabels:
jobRelease: hami-webui-prometheus
relabelings: []
hamiServiceMonitor:
enabled: true
interval: 15s
honorLabels: false
additionalLabels:
jobRelease: hami-webui-prometheus
svcNamespace: kube-system
relabelings: []
nodeSelector: {}
tolerations: []
affinity: {}
dcgm-exporter:
enabled: true
serviceMonitor:
enabled: true
interval: 15s
honorLabels: false
additionalLabels:
jobRelease: hami-webui-prometheus
nodeSelector:
gpu: "on"
kube-prometheus-stack:
enabled: true
crds:
enabled: false
defaultRules:
create: false
alertmanager:
enabled: false
grafana:
enabled: false
kubernetesServiceMonitors:
enabled: false
nodeExporter:
enabled: false
prometheusOperator:
enabled: false
prometheus:
prometheusSpec:
serviceMonitorSelector:
matchLabels:
jobRelease: hami-webui-prometheus
externalPrometheus:
enabled: false
# If externalPrometheus.enabled is true, this address will be used
address: "http://prometheus-kube-prometheus-prometheus.prometheus.svc.cluster.local:9090"

@ -0,0 +1,69 @@
# Developer guide
This guide helps you get started developing HAMi-WebUI.
## Dependencies
Make sure you have the following dependencies installed before setting up your developer environment:
- [Git](https://git-scm.com/)
- [Go](https://golang.org/dl/) (see [go.mod](../../server/go.mod) for minimum required version)
- [Node.js](https://nodejs.org/), [Vue.js](https://vuejs.org/), [Element-UI](https://element.eleme.cn/)
### macOS
We recommend using [Homebrew](https://brew.sh/) for installing any missing dependencies:
```
brew install git
brew install go
brew install node@20
```
## Download HAMi-WebUI
We recommend using the Git command-line interface to download the source code for the HAMi-WebUI project:
1. Open a terminal and run `git clone https://github.com/Project-HAMi/HAMi-WebUI.git`. This command downloads HAMi-WebUI to a new `hami-webui` directory in your current directory.
2. Open the `HAMi-WebUI` directory in your favorite code editor.
For alternative ways of cloning the HAMi-WebUI repository, refer to [GitHub's documentation](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository).
## Build HAMi-WebUI
When building HAMi-WebUI, be aware that it consists of two components:
- The _backend_.
- The _frontend_.
### Backend
Build and run the backend by running `make run` in the `server` directory of the repository. This command compiles the Go source code and starts a backend server.
By default, you can access the web-ui-server-swagger at `http://localhost:8000/q/swagger-ui`.
### Frontend
Build and run the frontend by running `make start-dev` in the `root` directory of the repository. This command installs the related dependencies and starts a bff server and a frontend server.
By default, you can access the web-ui at `http://localhost:3000/`.
## Build a Docker image
To build a HAMi-WebUI Frontend Docker image, run:
```
make build-image DOCKER_IMAGE=projecthami/hami-webui-fe VERSION=dev
```
The resulting image will be tagged as `projecthami/hami-webui-fe:dev`.
To build a HAMi-WebUI Backend Docker image, run:
```
make build-image DOCKER_IMAGE=projecthami/hami-webui-be VERSION=dev
```
The resulting image will be tagged as `projecthami/hami-webui-be:dev`.

@ -0,0 +1,176 @@
# Deploy HAMi-WebUI using Helm Charts
This topic includes instructions for installing and running HAMi-WebUI on Kubernetes using Helm Charts.
[Helm](https://helm.sh/) is an open-source command line tool used for managing Kubernetes applications. It is a graduate project in the [CNCF Landscape](https://www.cncf.io/projects/helm/).
The HAMi-WebUI open-source community offers Helm Charts for running it on Kubernetes. Please be aware that the code is provided without any warranties. If you encounter any problems, you can report them to the [Official GitHub repository](https://github.com/hami-webui/helm-charts/).
## Before you begin
To install HAMi-WebUI using Helm, ensure you have completed the following:
- Install a Kubernetes server on your machine. For information about installing Kubernetes, refer to [Install Kubernetes](https://kubernetes.io/docs/setup/).
- Install the latest stable version of Helm. For information on installing Helm, refer to [Install Helm](https://helm.sh/docs/intro/install/).
- Install HAMi on your Kubernetes cluster. For information about installing HAMi, refer to [Install HAMi](https://github.com/Project-HAMi/HAMi?tab=readme-ov-file#quick-start).
## Install HAMi-WebUI using Helm
When you install HAMi-WebUI using Helm, you complete the following tasks:
1. Set up the HAMi-WebUI Helm repository, which provides a space in which you will install HAMi-WebUI.
2. Deploy HAMi-WebUI using Helm, which installs HAMi-WebUI into a namespace.
3. Access HAMi-WebUI by navigating to the provided URL.
### Set up the HAMi-WebUI Helm repository
To set up the HAMi-WebUI Helm repository so that you download the correct HAMi-WebUI Helm charts on your machine, complete the following steps:
1. To add the HAMi-WebUI repository, use the following command syntax:
`helm repo add <DESIRED-NAME> <HELM-REPO-URL>`
The following example adds the `hami-webui` Helm repository.
```bash
helm repo add hami-charts https://project-hami.github.io/HAMi/
```
2. Run the following command to verify the repository was added:
```bash
helm repo list | grep hami
```
After you add the repository, you should see an output similar to the following:
```bash
hami-charts https://project-hami.github.io/HAMi/
```
3. Run the following command to update the repository to download the latest HAMi-WebUI Helm charts:
```bash
helm repo update
```
### Deploy the HAMi-WebUI Helm charts
After you have set up the HAMi-WebUI Helm repository, you can start to deploy it on your Kubernetes cluster.
When you deploy HAMi-WebUI Helm charts, use a separate namespace instead of relying on the default namespace. The default namespace might already have other applications running, which can lead to conflicts and other potential issues.
When you create a new namespace in Kubernetes, you can better organize, allocate, and manage cluster resources. For more information, refer to [Namespaces](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/).
1. To create a namespace, run the following command:
```bash
kubectl create namespace hami
```
You will see an output similar to this, which means that the namespace has been successfully created:
```bash
namespace/hami created
```
2. Search for the official `hami-charts/hami-webui` repository using the command:
`helm search repo <repo-name/package-name>`
For example, the following command provides a list of the HAMi-WebUI Helm Charts from which you will install the latest version of the HAMi-WebUI chart.
```bash
helm search repo hami-charts/hami-webui
```
3. Before proceeding, navigate to the [Configuration Guide for HAMi-WebUI Helm Chart](../../../charts/hami-webui/README.md#configuration-guide-for-hamiwebui-helm-chart), where you'll find instructions on configuring the necessary `values.yaml` based on your cluster's requirements.
> It is **critical** to modify the values accordingly before deploying.
4. Once you've adjusted the `values.yaml`, run the following command to deploy the HAMi-WebUI Helm Chart inside your namespace:
```bash
helm install my-hami-webui hami-charts/hami-webui --namespace hami
```
Where:
- `helm install`: Installs the chart by deploying it on the Kubernetes cluster
- `my-hami-webui`: The logical chart name that you provided
- `hami-charts/hami-webui`: The repository and package name to install
- `--namespace`: The Kubernetes namespace (i.e. `hami`) where you want to deploy the chart
5. To verify the deployment status, run the following command and verify that `deployed` appears in the **STATUS** column:
```bash
helm list -n hami
```
You should see an output similar to the following:
```bash
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
my-hami-webui hami 1 2024-09-11 14:19:09.003195 +0800 CST deployed hami-webui-1.1.0 1.1.0
```
6. To check the overall status of all the objects in the namespace, run the following command:
```bash
kubectl get all -n hami
```
If you encounter errors or warnings in the **STATUS** column, check the logs and refer to the Troubleshooting section of this documentation.
### Access HAMi-WebUI
1. Run the following command to do a port-forwarding of the HAMi-WebUI service on port `3000`.
```bash
kubectl port-forward service/my-hami-webui 3000:3000 --namespace=hami
```
For more information about port-forwarding, refer to [Use Port Forwarding to Access Applications in a Cluster](https://kubernetes.io/docs/tasks/access-application-cluster/port-forward-access-application-cluster/).
2. Navigate to `localhost:3000` in your browser.
The HAMi-WebUI resources-overview page appears.
## Troubleshooting
This section includes troubleshooting tips you might find helpful when deploying HAMi-WebUI on Kubernetes via Helm.
### Collect logs
It is important to view the HAMi-WebUI server logs while troubleshooting any issues.
To check the HAMi-WebUI logs, run the following command:
```bash
kubectl logs --namespace=hami deploy/my-hami-webui -c hami-webui-fe-oss
kubectl logs --namespace=hami deploy/my-hami-webui -c hami-webui-be-oss
```
For more information about accessing Kubernetes application logs, refer to [Pods](https://kubernetes.io/docs/reference/kubectl/cheatsheet/#interacting-with-running-pods) and [Deployments](https://kubernetes.io/docs/reference/kubectl/cheatsheet/#interacting-with-deployments-and-services).
## Uninstall the HAMi-WebUI deployment
To uninstall the HAMi-WebUI deployment, run the command:
`helm uninstall <RELEASE-NAME> <NAMESPACE-NAME>`
```bash
helm uninstall my-hami-webui -n hami
```
This deletes all of the objects from the given namespace hami.
If you want to delete the namespace `hami`, then run the command:
```bash
kubectl delete namespace hami
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

@ -0,0 +1,83 @@
{
"name": "hami-webui",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "export NODE_ENV=development && nest start --watch",
"start:debug": "export NODE_ENV=development && nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/axios": "^3.0.1",
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-socket.io": "^10.3.0",
"@nestjs/schedule": "^4.0.0",
"@nestjs/websockets": "^10.3.0",
"@types/cookie-parser": "^1.4.6",
"@vueuse/core": "^10.7.2",
"address": "^1.2.2",
"axios": "^1.6.5",
"cookie-parser": "^1.4.6",
"hbs": "^4.2.0",
"http-proxy-middleware": "^2.0.6",
"moment": "^2.30.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"socket.io": "^4.7.4",
"express":"^4.19.2"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

@ -0,0 +1,6 @@
# just a flag
ENV = 'development'
# base api
VUE_APP_BASE_API = '/'

@ -0,0 +1,5 @@
# just a flag
ENV = 'production'
# base api
VUE_APP_BASE_API = '/'

@ -0,0 +1,27 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'plugin:prettier/recommended',
],
parserOptions: {
parser: '@babel/eslint-parser',
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'space-before-function-paren': 'off',
'vue/multi-word-component-names': 'off',
'prettier/prettier': 'off',
'no-unused-vars': 'off',
'no-empty': 'off',
'no-useless-escape': 'off',
},
globals: {
globals: 'readonly',
},
};

@ -0,0 +1,23 @@
.DS_Store
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
**/*.log
tests/**/coverage/
tests/e2e/reports
selenium-debug.log
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.local
package-lock.json
yarn.lock

@ -0,0 +1,24 @@
# web
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn serve
```
### Compiles and minifies for production
```
yarn build
```
### Lints and fixes files
```
yarn lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

@ -0,0 +1,6 @@
module.exports = {
presets: ['@vue/cli-plugin-babel/preset'],
plugins: [
'@vue/babel-plugin-jsx'
]
};

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

@ -0,0 +1,75 @@
{
"name": "",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "export NODE_ENV=development && vue-cli-service serve",
"start:dev": "export NODE_ENV=development && vue-cli-service serve",
"startWin:dev": "setx NODE_ENV development && vue-cli-service serve",
"lint": "eslint --ext .js,.vue src",
"build": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview",
"new": "plop",
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml",
"test:unit": "jest --clearCache && vue-cli-service test:unit",
"test:ci": "npm run lint && npm run test:unit"
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"@tabler/core": "1.0.0-beta20",
"@tabler/icons-vue": "^2.36.0",
"animate.css": "v4.1.1",
"axios": "1.5.1",
"body-parser": "1.20.2",
"bootstrap": "5.3.1",
"core-js": "^3.8.3",
"crypto-browserify": "^3.12.0",
"echarts": "5.4.3",
"element-plus": "^2.4.1",
"generate-avatar": "1.4.10",
"js-cookie": "3.0.5",
"lodash": "^4.17.21",
"node-polyfill-webpack-plugin": "^2.0.1",
"normalize.css": "^8.0.1",
"nprogress": "0.2.0",
"socket.io-client": "^4.7.4",
"sortablejs": "^1.15.2",
"tom-select": "2.2.2",
"vue": "^3.2.13",
"vue-clipboard3": "2.0.0",
"vue-count-to": "1.0.13",
"vue-native-websocket-vue3": "^3.1.7",
"vue-router": "^4.0.3",
"vuedraggable": "^4.1.0",
"vuex": "^4.0.0",
"vuex-persistedstate": "^4.1.0",
"iframe-resizer": "^4.3.2",
"web-storage-cache": "^1.1.1"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/babel-plugin-jsx": "^1.1.5",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"babel-plugin-dynamic-import-node": "2.3.3",
"eslint": "8.56.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.0.3",
"mockjs": "^1.1.0",
"prettier": "^2.4.1",
"sass": "^1.32.7",
"sass-loader": "^12.0.0",
"svg-sprite-loader": "^6.0.9",
"vue-cli-plugin-element-plus": "~0.0.13"
},
"browserslist": [
"> 1%",
"last 2 versions"
]
}

@ -0,0 +1,22 @@
import router from '@/router';
import NProgress from 'nprogress'; // progress bar
import 'nprogress/nprogress.css'; // progress bar style
/**
* 登录鉴权路由前置守卫
*/
// 白名单
const whiteList = [
'/401',
'/404',
];
router.beforeEach(async (to, from, next) => {
NProgress.start();
next();
});
router.afterEach(() => {
// finish progress bar
NProgress.done();
});

@ -0,0 +1,52 @@
import request from '@/utils/request';
const apiPrefix = '/api/vgpu';
class cardApi {
getCardList(data = { filters: {} }) {
return {
url: apiPrefix + '/v1/gpus',
method: 'POST',
data,
};
}
getCardType(data = { filters: {} }) {
return {
url: apiPrefix + '/v1/gpu-types',
method: 'POST',
data,
};
}
getCardListReq(data) {
return request(this.getCardList(data));
}
getCardDetail(params) {
return request({
url: apiPrefix + '/v1/gpu',
method: 'get',
params,
});
}
getRangeVector(data) {
return request({
url: apiPrefix + '/v1/monitor/query/range-vector',
method: 'post',
data,
});
}
getInstantVector(data) {
return request({
url: apiPrefix + '/v1/monitor/query/instant-vector',
method: 'post',
data,
});
}
}
export default new cardApi();

@ -0,0 +1,15 @@
import request from '@/utils/request';
const apiPrefix = '/api/vgpu';
class InfoApi {
getSysInfo(data) {
return request({
url: apiPrefix + '/v1/sys-info',
method: 'POST',
data
});
}
}
export default new InfoApi();

@ -0,0 +1,20 @@
import request from '@/utils/request';
const apiPrefix = '/api/vgpu';
class monitorApi {
summary(data) {
return request({ apiPrefix, url: apiPrefix + '/v1/summary', method: 'POST', data });
}
usage(data) {
return request({
url: apiPrefix + '/v1/monitor/summary',
method: 'POST',
data,
});
}
}
export default new monitorApi();

@ -0,0 +1,36 @@
import request from '@/utils/request';
const apiPrefix = '/api/vgpu';
class nodeApi {
getNodeList(data) {
return {
url: apiPrefix + '/v1/nodes',
method: 'POST',
data,
};
}
getNodes(data) {
return request({
url: apiPrefix + '/v1/nodes',
method: 'POST',
data,
});
}
getNodeDetail(params) {
return request({
url: apiPrefix + '/v1/node',
method: 'GET',
params,
});
}
getNodeListReq(data) {
return request(this.getNodeList(data));
}
}
export default new nodeApi();

@ -0,0 +1,42 @@
import request from '@/utils/request';
const apiPrefix = '/api/vgpu';
class taskApi {
getTaskList(data) {
return {
url: apiPrefix + '/v1/containers',
method: 'POST',
dataPath: 'items',
data,
};
}
getTaskListReq(data) {
return request({
url: apiPrefix + '/v1/containers',
method: 'POST',
dataPath: 'items',
data,
});
}
getTaskDetail(params) {
return request({
url: apiPrefix + '/v1/container',
method: 'get',
params,
});
}
deleteTask(data) {
return request({
url: apiPrefix + '/v1/task/delete',
method: 'post',
data,
});
}
}
export default new taskApi();

@ -0,0 +1,254 @@
<template>
<div>
<back-header> {{ title }}管理 > {{ props.name }} </back-header>
<block-box class="node-block">
<div class="node-detail">
<div class="node-detail-left">
<div class="title">详细信息</div>
<ul class="node-detail-info">
<li v-for="{ label, value, render } in detailColumns">
<span class="label">{{ label }}</span>
<component v-if="render" :is="render(detail)" />
<span v-else class="value">{{ detail[value] }}</span>
</li>
<li class="cp" v-if="!hideCp">
<span v-for="{ label, count } in cp">
<span class="label">{{ label }}</span>
<span class="value">{{ count }} </span>
</span>
</li>
</ul>
</div>
<ul class="gauges">
<li v-for="item in gaugeConfig">
<Gauge v-bind="item" />
</li>
</ul>
</div>
</block-box>
<div class="line-box">
<block-box
:title="title.replace('率', '趋势(%')"
v-for="{ title, data } in gaugeConfig"
>
<template #extra>
<TimeSelect v-model="time" />
</template>
<div style="height: 200px">
<echarts-plus :options="getLineOptions({ data })" />
</div>
</block-box>
</div>
<block-box title="显卡列表" v-if="type !== 'deviceuuid'">
<CardList :hideTitle="true" :filters="filters" />
</block-box>
<block-box title="任务列表">
<TaskList :hideTitle="true" :filters="filters" />
</block-box>
</div>
</template>
<script setup lang="jsx">
import BackHeader from '@/components/BackHeader.vue';
import { useRoute } from 'vue-router';
import BlockBox from '@/components/BlockBox.vue';
import { onMounted, ref, watch } from 'vue';
import CardList from '~/vgpu/views/card/admin/index.vue';
import TaskList from '~/vgpu/views/task/admin/index.vue';
import Gauge from '~/vgpu/components/gauge.vue';
import useInstantVector from '~/vgpu/hooks/useInstantVector';
import EchartsPlus from '@/components/Echarts-plus.vue';
import cardApi from '~/vgpu/api/card';
import { timeParse } from '@/utils';
import { getLineOptions } from '~/vgpu/components/config';
import TimeSelect from '~/vgpu/components/timeSelect.vue';
const props = defineProps([
'title',
'detailColumns',
'type',
'detail',
'name',
'filters',
'hideCp',
]);
const route = useRoute();
const time = ref(1 / 24);
const cp = useInstantVector(
[
{
label: 'vGPU 超配',
count: '0',
query: `avg(hami_vgpu_count{node=~"$node"})`,
},
{
label: '算力超配',
count: '0',
query: `avg(hami_vcore_scaling{node=~"$node"})`,
},
{
label: '显存超配',
count: '1.5',
query: `avg(hami_vmemory_scaling{node=~"$node"})`,
},
],
(query) => query.replaceAll('$node', props.detail.name),
);
const gaugeConfig = useInstantVector(
[
{
title: '算力分配率',
percent: 0,
query: `sum(hami_container_vcore_allocated{${props.type}=~"$${props.type}"})`,
totalQuery: `sum(hami_core_size{${props.type}=~"$${props.type}"})`,
percentQuery: `sum(hami_container_vcore_allocated{${props.type}=~"$${props.type}"}) / sum(hami_core_size{${props.type}=~"$${props.type}"}) *100`,
total: 0,
used: 0,
unit: ' ',
},
{
title: '显存分配率',
percent: 0,
query: `sum(hami_container_vmemory_allocated{${props.type}=~"$${props.type}"}) / 1024`,
totalQuery: `sum(hami_memory_size{${props.type}=~"$${props.type}"}) / 1024`,
percentQuery: `(sum(hami_container_vmemory_allocated{${props.type}=~"$${props.type}"}) / 1024) /(sum(hami_memory_size{${props.type}=~"$${props.type}"}) / 1024) *100`,
total: 0,
used: 0,
unit: 'GiB',
},
{
title: '算力使用率',
percent: 0,
query: `avg(hami_core_util{${props.type}=~"$${props.type}"})`,
percentQuery: `avg(hami_core_util_avg{${props.type}=~"$${props.type}"})`,
totalQuery: `sum(hami_core_size{${props.type}=~"$${props.type}"})`,
total: 100,
used: 0,
unit: ' ',
},
{
title: '显存使用率',
percent: 0,
query: `sum(hami_memory_used{${props.type}=~"$${props.type}"}) / 1024`,
totalQuery: `sum(hami_memory_size{${props.type}=~"$${props.type}"})/1024`,
percentQuery: `(sum(hami_memory_used{${props.type}=~"$${props.type}"}) / 1024)/(sum(hami_memory_size{${props.type}=~"$${props.type}"})/1024)*100`,
total: 0,
used: 0,
unit: 'GiB',
},
],
(query) => query.replaceAll(`$${props.type}`, props.name),
time,
);
// const fetchLineData = async () => {
// const start = new Date();
// start.setTime(start.getTime() - 3600 * 1000 * 24 * time.value);
//
// const lineReqs = gaugeConfig.value.map((item) =>
// cardApi.getRangeVector({
// range: {
// start: timeParse(start),
// end: timeParse(new Date()),
// step: '1m',
// },
// query: item.percentQuery.replaceAll(`$${props.type}`, props.name),
// }),
// );
//
// const res = await Promise.all(lineReqs);
//
// gaugeConfig.value = gaugeConfig.value.map((item, index) => ({
// ...item,
// data: res[index].data[0]?.values || [],
// }));
// };
// watch(
// () => props.detail,
// async () => {
// fetchLineData();
// },
// );
// watch(time, () => {
// fetchLineData();
// });
</script>
<style lang="scss">
.node-detail {
display: flex;
height: 100%;
gap: 50px;
ul {
margin: 0;
padding: 0;
list-style: none;
}
.title {
color: #1d2b3a;
font-family: 'PingFang SC';
font-size: 14px;
font-style: normal;
font-weight: 500;
//line-height: 20px;
margin-bottom: 20px;
}
.node-detail-left {
min-width: 500px;
}
.node-detail-info {
display: flex;
flex-direction: column;
gap: 15px;
font-size: 12px;
.label {
display: inline-block;
width: 80px;
color: #939ea9;
}
.cp {
display: flex;
gap: 25px;
margin-top: 10px;
}
}
.gauges {
flex: 1;
display: flex;
li {
flex: 1;
}
}
}
.line-box {
display: grid;
grid-template-columns: repeat(2, 1fr);
column-gap: 20px;
}
.node-block {
display: flex;
flex-direction: column;
.home-block-content {
flex: 1;
}
}
</style>

@ -0,0 +1,121 @@
<template>
<block-box :title="title">
<template #extra>
<el-radio-group v-model="tabActive" size="small" @change="handleTabChange">
<el-radio-button
v-for="{ tab, key } in config"
:label="tab"
:value="key"
/>
</el-radio-group>
</template>
<echarts-plus
:options="
getTopOptions(
currentConfig.find((item) => item.key === tabActive)?.data || [],
)
"
:onClick="handleClick"
style="min-height: 250px; height: 100%"
/>
</block-box>
</template>
<script setup>
import BlockBox from '@/components/BlockBox.vue';
import { onMounted, ref } from 'vue';
import EchartsPlus from '@/components/Echarts-plus.vue';
import cardApi from '~/vgpu/api/card';
import { cloneDeep } from 'lodash';
const props = defineProps({
title: String,
key: String,
config: Array,
onClick: Function,
});
const currentConfig = ref(props.config);
const tabActive = ref('');
const handleTabChange = (key) => {
tabActive.value = key;
};
const handleClick = (params) => {
if (props.onClick) {
props.onClick({ ...params, tabActive: tabActive.value });
}
};
const getTopOptions = () => {
const config = currentConfig.value.find(
(item) => item.key === tabActive.value,
);
const data = cloneDeep(config?.data) || [];
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
formatter: function (params) {
var res = params[0].name + '<br/>';
for (var i = 0; i < params.length; i++) {
res +=
params[i].marker +
params[i].seriesName +
(+params[i].value).toFixed(0) +
`${config.unit || '%'}<br/>`;
}
return res;
},
},
legend: {
show: false,
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '0',
containLabel: true,
},
xAxis: {
type: 'value',
boundaryGap: [0, 0.01],
},
yAxis: {
type: 'category',
data: data
.reverse()
.map((item) =>
item.name.length > 15 ? item.name.slice(0, 15) + '...' : item.name,
),
},
series: [
{
name: '',
type: 'bar',
data: data,
},
],
};
};
onMounted(async () => {
tabActive.value = currentConfig.value[0].key;
currentConfig.value.forEach((v, i) => {
cardApi
.getInstantVector({
query: v.query,
})
.then((res) => {
currentConfig.value[i].data = res.data.map((item) => ({
name: item.metric[v.nameKey],
value: item.value,
}));
});
});
});
</script>

@ -0,0 +1,289 @@
import { timeParse } from '@/utils';
export default ({ percent, title, unit = '%' }) => {
const value = percent.toFixed(1);
let thisColor = '';
if (value < 30) {
thisColor = '#16A34A';
} else if (value >= 30 && value <= 80) {
thisColor = '#2563EB';
} else {
thisColor = '#DC2626';
}
return {
series: [
{
type: 'gauge',
itemStyle: {
color: thisColor,
// shadowColor: thisColor,
// shadowBlur: 5,
// shadowOffsetX: 2,
// shadowOffsetY: 2,
},
progress: {
show: true,
width: 8,
},
axisLine: {
lineStyle: {
width: 8,
backgroundColor: '#F5F7FA',
},
},
axisTick: {
show: false,
},
axisLabel: {
show: false, // 隐藏刻度标签
},
// splitNumber: 0,
splitLine: {
length: 4,
lineStyle: {
width: 2,
color: '#999',
},
distance: 5,
},
anchor: {
show: false,
showAbove: false,
size: 25,
itemStyle: {
borderWidth: 10,
},
},
pointer: {
show: false,
},
title: {
show: false,
},
detail: {
valueAnimation: true,
width: '60%',
lineHeight: 40,
borderRadius: 8,
offsetCenter: [0, '0%'],
// fontSize: 16,
fontWeight: 'bolder',
formatter: `{a|{value}}{b|${unit}}`,
rich: {
a: {
color: '#1D2B3A',
lineHeight: 10,
fontSize: 20,
},
b: {
color: '#1D2B3A',
},
},
},
data: [
{
value,
},
],
},
],
graphic: [
{
type: 'text',
left: 'center',
bottom: 8,
style: {
text: title,
fill: '#333', // 设置标题文字颜色
fontSize: 14, // 设置标题文字大小
borderRadius: 999,
backgroundColor: '#F5F7FA',
padding: [6, 16],
},
},
],
};
};
export const getPreviewBarPie = (statusConfig, { title }) => {
return {
tooltip: {
show: false,
},
// legend: {
// top: '5%',
// left: 'center',
// },
series: [
{
name: 'Access From',
type: 'pie',
radius: ['50%', '65%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 3,
borderColor: '#fff',
},
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: false,
fontSize: 40,
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
// data: [
// { value: data.running, itemStyle: {color: '#ff0000'} },
// { value: data.stopped , itemStyle: {color: '#ff0000'}},
// { value: data.error , itemStyle: {color: '#ff0000'}},
// ],
data: statusConfig.map((item) => ({
...item,
itemStyle: { color: item.color },
})),
},
],
grid: {
top: 1, // 上边距
bottom: 1, // 下边距
left: 1, // 左边距
right: 1, // 右边距
},
graphic: [
{
type: 'text', // 添加文本标签
left: 'center', // 文本标签水平居中
top: 'center', // 文本标签垂直居中
style: {
text: title, // 设置文本内容
fill: '#333', // 文字颜色
fontSize: 12, // 文字大小
},
},
],
};
};
export const getTopOptions = ({ core, memory }) => {
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
legend: {},
grid: {
left: '3%',
right: '4%',
bottom: '1%',
top: '10%',
containLabel: true,
},
xAxis: {
type: 'value',
boundaryGap: [0, 0.01],
axisLabel: {
formatter: function (value) {
return `${value} %`;
},
},
},
yAxis: {
type: 'category',
data: core.data.map((item) =>
item.name.length > 15 ? item.name.slice(0, 15) + '...' : item.name,
),
},
series: [
{
name: '算力',
type: 'bar',
data: core.data,
},
{
name: '显存',
type: 'bar',
data: memory.data,
},
],
};
};
export const getLineOptions = ({ data = [], unit = '%' }) => {
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
},
formatter: function (params) {
var res = params[0].name + '<br/>';
for (var i = 0; i < params.length; i++) {
res +=
params[i].marker + (+params[i].value).toFixed(0) + ` ${unit}<br/>`;
}
return res;
},
},
grid: {
top: 7, // 上边距
bottom: 20, // 下边距
left: '7%', // 左边距
right: 10, // 右边距
},
xAxis: {
type: 'category',
data: data.map((item) => timeParse(+item.timestamp)),
axisLabel: {
formatter: function (value) {
return timeParse(value, 'HH:mm');
},
},
},
yAxis: {
type: 'value',
},
series: [
{
data: data.map((item) => {
return item.value.toFixed(1);
}),
type: 'line',
areaStyle: {
normal: {
color: {
type: 'linear',
x: 0, // 渐变起始点 0%
y: 0, // 渐变起始点 0%
x2: 0, // 渐变结束点 100%
y2: 1, // 渐变结束点 100%
colorStops: [
{
offset: 0,
color: 'rgba(84, 112, 198, 0.16)', // 渐变起始颜色
},
{
offset: 1,
color: 'rgba(84, 112, 198, 0.00)', // 渐变结束颜色
},
],
global: false, // 缺省为 false
},
},
},
},
],
};
};

@ -0,0 +1,41 @@
<template>
<div class="gauge-box">
<echarts-plus
:options="getOptions({ ...$props, unit: gaugeUnit })"
class="gauge-box-echarts"
/>
<div class="gauge-info" v-if="!hideInfo">
<span>{{ title.includes('使用') ? '使用' : '分配' }}</span>
<span v-if="!title.includes('算力')">({{ unit }})</span> :
<b>{{ used.toFixed(1) }}/{{ total.toFixed() }}</b>
</div>
</div>
</template>
<script setup>
import EchartsPlus from '@/components/Echarts-plus.vue';
import getOptions from './config';
import { defineProps } from 'vue';
const props = defineProps([
'title',
'total',
'used',
'unit',
'gaugeUnit',
'percent',
'hideInfo',
]);
</script>
<style>
.gauge-box {
display: flex;
flex-direction: column;
height: 100%;
text-align: center;
font-size: 12px;
.gauge-box-echarts {
flex: 1;
}
}
</style>

@ -0,0 +1,219 @@
<template>
<ul class="preview">
<li class="preview-item" style="width: 20%; flex: none" v-if="!hidePie">
<block-box
:title="`${type === 'node' ? '节点显卡厂商分布' : '显卡类型分布'}`"
class="nodeCard"
>
<div class="pie">
<echarts-plus
:options="getPreviewBarPie(pieData, props)"
:onClick="handlePieClick"
ref="echartsRef"
/>
</div>
<ul class="nodeCard-legend">
<li
v-for="{ name, value, color } in pieData"
:style="{
fontWeight: currentName === name ? 'bold' : 'normal',
}"
>
<div class="left">
<span
class="color-box"
:style="{
'background-color': color,
}"
></span>
<span> {{ name }}</span>
</div>
<span>{{ value }}</span>
</li>
</ul>
</block-box>
</li>
<li class="preview-item">
<TabTop v-bind="totalTop" :onClick="handleClick" class="node-top" />
</li>
<li class="preview-item">
<TabTop v-bind="usedTop" :onClick="handleClick" class="node-top" />
</li>
</ul>
</template>
<script setup>
import BlockBox from '@/components/BlockBox.vue';
import EchartsPlus from '@/components/Echarts-plus.vue';
import { getPreviewBarPie, getTopOptions } from '~/vgpu/components/config';
import { onMounted, reactive, ref } from 'vue';
import cardApi from '~/vgpu/api/card';
import TabTop from '~/vgpu/components/TabTop.vue';
const props = defineProps({
title: {
default: '节点',
},
type: {
default: 'node',
},
hidePie: Boolean,
handleClick: Function,
handlePieClick: Function,
currentName: String,
});
const echartsRef = ref();
const totalTop = {
title: `${props.title}资源分配率 Top5`,
config: [
{
tab: 'vGPU',
key: 'vgpu',
nameKey: props.type,
data: [],
query: `topk(5, sum(hami_container_vgpu_allocated{}) by (${props.type}) / sum(hami_vgpu_count{}) by (${props.type}) * 100)`,
},
{
tab: '算力',
key: 'core',
nameKey: props.type,
data: [],
query: `topk(5, sum(hami_container_vcore_allocated{}) by (${props.type}) / sum(hami_core_size{}) by (${props.type}) * 100)`,
},
{
tab: '显存',
key: 'memory',
data: [],
nameKey: props.type,
query: `topk(5, sum(hami_container_vmemory_allocated{}) by (${props.type}) / sum(hami_memory_size{}) by (${props.type}) * 100)`,
},
],
};
const usedTop = {
title: `${props.title}资源使用率 Top5`,
config: [
{
tab: '算力',
key: 'core',
nameKey: props.type,
data: [],
query: `topk(5, avg(hami_core_util_avg) by (${props.type}))`,
},
{
tab: '显存',
key: 'memory',
data: [],
nameKey: props.type,
query: `topk(5, sum(hami_memory_used) by (${props.type}) / sum(hami_memory_size) by (${props.type}) * 100)`,
},
],
};
const pieConfig = {
deviceuuid: {
query:
'count by (devicetype) (sum by (deviceuuid, devicetype) (hami_vgpu_count))',
key: 'devicetype',
},
node: {
query: 'count by (provider) (sum by (node,provider) (hami_vgpu_count))',
key: 'provider',
},
};
const pieData = ref([]);
onMounted(async () => {
const thisPieConfig = pieConfig[props.type];
const { data } = await cardApi.getInstantVector({
query: thisPieConfig.query,
});
const colors = [
'#5470C6',
'#91CC75',
'#FAC858',
'#EE6666',
'#73C0DE',
'#3BA272',
'#FC8452',
'#9A60B4',
'#EA7CCC',
];
pieData.value = data.map((item, index) => {
return {
name: item.metric[thisPieConfig.key],
value: item.value,
color: colors[index],
};
});
});
</script>
<style scoped lang="scss">
ul {
margin: 0;
padding: 0;
list-style: none;
}
.preview {
width: 100%;
display: flex;
gap: 20px;
margin-bottom: 20px;
.preview-item {
flex: 1;
}
.nodeCard {
height: 100%;
.pie {
width: 200px;
height: 200px;
margin: 0 auto;
}
.nodeCard-legend {
width: 100%;
display: flex;
flex-direction: column;
gap: 15px;
li {
display: flex;
justify-content: space-between;
font-size: 12px;
align-items: center;
.left {
display: flex;
align-items: center;
gap: 5px;
}
.color-box {
width: 4px;
height: 4px;
display: inline-block;
}
}
}
}
.node-top {
display: flex;
flex-direction: column;
min-height: 350px;
height: 100%;
& > :nth-child(2) {
flex: 1;
max-height: 280px;
}
.node-top-echarts {
//flex: 1;
}
}
}
</style>

@ -0,0 +1,44 @@
<script setup>
import { ref, defineModel } from 'vue';
const time = defineModel();
const timeOptions = ref([
{
label: '近7天',
value: 7,
},
{
label: '近5天',
value: 5,
},
{
label: '近3天',
value: 3,
},
{
label: '24小时内',
value: 1,
},
{
label: '12小时内',
value: 0.5,
},
{
label: '1小时内',
value: 0.5 / 12,
},
]);
</script>
<template>
<el-select v-model="time" style="width: 150px">
<el-option
v-for="{ label, value } in timeOptions"
:label="label"
:value="value"
></el-option>
</el-select>
</template>
<style scoped lang="scss"></style>

@ -0,0 +1,155 @@
<template>
<Block title="资源使用情况">
<div style="display: flex; align-items: center">
<div class="alarm-left">
<div
class="alarm-left-item"
v-for="{ title, bg, count, color, icon } in alarmConfig"
:style="{ background: bg }"
>
<div class="title">
{{ title }}
</div>
<div class="count" :style="{ color }">
{{ count }}
</div>
<svg-icon :icon="icon" class="icon-bg" />
</div>
</div>
<ul class="usage">
<li v-for="item in config">
<echarts-plus :options="getOptions(item)" />
</li>
</ul>
</div>
</Block>
</template>
<script setup>
import Block from '~/vgpu/views/monitor/overview/Block.vue';
import monitorApi from '~/vgpu/api/monitor';
import { onMounted, ref, defineProps} from 'vue';
import EchartsPlus from '@/components/Echarts-plus.vue';
import getOptions from './config';
import { getDataByPath } from '@/utils';
const props = defineProps(['data']);
const config = ref([
{
title: 'vgpu分配率',
path: 'distributionRate.vgpu',
percent: 0,
},
{
title: '算力分配率',
path: 'distributionRate.core',
percent: 0,
},
{
title: '显存分配率',
path: 'distributionRate.memory',
percent: 0,
},
{
title: '算力利用率',
path: 'useRate.core',
percent: 0,
},
{
title: '显存利用率',
path: 'useRate.memory',
percent: 0,
},
]);
const alarmConfig = ref([
{
title: 'vgpu超配比',
bg: '#FEF2F2',
count: 0,
icon: 'alarm-warning',
color: '#DC2626',
path: 'scaling.vgpu',
},
{
title: '算力超配比',
bg: '#F5F7FA',
count: 0,
icon: 'alarm-history',
path: 'scaling.memory',
},
{
title: '显存超配比',
bg: '#F5F7FA',
count: 0,
icon: 'alarm-history',
path: 'scaling.core',
},
]);
onMounted(async () => {
const res = await monitorApi.usage(props.data);
config.value = config.value.map((item) => {
return { ...item, count: getDataByPath(res, item.path) };
});
alarmConfig.value = alarmConfig.value.map((item) => {
return { ...item, count: getDataByPath(res, item.path) };
});
});
</script>
<style scoped lang="scss">
.alarm-left {
display: flex;
flex-direction: column;
gap: 12px;
&-item {
width: 202px;
height: 80px;
border-radius: 6px;
padding: 16px;
position: relative;
.title {
color: #324558;
font-family: 'PingFang SC';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
margin-bottom: 10px;
}
.count {
font-family: Roboto;
font-size: 20px;
font-style: normal;
font-weight: 700;
line-height: 100%; /* 20px */
}
.icon-bg {
position: absolute;
right: 10px;
bottom: -10px;
width: 56px;
height: 56px;
}
}
}
.usage {
flex: 1;
list-style: none;
margin: 0;
padding: 0;
//padding-left: 180px;
display: flex;
height: 200px;
gap: 20px;
li {
flex: 1;
}
}
</style>

@ -0,0 +1,93 @@
import cardApi from '~/vgpu/api/card';
import { onMounted, ref, watch, watchEffect } from 'vue';
import { timeParse } from '@/utils';
const useInstantVector = (configs, parseQuery = (query) => query, times) => {
const data = ref(configs);
const fetchInstantData = async () => {
const reqs = configs.map(
async ({ query, totalQuery, percentQuery, cntQuery }, index) => {
if (parseQuery(query).includes('undefined')) {
return;
}
if (query) {
const usedData = await cardApi.getInstantVector({
query: parseQuery(query),
});
const used = usedData.data.length ? usedData.data[0]?.value : 0;
data.value[index].count = used;
data.value[index].used = used;
}
if (totalQuery) {
const totalData = await cardApi.getInstantVector({
query: parseQuery(totalQuery),
});
if (totalData.data[0]) {
data.value[index].total = totalData.data[0].value;
}
}
if (data.value[index].total !== 0) {
data.value[index].percent = data.value[index].used / data.value[index].total * 100;
}
if (percentQuery) {
const percentData = await cardApi.getRangeVector({
query: parseQuery(percentQuery),
range: {
start: timeParse(times?.value[0]),
end: timeParse(times?.value[1]),
step: '1m',
},
});
data.value[index].data = percentData.data[0]?.values;
}
},
);
Promise.all(reqs);
};
const fetchRangeData = async () => {
const reqs = configs.map(
async ({ query, totalQuery, percentQuery }, index) => {
if (parseQuery(query).includes('undefined')) {
return;
}
if (percentQuery) {
const percentData = await cardApi.getRangeVector({
query: parseQuery(percentQuery),
range: {
start: timeParse(times.value[0]),
end: timeParse(times.value[1]),
step: '1m',
},
});
data.value[index].data = percentData.data[0]?.values;
}
},
);
Promise.all(reqs);
};
watchEffect(() => {
fetchInstantData();
});
watch(
times,
() => {
fetchRangeData();
},
// { immediate: true },
);
return data;
};
export default useInstantVector;

@ -0,0 +1,26 @@
import cardApi from '~/vgpu/api/card';
import { onMounted, ref, watchEffect } from 'vue';
const useInstantVector = (configs, parseQuery = (query) => query) => {
const data = ref(configs);
const fetchData = async () => {
const reqs = configs.map(({ query }, index) =>
cardApi.getInstantVector({ query: parseQuery(query) }).then((res) => {
const num = res.data.length ? res.data[0]?.value : 0;
data.value[index] = { ...data.value[index], count: num, percent: num };
}),
);
Promise.all(reqs);
};
// onMounted(fetchData);
watchEffect(() => {
fetchData();
});
return data;
};
export default useInstantVector;

@ -0,0 +1,38 @@
import cardApi from '~/vgpu/api/card';
import { ref, watchEffect } from 'vue';
import { timeParse } from '@/utils';
const useInstantVector = (configs, parseQuery = (query) => query) => {
const data = ref(configs);
const fetchData = async () => {
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
const reqs = configs.map(({ query }) =>
cardApi.getRangeVector({
range: {
start: timeParse(start),
end: timeParse(new Date()),
step: '1m',
},
query: item.query.replace('$node', detail.value.name),
}),
);
const res = await Promise.all(reqs);
data.value = data.value.map((item, i) => {
const num = res[i].data.length ? res[i].data[0]?.value : 0;
return { ...item, count: num, percent: num };
});
};
// onMounted(fetchData);
watchEffect(() => {
fetchData();
});
return data;
};
export default useInstantVector;

@ -0,0 +1,59 @@
export default (Layout) => ({
path: '/admin/vgpu',
component: Layout,
redirect: '/admin/vgpu/node/admin',
name: 'vgpu',
meta: {
title: 'GPU 管理',
icon: 'vgpu-gpu-l',
},
children: [
{
name: 'resource-admin',
meta: { title: '资源管理' },
path: '/admin/vgpu/monitor',
component: () => import('~/vgpu/views/monitor/index.vue'),
children: [
{
path: '/admin/vgpu/monitor/overview',
component: () => import('~/vgpu/views/monitor/overview/index.vue'),
name: 'overview',
meta: { title: '资源总览', icon: 'dashboard', noCache: true },
},
{
path: '/admin/vgpu/node/admin',
component: () => import('~/vgpu/views/node/admin/index.vue'),
name: 'node-admin',
meta: { title: '节点管理', icon: 'vgpu-node', noCache: true },
},
{
path: '/admin/vgpu/node/admin/:uid',
component: () => import('~/vgpu/views/node/admin/Detail.vue'),
name: 'node-admin-detail',
},
{
path: '/admin/vgpu/card/admin',
component: () => import('~/vgpu/views/card/admin/index.vue'),
name: 'card-admin',
meta: { title: '显卡管理', icon: 'vgpu-card', noCache: true },
},
{
path: '/admin/vgpu/card/admin/:uuid',
component: () => import('~/vgpu/views/card/admin/Detail.vue'),
name: 'card-admin-detail',
},
{
path: '/admin/vgpu/task/admin',
component: () => import('~/vgpu/views/task/admin/index.vue'),
name: 'task-admin',
meta: { title: '任务管理', icon: 'vgpu-task', noCache: true },
},
{
path: '/admin/vgpu/task/admin/detail',
component: () => import('~/vgpu/views/task/admin/Detail.vue'),
name: 'task-admin-detail',
},
],
},
],
});

@ -0,0 +1,404 @@
<template>
<div>
<back-header> 显卡管理 > {{ detail.uuid }} </back-header>
<block-box class="node-block">
<div class="card-detail">
<div class="card-detail-left">
<div class="title">详细信息</div>
<ul class="card-detail-info">
<li v-for="{ label, value, render } in columns" :key="label">
<span class="label">{{ label }}</span>
<component v-if="render" :is="render(detail)" />
<span v-else class="value">{{ detail[value] }}</span>
</li>
</ul>
</div>
</div>
</block-box>
<block-box>
<ul class="card-gauges">
<li v-for="(item, index) in gaugeConfig" :key="index">
<template v-if="!detail.isExternal || index >= 2">
<Gauge v-bind="item" />
</template>
<template v-else-if="detail.isExternal && index < 2">
<el-empty description="暂无资源分配数据" :image-size="90" />
</template>
</li>
<li v-for="(item, index) in lineTools" :key="index">
<Gauge v-bind="item" />
</li>
</ul>
</block-box>
<div class="line-box">
<block-box title="资源分配趋势(%">
<template #extra>
<time-picker v-model="times" type="datetimerange" size="small" />
</template>
<div style="height: 200px">
<echarts-plus
:options="
getRangeOptions({
core: gaugeConfig[0].data,
memory: gaugeConfig[1].data,
})
"
/>
</div>
</block-box>
<block-box title="资源使用趋势(%">
<template #extra>
<time-picker v-model="times" type="datetimerange" size="small" />
</template>
<div style="height: 200px">
<echarts-plus
:options="
getRangeOptions({
core: gaugeConfig[2].data,
memory: gaugeConfig[3].data,
})
"
/>
</div>
</block-box>
<block-box :title="title" v-for="{ title, data, unit } in lineTools" :key="title">
<template #extra>
<time-picker v-model="times" type="datetimerange" size="small" />
</template>
<div style="height: 200px">
<echarts-plus :options="getLineOptions2({ data, unit })" />
</div>
</block-box>
</div>
<block-box title="任务列表">
<template v-if="detail.isExternal">
<el-alert title="由于显卡未纳管,无法获取到任务数据" show-icon type="warning" :closable="false" />
<el-empty description="暂无任务数据" :image-size="100" />
</template>
<template v-else>
<TaskList :hideTitle="true" :filters="{ deviceId: detail.uuid }" />
</template>
</block-box>
</div>
</template>
<script setup lang="jsx">
import BackHeader from '@/components/BackHeader.vue';
import { useRoute } from 'vue-router';
import BlockBox from '@/components/BlockBox.vue';
import { onMounted, ref, watch, defineProps } from 'vue';
import TaskList from '~/vgpu/views/task/admin/index.vue';
import {ElPopover} from 'element-plus';
import Gauge from '~/vgpu/components/gauge.vue';
import useInstantVector from '~/vgpu/hooks/useInstantVector';
import { QuestionFilled } from '@element-plus/icons-vue';
import EchartsPlus from '@/components/Echarts-plus.vue';
import cardApi from '~/vgpu/api/card';
import { timeParse } from '@/utils';
import { getLineOptions } from '~/vgpu/views/monitor/overview/getOptions';
import { getLineOptions as getLineOptions2 } from '~/vgpu/components/config';
import TimeSelect from '~/vgpu/components/timeSelect.vue';
import { getRangeOptions } from './getOptions';
const props = defineProps([
'title',
'detailColumns',
'type',
'detail',
'name',
'filters',
'hideCp',
]);
const route = useRoute();
const detail = ref({});
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000);
const times = ref([start, end]);
const columns = [
{
label: '显卡状态',
value: 'health',
render: ({ health, isExternal }) => {
if (detail.value && detail.value.health !== undefined) {
const text = health ? '健康' : '硬件错误';
const color = health ? '#2563eb' : '#EF4444';
return (
<div
style={{
color,
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: '5px',
}}
>
<el-tag disable-transitions type={isExternal ? 'warning' : (health ? 'success' : 'danger')}>
{isExternal ? '未纳管' : (health ? '健康' : '硬件错误')}
</el-tag>
{!health && (
<ElPopover trigger="hover" popper-style={{ width: '190px' }}>
{{
reference: () => (
<el-icon color="#939EA9" size="14">
<QuestionFilled />
</el-icon>
),
default: () => (
<span style={{ marginLeft: '5px' }}>
请排查该 GPU 硬件问题
</span>
),
}}
</ElPopover>
)}
</div>
);
} else {
return <el-tag disable-transitions size="small" type="info">加载中...</el-tag>;
}
},
},
// {
// label: '',
// value: 'uuid',
// render: ({ type }) => <span>{type?.split('-')[0]}</span>,
// },
{
label: '显卡 ID',
value: 'uuid',
render: ({ uuid }) => <text-plus text={uuid} copy />,
},
{
label: '所属节点',
value: 'nodeName',
},
{
label: '显卡型号',
value: 'type',
},
{
label: '设备号',
value: 'device_no',
},
{
label: '驱动版本',
value: 'driver_version',
},
];
const cp = useInstantVector(
[
{
label: 'vGPU超配',
count: '0',
query: `avg(sum(hami_vgpu_count{node=~"$node"}) by (instance))`,
},
{
label: '算力超配',
count: '0',
query: `avg(sum(hami_vcore_scaling{node=~"$node"}) by (instance))`,
},
{
label: '显存超配',
count: '1.5',
query: `avg(sum(hami_vmemory_scaling{node=~"$node"}) by (instance))`,
},
],
(query) => query.replaceAll('$node', props.detail.name),
);
const gaugeConfig = useInstantVector(
[
{
title: '算力分配率',
percent: 0,
query: `avg(sum(hami_container_vcore_allocated{deviceuuid=~"$deviceuuid"}) by (instance))`,
totalQuery: `avg(sum(hami_core_size{deviceuuid=~"$deviceuuid"}) by (instance))`,
percentQuery: `avg(sum(hami_container_vcore_allocated{deviceuuid=~"$deviceuuid"}) by (instance))/avg(sum(hami_core_size{deviceuuid=~"$deviceuuid"}) by (instance)) *100`,
total: 0,
used: 0,
unit: ' ',
},
{
title: '显存分配率',
percent: 0,
query: `avg(sum(hami_container_vmemory_allocated{deviceuuid=~"$deviceuuid"}) by (instance)) / 1024`,
totalQuery: `avg(sum(hami_memory_size{deviceuuid=~"$deviceuuid"}) by (instance)) / 1024`,
percentQuery: `(avg(sum(hami_container_vmemory_allocated{deviceuuid=~"$deviceuuid"}) by (instance)) / 1024 )/(avg(sum(hami_memory_size{deviceuuid=~"$deviceuuid"}) by (instance)) / 1024) *100 `,
total: 0,
used: 0,
unit: 'GiB',
},
{
title: '算力使用率',
percent: 0,
query: `avg(sum(hami_core_util{deviceuuid=~"$deviceuuid"}) by (instance))`,
percentQuery: `avg(sum(hami_core_util_avg{deviceuuid=~"$deviceuuid"}) by (instance))`,
total: 100,
used: 0,
unit: ' ',
},
{
title: '显存使用率',
percent: 0,
query: `avg(sum(hami_memory_used{deviceuuid=~"$deviceuuid"}) by (instance)) / 1024`,
totalQuery: `avg(sum(hami_memory_size{deviceuuid=~"$deviceuuid"}) by (instance))/1024`,
percentQuery: `(avg(sum(hami_memory_used{deviceuuid=~"$deviceuuid"}) by (instance)) / 1024)/(avg(sum(hami_memory_size{deviceuuid=~"$deviceuuid"}) by (instance))/1024)*100`,
total: 0,
used: 0,
unit: 'GiB',
},
],
(query) => query.replaceAll(`$deviceuuid`, route.params.uuid),
times,
);
const lineTools = ref([
{
title: 'GPU功率 (W)',
query: `avg by (device_no,driver_version) (hami_device_power{deviceuuid=~"$deviceuuid"})`,
data: [],
unit: 'W',
gaugeUnit: 'W',
percent: 0,
total: 0,
hideInfo: true,
},
{
title: 'GPU 温度(℃)',
query: `avg by (device_no,driver_version) (hami_device_temperature{deviceuuid=~"$deviceuuid"})`,
data: [],
unit: '℃',
gaugeUnit: '℃',
percent: 0,
total: 0,
hideInfo: true,
},
]);
const fetchLineData = async () => {
lineTools.value.map((item, index) => {
cardApi
.getRangeVector({
range: {
start: timeParse(times.value[0]),
end: timeParse(times.value[1]),
step: '1m',
},
query: item.query.replaceAll(`$deviceuuid`, route.params.uuid),
})
.then((res) => {
const { device_no, driver_version } = res.data[0].metric;
if (device_no && driver_version) {
detail.value = { ...detail.value, device_no, driver_version };
}
lineTools.value[index].data = res.data[0]?.values || [];
});
cardApi
.getInstantVector({
query: item.query.replaceAll(`$deviceuuid`, route.params.uuid),
})
.then((res) => {
lineTools.value[index].percent = res.data[0]?.value || 0;
});
});
};
onMounted(async () => {
const res = await cardApi.getCardDetail({ uid: route.params.uuid });
detail.value = { ...detail.value, ...res };
});
watch(
times,
() => {
fetchLineData();
},
{ immediate: true },
);
</script>
<style lang="scss">
.card-detail {
display: flex;
height: 100%;
gap: 50px;
ul {
margin: 0;
padding: 0;
list-style: none;
}
.title {
color: #1d2b3a;
font-family: 'PingFang SC';
font-size: 14px;
font-style: normal;
font-weight: 500;
//line-height: 20px;
margin-bottom: 20px;
}
.card-detail-left {
min-width: 1050px;
}
.card-detail-info {
//display: flex;
//flex-direction: column;
gap: 15px;
font-size: 12px;
display: grid;
grid-template-columns: 3fr 2fr;
.label {
display: inline-block;
width: 80px;
height: 20px;
color: #939ea9;
}
.cp {
display: flex;
gap: 25px;
}
}
}
.card-gauges {
margin: 0;
padding: 0;
list-style: none;
display: flex;
height: 200px;
li {
flex: 1;
}
}
.line-box {
display: grid;
grid-template-columns: repeat(2, 1fr);
column-gap: 20px;
}
.node-block {
display: flex;
flex-direction: column;
.home-block-content {
flex: 1;
}
}
</style>

@ -0,0 +1,82 @@
<template>
<Detail
title="显卡"
:detailColumns="columns"
type="deviceuuid"
:detail="detail"
:name="detail.uuid"
:filters="{ deviceId: detail.uuid }"
hideCp="true"
/>
</template>
<script setup lang="jsx">
import Detail from '~/vgpu/components/Detail.vue';
import { onMounted, ref } from 'vue';
import nodeApi from '~/vgpu/api/node';
import { useRoute } from 'vue-router';
import cardApi from '~/vgpu/api/card';
const detail = ref({});
const route = useRoute();
const columns = [
// {
// label: '',
// value: 'uuid',
// render: ({ type }) => <span>{type?.split('-')[0]}</span>,
// },
{
label: '显卡 ID',
value: 'uuid',
},
{
label: '所属节点',
value: 'nodeName',
},
{
label: '显卡型号',
value: 'type',
},
// {
// label: 'vGPU(/)',
// value: 'vgpu',
// render: ({ vgpuTotal, vgpuUsed }) => (
// <span>
// {vgpuUsed}/{vgpuTotal}
// </span>
// ),
// },
// {
// label: '(/)',
// value: 'allocatedCores',
// render: ({ coreTotal, coreUsed }) => (
// <span>
// {coreUsed}/{coreTotal}
// </span>
// ),
// },
// {
// label: '(/)',
// value: 'allocatedDevices',
// render: ({ memoryTotal, memoryUsed }) => (
// <span>
// {memoryUsed}/{memoryTotal} M
// </span>
// ),
// },
// {
// label: '',
// value: 'DCGM_FI_DRIVER_VERSION',
// },
// {
// label: '',
// value: 'device',
// },
];
onMounted(async () => {
detail.value = await cardApi.getCardDetail({ uid: route.params.uuid });
});
</script>

@ -0,0 +1,126 @@
import { timeParse } from '@/utils';
export const getRangeOptions = ({ core = [], memory = [] }) => {
return {
legend: {
// data: [],
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
},
formatter: function (params) {
var res = params[0].name + '<br/>';
for (var i = 0; i < params.length; i++) {
res +=
params[i].marker +
params[i].seriesName +
' : ' +
(+params[i].value).toFixed(0) +
`%<br/>`;
}
return res;
},
},
grid: {
top: 37, // 上边距
bottom: 20, // 下边距
left: '7%', // 左边距
right: 10, // 右边距
},
xAxis: {
type: 'category',
data: core.map((item) => timeParse(+item.timestamp)),
axisLabel: {
formatter: function (value) {
return timeParse(value, 'HH:mm');
// return timeParse(value, 'MM-DD');
},
// interval: function (index, value) {
// var date = new Date(value);
//
// return date.getHours() % 2 === 0 && date.getMinutes() === 0;
// },
},
},
yAxis: {
type: 'value',
// max: 100,
axisLabel: {
formatter: function (value) {
return `${value} %`;
},
},
},
series: [
{
name: '算力',
data: core,
type: 'line',
areaStyle: {
normal: {
color: {
type: 'linear',
x: 0, // 渐变起始点 0%
y: 0, // 渐变起始点 0%
x2: 0, // 渐变结束点 100%
y2: 1, // 渐变结束点 100%
colorStops: [
{
offset: 0,
color: 'rgba(84, 112, 198, 0.16)', // 渐变起始颜色
},
{
offset: 1,
color: 'rgba(84, 112, 198, 0.00)', // 渐变结束颜色
},
],
global: false, // 缺省为 false
},
},
},
itemStyle: {
color: 'rgb(84, 112, 198)', // 设置线条颜色为橙色
},
lineStyle: {
color: 'rgb(84, 112, 198)', // 设置线条颜色为橙色
},
},
{
name: '显存',
data: memory,
type: 'line',
areaStyle: {
normal: {
color: {
type: 'linear',
x: 0, // 渐变起始点 0%
y: 0, // 渐变起始点 0%
x2: 0, // 渐变结束点 100%
y2: 1, // 渐变结束点 100%
colorStops: [
{
offset: 0,
color: 'rgba(34, 139, 34, 0.16)', // 渐变起始颜色
},
{
offset: 1,
color: 'rgba(34, 139, 34, 0.00)', // 渐变结束颜色
},
],
global: false, // 缺省为 false
},
},
},
itemStyle: {
color: 'rgb(145, 204, 117)', // 设置线条颜色为橙色
},
lineStyle: {
color: 'rgb(145, 204, 117)', // 设置线条颜色为橙色
},
},
],
};
};

@ -0,0 +1,167 @@
<template>
<list-header
v-if="!hideTitle"
description="显卡管理用于监控物理显卡的状态。它用于监控物理显卡的分配使用情况,以及查看物理显卡上运行的所有任务。"
/>
<preview-bar
v-if="!hideTitle"
title="显卡"
type="deviceuuid"
:handle-click="handleClick"
:handle-pie-click="handlePieClick"
:currentName="tableRef.currentParams?.filters?.type"
/>
<table-plus
:api="cardApi.getCardList({ filters })"
:columns="columns"
:rowAction="rowAction"
:searchSchema="searchSchema"
hideTag
style="height: auto"
ref="tableRef"
staticPage
>
</table-plus>
</template>
<script setup lang="jsx">
import cardApi from '~/vgpu/api/card';
import { useRouter } from 'vue-router';
import searchSchema from '~/vgpu/views/card/admin/searchSchema';
import PreviewBar from '~/vgpu/components/previewBar.vue';
import { defineProps, ref, watch } from 'vue';
import { roundToDecimal } from '@/utils';
const props = defineProps(['hideTitle', 'filters']);
const router = useRouter();
const tableRef = ref({});
const handleClick = (params) => {
router.push({
path: `/admin/vgpu/card/admin/${params.data.name}`,
});
};
const columns = [
{
title: '显卡 ID',
dataIndex: 'uuid',
render: ({ uuid }) => (
<text-plus text={uuid} to={`/admin/vgpu/card/admin/${uuid}`} />
),
},
{
title: '显卡状态',
dataIndex: 'health',
render: ({ health, isExternal }) => (
<el-tag disable-transitions type={isExternal ? 'warning' : (health ? 'success' : 'danger')}>
{isExternal ? '未纳管' : (health ? '健康' : '硬件错误')}
</el-tag>
)
},
{
title: '所属节点',
dataIndex: 'nodeName',
},
{
title: '显卡型号',
dataIndex: 'type',
},
{
title: 'vGPU',
dataIndex: 'used',
render: ({ vgpuTotal, vgpuUsed, isExternal }) => (
<span>
{isExternal ? '--' : vgpuUsed}/{isExternal ? '--' : vgpuTotal}
</span>
),
},
{
title: '算力(已分配/总量)',
dataIndex: 'used',
render: ({ coreTotal, coreUsed, isExternal }) => (
<span>
{isExternal ? '--' : coreUsed}/{coreTotal}
</span>
),
},
{
title: '显存(已分配/总量)',
dataIndex: 'w',
render: ({ memoryTotal, memoryUsed, isExternal }) => (
<span>
{isExternal ? '--' : roundToDecimal(memoryUsed / 1024, 1)}/
{roundToDecimal(memoryTotal / 1024, 1)} GiB
</span>
),
},
];
const rowAction = [
{
title: '查看详情',
onClick: (row) => {
router.push({
path: `/admin/vgpu/card/admin/${row.uuid}`,
});
},
},
];
const PieRef = ref();
const handlePieClick = (params, echarts) => {
PieRef.value = echarts;
const name = params.data.name;
if (tableRef.value.currentParams.filters.type === name) {
echarts.dispatchAction({
type: 'downplay',
seriesIndex: 0,
});
return delete tableRef.value.currentParams.filters.type;
}
echarts.dispatchAction({
type: 'downplay',
seriesIndex: 0,
});
echarts.dispatchAction({
type: 'highlight',
seriesIndex: 0,
dataIndex: params.dataIndex,
});
tableRef.value.currentParams.filters.type = name;
};
watch(
() => tableRef.value.currentParams?.filters?.type,
(newVal) => {
if (!PieRef.value) return;
const data = PieRef.value.getOption().series[0].data;
if (newVal) {
PieRef.value.dispatchAction({
type: 'downplay',
seriesIndex: 0,
});
PieRef.value.dispatchAction({
type: 'highlight',
seriesIndex: 0,
dataIndex: data.findIndex((item) => item.name === newVal),
});
} else {
PieRef.value.dispatchAction({
type: 'downplay',
seriesIndex: 0,
});
}
},
);
</script>
<style></style>

@ -0,0 +1,34 @@
import nodeApi from '~/vgpu/api/node';
import cardApi from '~/vgpu/api/card';
export default {
items: [
{
label: '显卡 ID',
name: 'uid',
component: 'input',
},
{
label: '所属节点',
name: 'nodeName',
component: 'select',
props: {
mode: 'remote',
api: nodeApi.getNodeList({filters:{}}),
labelKey: 'name',
valueKey: 'name',
},
},
{
label: '显卡型号',
name: 'type',
component: 'select',
props: {
mode: 'remote',
api: cardApi.getCardType(),
labelKey: 'type',
valueKey: 'type',
},
},
],
};

@ -0,0 +1,3 @@
<template>
<router-view></router-view>
</template>

@ -0,0 +1,3 @@
<template>
<router-view></router-view>
</template>

@ -0,0 +1,39 @@
<template>
<div class="home-block">
<div class="home-block-header" v-if="title">
<div class="title">{{ title }}</div>
<div class="extra">
<slot name="extra" />
</div>
</div>
<div class="home-block-content"><slot /></div>
</div>
</template>
<script setup>
defineProps(['title']);
</script>
<style lang="scss">
.home-block {
border-radius: 8px;
background: #fff;
box-shadow: 0px 1px 1px 0px rgba(2, 5, 8, 0.02),
0px 1px 4px 0px rgba(2, 5, 8, 0.06);
padding: 20px;
&-header {
display: flex;
justify-content: space-between;
.title {
color: #1d2b3a;
font-family: 'PingFang SC';
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
margin-bottom: 20px;
}
}
}
</style>

@ -0,0 +1,179 @@
export const rangeConfigInit = [
{
title: '资源分配趋势',
dataSource: [
{
name: 'vGPU',
query: `sum(hami_container_vgpu_allocated) / sum(hami_vgpu_count) * 100`,
data: [],
type: 'line',
areaStyle: {
normal: {
color: {
type: 'linear',
x: 0, // 渐变起始点 0%
y: 0, // 渐变起始点 0%
x2: 0, // 渐变结束点 100%
y2: 1, // 渐变结束点 100%
colorStops: [
{
offset: 0,
color: 'rgba(250, 200, 88, 0.16)', // 渐变起始颜色
},
{
offset: 1,
color: 'rgba(250, 200, 88, 0.00)', // 渐变结束颜色
},
],
global: false, // 缺省为 false
},
},
},
itemStyle: {
color: 'rgb(250, 200, 88)', // 设置线条颜色为橙色
},
lineStyle: {
color: 'rgb(250, 200, 88)', // 设置线条颜色为橙色
},
},
{
name: '算力',
query: `sum(hami_container_vcore_allocated) / sum(hami_core_size) * 100`,
data: [],
type: 'line',
areaStyle: {
normal: {
color: {
type: 'linear',
x: 0, // 渐变起始点 0%
y: 0, // 渐变起始点 0%
x2: 0, // 渐变结束点 100%
y2: 1, // 渐变结束点 100%
colorStops: [
{
offset: 0,
color: 'rgba(84, 112, 198, 0.16)', // 渐变起始颜色
},
{
offset: 1,
color: 'rgba(84, 112, 198, 0.00)', // 渐变结束颜色
},
],
global: false, // 缺省为 false
},
},
},
itemStyle: {
color: 'rgb(84, 112, 198)', // 设置线条颜色为橙色
},
lineStyle: {
color: 'rgb(84, 112, 198)', // 设置线条颜色为橙色
},
},
{
name: '显存',
query: `sum(hami_container_vmemory_allocated) / sum(hami_memory_size) * 100`,
data: [],
areaStyle: {
normal: {
color: {
type: 'linear',
x: 0, // 渐变起始点 0%
y: 0, // 渐变起始点 0%
x2: 0, // 渐变结束点 100%
y2: 1, // 渐变结束点 100%
colorStops: [
{
offset: 0,
color: 'rgba(34, 139, 34, 0.16)', // 渐变起始颜色
},
{
offset: 1,
color: 'rgba(34, 139, 34, 0.00)', // 渐变结束颜色
},
],
global: false, // 缺省为 false
},
},
},
itemStyle: {
color: 'rgb(145, 204, 117)', // 设置线条颜色为橙色
},
lineStyle: {
color: 'rgb(145, 204, 117)', // 设置线条颜色为橙色
},
},
],
},
{
title: '资源使用趋势',
dataSource: [
{
name: '算力',
query: `avg(hami_core_util_avg)`,
data: [],
areaStyle: {
normal: {
color: {
type: 'linear',
x: 0, // 渐变起始点 0%
y: 0, // 渐变起始点 0%
x2: 0, // 渐变结束点 100%
y2: 1, // 渐变结束点 100%
colorStops: [
{
offset: 0,
color: 'rgba(84, 112, 198, 0.16)', // 渐变起始颜色
},
{
offset: 1,
color: 'rgba(84, 112, 198, 0.00)', // 渐变结束颜色
},
],
global: false, // 缺省为 false
},
},
},
itemStyle: {
color: 'rgb(84, 112, 198)', // 设置线条颜色为橙色
},
lineStyle: {
color: 'rgb(84, 112, 198)', // 设置线条颜色为橙色
},
},
{
name: '显存',
query: `sum(hami_memory_used) / sum(hami_memory_size) * 100`,
data: [],
areaStyle: {
normal: {
color: {
type: 'linear',
x: 0, // 渐变起始点 0%
y: 0, // 渐变起始点 0%
x2: 0, // 渐变结束点 100%
y2: 1, // 渐变结束点 100%
colorStops: [
{
offset: 0,
color: 'rgba(34, 139, 34, 0.16)', // 渐变起始颜色
},
{
offset: 1,
color: 'rgba(34, 139, 34, 0.00)', // 渐变结束颜色
},
],
global: false, // 缺省为 false
},
},
},
itemStyle: {
color: 'rgb(145, 204, 117)', // 设置线条颜色为橙色
},
lineStyle: {
color: 'rgb(145, 204, 117)', // 设置线条颜色为橙色
},
},
],
},
];

@ -0,0 +1,560 @@
import { timeParse } from '@/utils';
import { cloneDeep } from 'lodash';
import nodeApi from '~/vgpu/api/node';
import { ElMessage } from 'element-plus';
export const getResourceStatus = (statusConfig) => {
return {
tooltip: {
show: false,
},
// legend: {
// top: '5%',
// left: 'center',
// },
series: [
{
name: 'Access From',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 3,
borderColor: '#fff',
},
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: false,
fontSize: 40,
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
// data: [
// { value: data.running, itemStyle: {color: '#ff0000'} },
// { value: data.stopped , itemStyle: {color: '#ff0000'}},
// { value: data.error , itemStyle: {color: '#ff0000'}},
// ],
data: statusConfig.map((item) => ({
...item,
itemStyle: { color: item.color },
})),
},
],
grid: {
top: 1, // 上边距
bottom: 1, // 下边距
left: 1, // 左边距
right: 1, // 右边距
},
graphic: [
{
type: 'text', // 添加文本标签
left: 'center', // 文本标签水平居中
top: 'center', // 文本标签垂直居中
style: {
text: '节点', // 设置文本内容
fill: '#333', // 文字颜色
fontSize: 12, // 文字大小
},
},
],
};
};
export const getPressure = ({ percent = 0, title }) => {
let thisColor = '';
if (percent < 30) {
thisColor = '#16A34A';
} else if (percent >= 30 && percent <= 80) {
thisColor = '#2563EB';
} else {
thisColor = '#DC2626';
}
return {
series: [
{
type: 'gauge',
itemStyle: {
color: thisColor,
// shadowColor: thisColor,
// shadowBlur: 5,
// shadowOffsetX: 2,
// shadowOffsetY: 2,
},
progress: {
show: true,
width: 8,
},
axisLine: {
lineStyle: {
width: 8,
backgroundColor: '#F5F7FA',
},
},
axisTick: {
show: false,
},
axisLabel: {
show: false, // 隐藏刻度标签
},
// splitNumber: 0,
splitLine: {
length: 3,
lineStyle: {
width: 2,
color: '#999',
},
distance: 3,
},
anchor: {
show: false,
showAbove: false,
size: 25,
itemStyle: {
borderWidth: 10,
},
},
pointer: {
show: false,
},
title: {
show: false,
},
detail: {
valueAnimation: true,
width: '60%',
lineHeight: 40,
borderRadius: 8,
offsetCenter: [0, '0%'],
// fontSize: 16,
fontWeight: 'bolder',
formatter: '{a|{value}}{b|%}',
rich: {
a: {
color: '#1D2B3A',
lineHeight: 10,
fontSize: 20,
},
b: {
color: '#1D2B3A',
},
},
},
data: [
{
value: percent.toFixed(1),
},
],
},
],
graphic: [
{
type: 'text',
left: 'center',
bottom: 8,
style: {
text: title,
fill: '#333', // 设置标题文字颜色
fontSize: 14, // 设置标题文字大小
borderRadius: 999,
backgroundColor: '#F5F7FA',
padding: [6, 16],
},
},
],
};
};
export const getAlarm = ({ timeKeys, data }) => {
return {
legend: {
// data: [],
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
},
},
xAxis: {
type: 'category',
data: data.map((item) => timeParse(+item.timestamp)),
axisLabel: {
formatter: function (value) {
return timeParse(value, 'HH:mm');
},
},
},
yAxis: {
type: 'value',
axisLabel: {
formatter: function (value) {
return `${value}`;
},
},
},
series: [
{
data,
type: 'line',
smooth: true,
showSymbol: true,
areaStyle: {
normal: {
color: {
type: 'linear',
x: 0, // 渐变起始点 0%
y: 0, // 渐变起始点 0%
x2: 0, // 渐变结束点 100%
y2: 1, // 渐变结束点 100%
colorStops: [
{
offset: 0,
color: 'rgba(37, 99, 235, 0.16)', // 渐变起始颜色
},
{
offset: 1,
color: 'rgba(37, 99, 235, 0.00)', // 渐变结束颜色
},
],
global: false, // 缺省为 false
},
},
},
itemStyle: {
color: 'rgb(84, 112, 198)', // 设置线条颜色为橙色
},
lineStyle: {
color: 'rgb(84, 112, 198)', // 设置线条颜色为橙色
},
},
],
grid: {
top: '3%', // 上边距
bottom: '8%', // 下边距
left: '8%', // 左边距
right: '3%', // 右边距
},
graphic: [
{
type: 'text', // 添加文本标签
left: '23%', // 文本标签水平居中
top: 'center', // 文本标签垂直居中
style: {
text: '显卡', // 设置文本内容
fill: '#333', // 文字颜色
fontSize: 12, // 文字大小
},
},
// {
// type: 'text', // 添加文本标签
// left: 'center', // 文本标签水平居中
// top: '170', // 文本标签垂直居中
// style: {
// text: list.length + '个', // 设置文本内容
// fill: '#333', // 文字颜色
// fontSize: 16, // 文字大小
// },
// },
],
};
};
export const handleChartClick = async (params, router) => {
const name = params.data.name;
const { list } = await nodeApi.getNodes({ filters: {} });
const node = list.find((node) => node.name === name);
if (node) {
const uuid = node.uid;
router.push(`/admin/vgpu/node/admin/${uuid}?nodeName=${name}`);
} else {
ElMessage.error('节点未找到');
}
};
export const getCardOptions = (list, chartWidth) => {
const data = list.reduce((all, current) => {
const name = current.type;
if (all[name]) {
all[name]++;
} else {
all[name] = 1;
}
return all;
}, {});
const dataList = Object.entries(data);
return {
tooltip: {
show: false,
},
series: [
{
type: 'pie',
radius: ['50%', '65%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 1,
borderColor: '#fff',
},
label: {
alignTo: 'edge',
formatter: '{name|{b}}\n{cnt|{c} 张}',
minMargin: 5,
edgeDistance: 10,
lineHeight: 15,
rich: {
cnt: {
fontSize: 10,
color: '#999'
}
}
},
labelLayout: function (params) {
const isLeft = params.labelRect.x < chartWidth / 2;
const points = params.labelLinePoints;
points[2][0] = isLeft
? params.labelRect.x
: params.labelRect.x + params.labelRect.width;
return {
labelLinePoints: points,
};
},
data: dataList.map(([key, value]) => ({
name: key,
value: value,
})),
},
],
};
};
export const getLineOptions = ({
title,
usedData = [],
totalData = [],
unit = '%',
}) => {
const xData = usedData;
let yData = cloneDeep(totalData);
const xAxisData = usedData.length ? usedData : totalData;
return {
legend: {
data: ['分配率', usedData.length && '使用率'],
// left: 0,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
},
formatter: function (params) {
var res = timeParse(+params[0].name) + '<br/>';
for (var i = 0; i < params.length; i++) {
res +=
params[i].marker +
params[i].seriesName +
' : ' +
(+params[i].value).toFixed(0) +
`${unit}<br/>`;
}
return res;
},
},
grid: {
top: 37, // 上边距
bottom: 20, // 下边距
left: '7%', // 左边距
right: 10, // 右边距
},
xAxis: {
type: 'category',
data: xAxisData.map((item) => timeParse(+item.timestamp)),
axisLabel: {
formatter: function (value) {
return timeParse(value, 'HH:mm');
// return timeParse(value, 'MM-DD');
},
// interval: function (index, value) {
// var date = new Date(value);
//
// return date.getHours() % 2 === 0 && date.getMinutes() === 0;
// },
},
},
yAxis: {
type: 'value',
max: 100,
axisLabel: {
formatter: function (value) {
return `${value} ${unit}`;
},
},
},
series: [
{
name: '使用率',
data: usedData.map((item) => {
if (title === 'GPU 内存使用') {
return (item.value / 1024).toFixed(1);
}
return { value: item.value.toFixed(1), name: item.timestamp };
}),
type: 'line',
areaStyle: {
normal: {
color: {
type: 'linear',
x: 0, // 渐变起始点 0%
y: 0, // 渐变起始点 0%
x2: 0, // 渐变结束点 100%
y2: 1, // 渐变结束点 100%
colorStops: [
{
offset: 0,
color: 'rgba(84, 112, 198, 0.16)', // 渐变起始颜色
},
{
offset: 1,
color: 'rgba(84, 112, 198, 0.00)', // 渐变结束颜色
},
],
global: false, // 缺省为 false
},
},
},
},
{
name: '分配率',
data: yData.map((item) => {
if (!item) return item;
return { value: item.value.toFixed(1), name: item.timestamp };
}),
type: 'line',
areaStyle: {
normal: {
color: {
type: 'linear',
x: 0, // 渐变起始点 0%
y: 0, // 渐变起始点 0%
x2: 0, // 渐变结束点 100%
y2: 1, // 渐变结束点 100%
colorStops: [
{
offset: 0,
color: 'rgba(34, 139, 34, 0.16)', // 渐变起始颜色
},
{
offset: 1,
color: 'rgba(34, 139, 34, 0.00)', // 渐变结束颜色
},
],
global: false, // 缺省为 false
},
},
},
},
],
};
};
export const getRangeOptions = (data) => {
return {
legend: {
// data: [],
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
},
formatter: function (params) {
var res = params[0].name + '<br/>';
for (var i = 0; i < params.length; i++) {
res +=
params[i].marker +
params[i].seriesName +
' : ' +
(+params[i].value).toFixed(0) +
`%<br/>`;
}
return res;
},
},
grid: {
top: 37, // 上边距
bottom: 20, // 下边距
left: '7%', // 左边距
right: 10, // 右边距
},
xAxis: {
type: 'category',
data: data[0].data.map((item) => timeParse(+item.timestamp)),
axisLabel: {
formatter: function (value) {
return timeParse(value, 'HH:mm');
// return timeParse(value, 'MM-DD');
},
// interval: function (index, value) {
// var date = new Date(value);
//
// return date.getHours() % 2 === 0 && date.getMinutes() === 0;
// },
},
},
yAxis: {
type: 'value',
// max: 100,
axisLabel: {
formatter: function (value) {
return `${value} %`;
},
},
},
series: data.map((item) => ({
...item,
type: 'line',
// areaStyle: {
// normal: {
// color: {
// type: 'linear',
// x: 0, // 渐变起始点 0%
// y: 0, // 渐变起始点 0%
// x2: 0, // 渐变结束点 100%
// y2: 1, // 渐变结束点 100%
// colorStops: [
// {
// offset: 0,
// color: 'rgba(34, 139, 34, 0.16)', // 渐变起始颜色
// },
// {
// offset: 1,
// color: 'rgba(34, 139, 34, 0.00)', // 渐变结束颜色
// },
// ],
// global: false, // 缺省为 false
// },
// },
// },
})),
};
};

@ -0,0 +1,486 @@
<template>
<div class="home">
<div class="home-left">
<Block title="显卡资源">
<template #extra>
<div class="all-btn" @click="router.push('/admin/vgpu/card/admin')">
全部<svg-icon icon="more" style="margin-left: 4px" />
</div>
</template>
<div class="card-overview">
<div v-for="item in cardGaugeConfig" :key="item.title">
<Gauge v-bind="item" />
</div>
</div>
</Block>
<Block title="资源总览">
<template #extra>
<div class="all-btn" @click="router.push('/admin/vgpu/card/admin')">
全部<svg-icon icon="more" style="margin-left: 4px" />
</div>
</template>
<ul class="resourceOverview">
<li
v-for="{ title, count, icon, to, unit } in resourceOverview"
:key="title"
@click="router.push(to)"
>
<div class="avatar">
<svg-icon :icon="icon" />
</div>
<div class="main">
<div>
{{ title }}
</div>
<div class="count">
{{ count }} <span style="font-size: 12px">{{ unit }}</span>
</div>
</div>
</li>
</ul>
</Block>
<Block v-for="{ title, dataSource } in rangeConfig" :title="title" :key="title">
<template #extra>
<time-picker v-model="times" type="datetimerange" size="small" />
</template>
<echarts-plus
:options="getRangeOptions(dataSource)"
style="height: 250px"
/>
</Block>
</div>
<div class="home-right">
<Block title="节点总览" style="margin-bottom: 16px">
<template #extra>
<div class="all-btn" @click="router.push('/admin/vgpu/node/admin')">
全部<svg-icon icon="more" style="margin-left: 4px" />
</div>
</template>
<ul class="node-all">
<li
v-for="{ title, status, count, color } in nodes"
:key="title"
@click="
router.push(`/admin/vgpu/node/admin?isSchedulable=${status}`)
"
>
<div class="title">{{ title }}</div>
<div class="count" :style="{ color }">
{{ count }}
</div>
</li>
</ul>
</Block>
<Block title="显卡类型分布" style="margin-bottom: 16px">
<template #extra>
<div class="all-btn" @click="router.push('/admin/vgpu/card/admin')">
全部<svg-icon icon="more" style="margin-left: 4px" />
</div>
</template>
<div style="height: 218px">
<echarts-plus :options="getCardOptions(cardData, chartWidth)" :onClick="handlePieClick" />
</div>
</Block>
<TabTop
v-bind="nodeTotalTop"
:onClick="(params) => handleChartClick(params, router)"
style="margin-bottom: 16px"
/>
<TabTop
v-bind="nodeUsedTop"
:onClick="(params) => handleChartClick(params, router)"
/>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, computed, reactive, watch, watchEffect } from 'vue';
import {
getCardOptions,
handleChartClick,
getRangeOptions,
} from './getOptions';
import Block from './Block.vue';
import './style.scss';
import { timeParse, getDaysInRange, getRandom } from '@/utils';
import { useRouter } from 'vue-router';
import UserCard from '@/components/UserCard.vue';
import nodeApi from '~/vgpu/api/node';
import taskApi from '~/vgpu/api/task';
import monitorApi from '~/vgpu/api/monitor';
import cardApi from '~/vgpu/api/card';
import useInstantVector from '~/vgpu/hooks/useInstantVector';
import useFetchList from '@/hooks/useFetchList';
import { getTopOptions } from '~/vgpu/components/config';
import EchartsPlus from '@/components/Echarts-plus.vue';
import TabTop from '~/vgpu/components/TabTop.vue';
import TimeSelect from '~/vgpu/components/timeSelect.vue';
import Gauge from '~/vgpu/components/gauge.vue';
import { rangeConfigInit } from './config';
const router = useRouter();
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000);
const times = ref([start, end]);
const handlePieClick = (params) => {
router.push(`/admin/vgpu/card/admin?type=${params.data.name}`);
};
const alarmData = ref([])
const chartWidth = ref(200);
const cardGaugeConfig = useInstantVector([
{
title: 'vGPU 分配率',
percent: 0,
query: `avg(sum (hami_container_vgpu_allocated) by (instance))`,
totalQuery: `avg(sum (hami_vgpu_count) by (instance))`,
percentQuery: `avg(sum (hami_container_vgpu_allocated) by (instance))/avg(sum (hami_vgpu_count) by (instance)) *100`,
total: 0,
used: 0,
unit: '个',
},
{
title: '算力分配率',
percent: 0,
query: `avg(sum(hami_container_vcore_allocated) by (instance))`,
totalQuery: `avg(sum(hami_core_size) by (instance))`,
percentQuery: `avg(sum(hami_container_vcore_allocated) by (instance))/avg(sum(hami_core_size) by (instance)) *100`,
total: 0,
used: 0,
unit: ' ',
},
{
title: '显存分配率',
percent: 0,
query: `avg(sum(hami_container_vmemory_allocated) by (instance)) / 1024`,
totalQuery: `avg(sum(hami_memory_size) by (instance)) / 1024`,
percentQuery: `(avg(sum(hami_container_vmemory_allocated) by (instance)) / 1024 )/(avg(sum(hami_memory_size) by (instance)) / 1024) *100 `,
total: 0,
used: 0,
unit: 'GiB',
},
{
title: '算力使用率',
percent: 0,
query: `avg(sum(hami_core_util) by (instance))`,
percentQuery: `avg(sum(hami_core_util_avg) by (instance))`,
totalQuery: `avg(sum(hami_core_size) by (instance))`,
total: 100,
used: 0,
unit: ' ',
},
{
title: '显存使用率',
percent: 0,
query: `avg(sum(hami_memory_used) by (instance)) / 1024`,
totalQuery: `avg(sum(hami_memory_size) by (instance))/1024`,
percentQuery: `(avg(sum(hami_memory_used) by (instance)) / 1024)/(avg(sum(hami_memory_size) by (instance))/1024)*100`,
total: 0,
used: 0,
unit: 'GiB',
},
]);
const resourceOverview = ref([
{
title: '节点',
count: 0,
icon: 'vgpu-node',
unit: '个',
},
{
title: '显卡',
count: 0,
icon: 'vgpu-gpu-d',
unit: '张',
},
{
title: 'vGPU',
count: 0,
icon: 'vgpu-card',
unit: '个',
},
{
title: '算力',
count: 12,
icon: 'vgpu-core',
unit: ' ',
},
{
title: '显存',
count: 31,
icon: 'vgpu-mem',
unit: 'GiB',
},
]);
const nodeConfig = reactive({
instance: [
{ title: '可调度', key: 'yes', color: '#2563EB', value: 0 },
{ title: '禁止调度', key: 'no', color: '#DC2626', value: 0 },
],
core: [
{ title: '已分配', key: 'used', color: '#2563EB', value: 0 },
{ title: '闲置', key: 'free', color: '#B6C2CD', value: 0 },
{ title: '分配率', key: 'percent', color: '#B6C2CD', value: 0, unit: '%' },
],
memory: [
{
title: '已分配',
key: 'used',
color: '#2563EB',
value: 0,
unit: 'GiB',
},
{
title: '闲置',
key: 'free',
color: '#B6C2CD',
value: 0,
unit: 'GiB',
},
{
title: '分配率',
key: 'percent',
color: '#B6C2CD',
value: 0,
unit: '%',
},
],
});
const nodeData = useFetchList(nodeApi.getNodeListReq({ filters: {} }));
const cardData = useFetchList(cardApi.getCardListReq({ filters: {} }));
const cardDetail = useInstantVector([
{
title: 'vGPU',
count: 0,
query: 'avg(hami_vgpu_count)',
unit: '个',
icon: 'gpu2',
},
{
title: '算力',
count: 0,
unit: ' ',
icon: 'account',
query: 'avg(hami_core_size)',
},
{
title: '显存',
count: 0,
unit: 'GiB',
icon: 'volume',
query: 'avg(hami_memory_size) / 1024',
},
]);
const nodes = computed(() => [
{
title: '可调度',
count: nodeData.value.filter((item) => !item.isExternal && item.isSchedulable).length,
isSchedulable: true,
isExternal: false,
status: 'true',
color: '#16A34A',
},
{
title: '禁止调度',
count: nodeData.value.filter((item) => !item.isExternal && !item.isSchedulable).length,
isSchedulable: false,
isExternal: false,
status: 'false',
color: '#1D2B3A',
},
]);
const exceed = useInstantVector([
{ title: 'vGPU 超配', count: 0, type: 'vgpu', query: 'avg(hami_vgpu_count)' },
{
title: '算力超配',
count: 0,
type: 'core',
query: 'avg(hami_vcore_scaling)',
},
{
title: '显存超配',
count: 0,
type: 'memory',
query: 'avg(hami_vmemory_scaling)',
},
]);
const nodeUsedTop = {
title: '节点资源使用率 Top5',
key: 'used',
config: [
{
tab: '算力',
key: 'core',
nameKey: 'node',
data: [],
query: 'topk(5, avg(hami_core_util_avg) by (node))',
},
{
tab: '显存',
key: 'memory',
data: [],
nameKey: 'node',
query:
'topk(5, avg(hami_memory_used) by (node) / avg(hami_memory_size) by (node) * 100)',
},
],
};
const nodeTotalTop = {
title: '节点资源分配率 Top5',
key: 'used',
config: [
{
tab: 'vGPU',
key: 'vgpu',
nameKey: 'node',
data: [],
query: `topk(5, avg(hami_container_vgpu_allocated{}) by (node) / avg(hami_vgpu_count{}) by (node) * 100)`,
},
{
tab: '算力',
key: 'core',
nameKey: 'node',
data: [],
query:
'topk(5, avg(hami_container_vcore_allocated{}) by (node) / avg(hami_core_size{}) by (node) * 100)',
},
{
tab: '显存',
key: 'memory',
data: [],
nameKey: 'node',
query:
'topk(5, avg(hami_container_vmemory_allocated{}) by (node) / avg(hami_memory_size{}) by (node) * 100)',
},
],
};
const rangeConfig = ref(rangeConfigInit);
const fetchRangeData = () => {
const params = {
range: {
start: timeParse(times.value[0]),
end: timeParse(times.value[1]),
step: '1m',
},
};
for (const item of rangeConfig.value) {
for (const v of item.dataSource) {
cardApi
.getRangeVector({
...params,
query: v.query,
})
.then((res) => {
v.data = res.data[0].values;
});
}
}
cardApi
.getRangeVector({
...params,
query: `sum({__name__=~"alert:.*:count"})`,
})
.then((res) => {
alarmData.value = res.data[0].values;
});
};
watchEffect(() => {
resourceOverview.value[0].count = nodeData.value.length;
resourceOverview.value[1].count = cardData.value.length;
resourceOverview.value[2].count = cardGaugeConfig.value[0].total;
resourceOverview.value[3].count = cardGaugeConfig.value[1].total;
resourceOverview.value[4].count = cardGaugeConfig.value[2].total.toFixed(0);
});
onMounted(async () => {
const summary = await monitorApi.summary({
filters: {},
});
const nodeDataRes = {
yes: nodeData.value.filter((item) => item.isSchedulable).length,
no: nodeData.value.filter((item) => !item.isSchedulable).length,
};
nodeConfig.instance = nodeConfig.instance.map((item) => {
return { ...item, value: nodeDataRes[item.key] };
});
nodeConfig.core = nodeConfig.core.map((item) => {
const core_total = cardDetail.value[1].percent;
const coreData = {
percent: ((summary.coreUsed / core_total).toFixed(2) * 100).toFixed(0),
used: summary.coreUsed,
free: summary.coreTotal - summary.coreUsed,
};
return { ...item, value: coreData[item.key] };
});
nodeConfig.memory = nodeConfig.memory.map((item) => {
const memory_total = cardDetail.value[2].percent;
const coreData = {
percent: (
(summary.memoryUsed / 1024 / memory_total).toFixed(2) * 100
).toFixed(0),
used: summary.memoryUsed,
free: summary.memoryTotal - summary.memoryUsed,
};
return { ...item, value: coreData[item.key] };
});
});
watch(
times,
() => {
// fetchLineData();
fetchRangeData();
},
{ immediate: true },
);
</script>
<style>
.el-progress-bar__outer {
background-color: #b6c2cd;
}
.card-overview {
padding-bottom: 10px;
height: 190px;
display: grid;
grid-template-columns: repeat(5, 1fr);
.gauge-info {
margin-top: 10px;
}
}
</style>

@ -0,0 +1,140 @@
export const leftConfig = [
{
title: '资源状态',
morePath: '/iac/manager/instance',
list: [
{
title: '云主机分布',
data: [
{ name: '华为云', value: 400 },
{ name: '阿里云', value: 300 },
{ name: '腾讯云', value: 100 },
],
unit: '台',
},
{
title: '云服务器状态',
data: [
{ name: '开机', value: 700 },
{ name: '关机', value: 100 },
],
},
{
title: '云硬盘状态',
data: [
{ name: '使用中', value: 900 },
{ name: '闲置', value: 100 },
],
},
{
title: '云网卡状态',
data: [
{ name: '使用中', value: 300 },
{ name: '闲置', value: 200 },
],
},
],
},
{
title: '优化建议',
morePath: '/iac/manager/instance',
list: [
{ tip: '建议降配', value: 1 },
{ tip: '建议升配', value: 1 },
{ tip: '建议变更付费方式', value: 1 },
{ tip: '建议回收', value: 114 },
],
},
{
title: '资源消耗',
morePath: '/iac/manager/instance',
list: [
{
title: '按主机数用量',
data: [
{ name: '业务1', value: 400 },
{ name: '业务2', value: 300 },
{ name: '业务3', value: 100 },
],
unit: '台',
},
{
title: '按CPU用量',
data: [
{ name: '业务1', value: 400 },
{ name: '业务2', value: 200 },
{ name: '业务3', value: 500 },
],
},
{
title: '按内存用量',
data: [
{ name: '业务1', value: 200 },
{ name: '业务2', value: 100 },
{ name: '业务3', value: 100 },
],
},
{
title: '按存储数用量',
data: [
{ name: '业务1', value: 400 },
{ name: '业务2', value: 600 },
{ name: '业务3', value: 300 },
],
},
],
},
];
export const rightConfig = {
provides: [
{
title: 'CPU',
percent: 20,
},
{
title: '内存',
percent: 67,
},
{
title: '存储',
percent: 50,
},
],
approves: [
{
title: '待审批',
count: 80,
},
{
title: '审批中',
count: 15,
},
{
title: '已完成',
count: 228,
},
],
myAdmin: [
{
title: '云账号',
count: 5,
},
{
title: '云主机',
count: 572,
},
{
title: '云硬盘',
count: 803,
},
{
title: '宿主机',
count: 12,
},
{
title: '存储器',
count: 31,
},
],
};

@ -0,0 +1,13 @@
export default function resso({ color }) {
return (
<div
class="color"
style={{
background: color,
width: '6px',
height: '6px',
'border-radius': '2px',
}}
/>
);
}

@ -0,0 +1,499 @@
.home {
display: flex;
gap: 16px;
height: 100%;
font-size: 12px;
ul {
margin: 0;
padding: 0;
list-style: none;
}
.home-block {
margin-bottom: 0;
}
.all-btn {
color: #939ea9;
text-align: center;
font-family: 'PingFang SC';
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 166.667% */
cursor: pointer;
&:hover {
color: var(--el-color-primary);
}
}
&-left {
width: 65%;
display: flex;
flex-direction: column;
gap: 16px;
}
&-right {
flex: 1;
//display: flex;
//flex-direction: column;
//gap: 16px;
//width: 100%;
}
.visited {
list-style: none;
display: flex;
gap: 12px;
flex-wrap: wrap;
li {
display: flex;
padding: 8px 16px;
align-items: center;
gap: 8px;
// flex: 1 0 0;
border-radius: 4px;
border: 1px solid #e4ebf1;
background: #fff;
max-width: 180px;
&:hover {
border: 1px solid var(--el-color-primary-light-3);
color: var(--el-color-primary);
cursor: pointer;
}
}
}
.visited-empty {
text-align: center;
color: #939ea9;
}
.add-btn {
color: var(--el-color-primary);
text-align: center;
font-family: 'PingFang SC';
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 166.667% */
cursor: pointer;
&:hover {
opacity: 0.7;
}
}
.resourceOverview {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
li {
height: 80px;
flex-shrink: 0;
border-radius: 6px;
background: #f5f7fa;
padding: 16px;
display: flex;
//&:hover {
// color: var(--el-color-primary);
// cursor: pointer;
//}
.avatar {
display: flex;
width: 48px;
height: 48px;
padding: 14px;
justify-content: center;
align-items: center;
flex-shrink: 0;
border-radius: 999px;
border: 2px solid #fff;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0) 0%,
#fff 100%
);
margin-right: 16px;
}
.count {
font-family: Roboto;
font-size: 20px;
font-style: normal;
font-weight: 700;
line-height: 100%; /* 20px */
margin-top: 10px;
}
}
}
.resourceStatus {
display: flex;
gap: 20px;
min-height: 160px;
&-left {
flex: 1;
// padding: 10px 20px;
padding-right: 0;
display: flex;
align-items: center;
gap: 34px;
.pie {
height: 120px;
width: 120px;
margin-left: 20px;
}
.counts {
flex: 1;
display: flex;
justify-content: space-between;
gap: 8px;
padding-right: 40px;
flex-wrap: wrap;
&-item {
padding: 8px 12px;
display: flex;
flex-direction: column;
justify-content: center;
&-title {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 4px;
}
&-count {
color: #1d2b3a;
font-family: Roboto;
font-size: 20px;
font-style: normal;
font-weight: 700;
line-height: 100%; /* 20px */
}
}
}
}
&-right {
width: 55%;
display: flex;
align-items: center;
gap: 12px;
li {
flex: 1;
min-height: 150px;
border-radius: 6px;
background: #f5f7fa;
padding: 16px;
display: flex;
flex-direction: column;
justify-content: center;
.title {
display: flex;
gap: 4px;
align-items: center;
}
.progress {
padding: 25px 0;
background-color: transparent;
.el-progress {
width: 100%;
}
}
.volume {
display: flex;
//flex-direction: column;
gap: 8px;
flex-wrap: wrap;
}
.volume-item {
flex: 1;
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
min-width: 80px;
.title {
color: #697886;
font-family: 'PingFang SC';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 100%; /* 12px */
//width:80px ;
}
.count {
color: #1d2b3a;
font-family: Roboto;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 100%;
}
}
}
}
}
#alarm {
}
.alarm {
display: flex;
gap: 32px;
&-left {
display: flex;
flex-direction: column;
gap: 12px;
&-item {
width: 202px;
height: 120px;
border-radius: 6px;
padding: 16px;
position: relative;
.title {
color: #324558;
font-family: 'PingFang SC';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 150% */
margin-bottom: 10px;
}
.count {
font-family: Roboto;
font-size: 20px;
font-style: normal;
font-weight: 700;
line-height: 100%; /* 20px */
}
.icon-bg {
position: absolute;
right: 10px;
bottom: -10px;
width: 56px;
height: 56px;
}
}
}
&-right {
flex: 1;
position: relative;
.title {
position: absolute;
top: -40px;
color: #1d2b3a;
font-family: 'PingFang SC';
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
}
}
}
.pressure {
display: flex;
height: 150px;
li {
flex: 1;
}
}
.user {
border: 1px solid #fff;
padding: 0;
}
.myApproval {
display: flex;
li {
display: flex;
padding: 8px 0px;
flex-direction: column;
align-items: center;
gap: 8px;
flex: 1 0 0;
&:hover {
color: var(--el-color-primary);
cursor: pointer;
}
.count {
font-family: Roboto;
font-size: 20px;
font-style: normal;
font-weight: 700;
line-height: 100%; /* 20px */
position: relative;
.isNew {
display: inline-flex;
min-width: 20px;
padding: 0px 6px;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 6px 6px 6px 2px;
background: var(--light-color-semantic-danger-default, #dc2626);
color: #fff;
text-align: center;
font-family: Roboto;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 166.667% */
position: absolute;
right: -40px;
top: -8px;
}
}
}
}
.myApply {
display: grid;
gap: 12px;
grid-template-columns: repeat(2, 1fr);
li {
display: flex;
// width: 202px;
padding: 16px 16px;
align-items: center;
gap: 8px;
border-radius: 4px;
background: #f5f7fa;
justify-content: space-between;
&:hover {
color: var(--el-color-primary);
cursor: pointer;
}
.count {
font-family: Roboto;
font-size: 14px;
font-style: normal;
font-weight: 700;
line-height: 20px; /* 142.857% */
}
}
}
.node-all {
display: grid;
gap: 12px;
grid-template-columns: repeat(2, 1fr);
li {
display: flex;
// width: 202px;
padding: 16px 16px;
align-items: center;
gap: 8px;
border-radius: 4px;
background: #f5f7fa;
justify-content: space-between;
&:hover {
color: var(--el-color-primary);
cursor: pointer;
}
.count {
font-family: Roboto;
font-size: 14px;
font-style: normal;
font-weight: 700;
line-height: 20px; /* 142.857% */
}
}
}
.card-resource {
display: flex;
//padding: 0 20px;
min-height: 120px;
.left {
flex: 1;
min-width: 402px;
.pie {
width: 100%;
height: 100%;
}
}
.cardDetail {
flex: 1;
display: flex;
flex-wrap: wrap;
//grid-template-columns: repeat(3, 1fr);
gap: 12px;
align-content: center;
}
.cardDetail-item {
flex: 1;
height: 80px;
flex-shrink: 0;
border-radius: 6px;
background: #f5f7fa;
padding: 16px;
display: flex;
&:hover {
//color: var(--el-color-primary);
//cursor: pointer;
}
.avatar {
display: flex;
width: 48px;
height: 48px;
padding: 14px;
justify-content: center;
align-items: center;
flex-shrink: 0;
border-radius: 999px;
border: 2px solid #fff;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0) 0%,
#fff 100%
);
margin-right: 16px;
}
.count {
font-family: Roboto;
font-size: 20px;
font-style: normal;
font-weight: 700;
line-height: 100%; /* 20px */
margin-top: 10px;
}
}
}
.exceed {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
li {
height: 80px;
border-radius: 4px;
background: #f5f7fa;
padding: 16px;
.count {
font-size: 20px;
font-weight: bold;
margin-top: 5px;
span {
font-size: 12px;
}
}
}
}
}

@ -0,0 +1,438 @@
<template>
<div>
<back-header>
节点管理 > {{ detail.name }}
<!-- <template #extra>-->
<!-- <el-form-item-->
<!-- label="节点调度"-->
<!-- style="margin-bottom: 0; margin-right: 20px"-->
<!-- >-->
<!-- <el-radio-group-->
<!-- :disabled="detail.isExternal"-->
<!-- v-model="tempSchedulable"-->
<!-- size="small"-->
<!-- @change="onChangeSchedulable"-->
<!-- >-->
<!-- <el-radio-button label="启用" :value="true" />-->
<!-- <el-radio-button label="禁用" :value="false" />-->
<!-- </el-radio-group>-->
<!-- </el-form-item>-->
<!-- </template>-->
</back-header>
<block-box class="node-block">
<div class="node-detail">
<div class="node-detail-left">
<div class="title">详细信息</div>
<ul class="node-detail-info">
<li v-for="{ label, value, render } in detailColumns" :key="label">
<span class="label">{{ label }}</span>
<component v-if="render" :is="render(detail)" />
<span v-else class="value">{{ detail[value] }}</span>
</li>
</ul>
</div>
</div>
</block-box>
<block-box>
<ul class="card-gauges">
<li v-for="(item, index) in gaugeConfig" :key="index">
<template v-if="!detail.isExternal || index >= 2">
<Gauge v-bind="item" />
</template>
<template v-else-if="detail.isExternal && index < 2">
<el-empty description="暂无资源分配数据" :image-size="90" />
</template>
</li>
</ul>
</block-box>
<div class="line-box">
<block-box title="资源分配趋势(%">
<template #extra>
<time-picker v-model="times" type="datetimerange" size="small" />
</template>
<div style="height: 200px">
<echarts-plus
:options="
getRangeOptions({
core: gaugeConfig[0].data,
memory: gaugeConfig[1].data,
})
"
/>
</div>
</block-box>
<block-box title="资源使用趋势(%">
<template #extra>
<time-picker v-model="times" type="datetimerange" size="small" />
</template>
<div style="height: 200px">
<echarts-plus
:options="
getRangeOptions({
core: gaugeConfig[2].data,
memory: gaugeConfig[3].data,
})
"
/>
</div>
</block-box>
</div>
<block-box title="显卡列表">
<CardList :hideTitle="true" :filters="{ nodeUid: detail.uid }" />
</block-box>
<block-box title="任务列表">
<template v-if="detail.isExternal">
<el-alert title="由于节点未纳管,无法获取到任务数据" show-icon type="warning" :closable="false" />
<el-empty description="暂无任务数据" :image-size="100" />
</template>
<template v-else>
<TaskList :hideTitle="true" :filters="{ nodeUid: detail.uid }" />
</template>
</block-box>
</div>
</template>
<script setup lang="jsx">
import BackHeader from '@/components/BackHeader.vue';
import { useRoute, useRouter } from 'vue-router';
import BlockBox from '@/components/BlockBox.vue';
import {computed, onMounted, ref, watch} from 'vue';
import { Tools } from '@element-plus/icons-vue';
import CardList from '~/vgpu/views/card/admin/index.vue';
import TaskList from '~/vgpu/views/task/admin/index.vue';
import Gauge from '~/vgpu/components/gauge.vue';
import useInstantVector from '~/vgpu/hooks/useInstantVector';
import EchartsPlus from '@/components/Echarts-plus.vue';
import TimeSelect from '~/vgpu/components/timeSelect.vue';
import nodeApi from '~/vgpu/api/node';
import { getLineOptions } from '~/vgpu/views/monitor/overview/getOptions';
import { ElMessage, ElMessageBox } from 'element-plus';
import api from '~/vgpu/api/task';
import { getRangeOptions } from './getOptions';
import {getDaysInRange} from "@/utils";
const route = useRoute();
const router = useRouter();
const detail = ref({});
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000);
const times = ref([start, end]);
const isSchedulable = ref(true);
const tempSchedulable = ref(isSchedulable.value);
const cp = useInstantVector(
[
{
label: 'vGPU 超配',
count: '0',
query: `avg(hami_vgpu_count{node=~"$node"})`,
},
{
label: '算力超配',
count: '0',
query: `avg(hami_vcore_scaling{node=~"$node"})`,
},
{
label: '显存超配',
count: '1.5',
query: `avg(hami_vmemory_scaling{node=~"$node"})`,
},
],
(query) => query.replaceAll('$node', detail.value.name),
);
const gaugeConfig = useInstantVector(
[
{
title: '算力分配率',
percent: 0,
query: `avg(sum(hami_container_vcore_allocated{node=~"$node"}) by (instance))`,
totalQuery: `avg(sum(hami_core_size{node=~"$node"}) by (instance))`,
percentQuery: `avg(sum(hami_container_vcore_allocated{node=~"$node"}) by (instance)) / avg(sum(hami_core_size{node=~"$node"}) by (instance)) *100`,
total: 0,
used: 0,
unit: ' ',
},
{
title: '显存分配率',
percent: 0,
query: `avg(sum(hami_container_vmemory_allocated{node=~"$node"}) by (instance)) / 1024`,
totalQuery: `avg(sum(hami_memory_size{node=~"$node"}) by (instance)) / 1024`,
percentQuery: `(avg(sum(hami_container_vmemory_allocated{node=~"$node"}) by (instance)) / 1024) /(avg(sum(hami_memory_size{node=~"$node"}) by (instance)) / 1024) *100`,
total: 0,
used: 0,
unit: 'GiB',
},
{
title: '算力使用率',
percent: 0,
query: `avg(sum(hami_core_util{node=~"$node"}) by (instance))`,
percentQuery: `avg(sum(hami_core_util_avg{node=~"$node"}) by (instance))`,
totalQuery: `avg(sum(hami_core_size{node=~"$node"}) by (instance))`,
total: 100,
used: 0,
unit: ' ',
},
{
title: '显存使用率',
percent: 0,
query: `avg(sum(hami_memory_used{node=~"$node"}) by (instance)) / 1024`,
totalQuery: `avg(sum(hami_memory_size{node=~"$node"}) by (instance))/1024`,
percentQuery: `(avg(sum(hami_memory_used{node=~"$node"}) by (instance)) / 1024)/(avg(sum(hami_memory_size{node=~"$node"}) by (instance))/1024)*100`,
total: 0,
used: 0,
unit: 'GiB',
},
],
(query) => query.replaceAll(`$node`, detail.value.name),
times,
);
const detailColumns = [
{
label: '节点状态',
value: 'status',
render: ({ isSchedulable, isExternal }) => {
if (detail.value && detail.value.isSchedulable !== undefined) {
return (
<el-tag disable-transitions type={isExternal ? 'warning' : (isSchedulable ? 'success' : 'danger')}>
{isExternal ? '未纳管' : (isSchedulable ? '可调度' : '禁止调度')}
</el-tag>
);
} else {
return <el-tag disable-transitions size="small" type="info">加载中...</el-tag>;
}
},
},
{
label: '节点 IP 地址',
value: 'ip',
render: ({ ip }) => <text-plus text={ip} copy />,
},
{
label: '节点 UUID',
value: 'uid',
render: ({ uid }) => <text-plus text={uid} copy />,
},
{
label: '操作系统类型',
value: 'operatingSystem',
render: ({ operatingSystem }) => (
<span>
{operatingSystem==='' ? '--' : operatingSystem}
</span>
),
},
{
label: '系统架构',
value: 'architecture',
render: ({ architecture }) => (
<span>
{architecture==='' ? '--' : architecture}
</span>
),
},
{
label: 'kubelet 版本',
value: 'kubeletVersion',
render: ({ kubeletVersion }) => (
<span>
{kubeletVersion==='' ? '--' : kubeletVersion}
</span>
),
},
{
label: '操作系统版本',
value: 'osImage',
render: ({ osImage }) => (
<span>
{osImage==='' ? '--' : osImage}
</span>
),
},
{
label: '内核版本',
value: 'kernelVersion',
render: ({ kernelVersion }) => (
<span>
{kernelVersion==='' ? '--' : kernelVersion}
</span>
),
},
{
label: 'kube-proxy 版本',
value: 'kubeProxyVersion',
render: ({ kubeProxyVersion }) => (
<span>
{kubeProxyVersion==='' ? '--' : kubeProxyVersion}
</span>
),
},
{
label: '容器运行时',
value: 'containerRuntimeVersion',
render: ({ containerRuntimeVersion }) => (
<span>
{containerRuntimeVersion==='' ? '--' : containerRuntimeVersion}
</span>
),
},
{
label: '显卡数量',
value: 'cardCnt',
render: ({ cardCnt }) => (
<span>
{cardCnt==='' ? '--' : cardCnt}
</span>
),
},
{
label: '创建时间',
value: 'creationTimestamp',
render: ({ creationTimestamp }) => (
<span>
{creationTimestamp==='' ? '--' : creationTimestamp}
</span>
),
},
];
const onChangeSchedulable = (val) => {
ElMessageBox.confirm(
`确认对该节点进行${val ? '启用' : '禁用'}操作?`,
'操作确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
},
)
.then(async () => {
try {
await nodeApi
.stop({
nodeName: detail.value.name,
switch: val ? 'off' : 'on',
})
.then(() => {
setTimeout(() => {
refresh();
ElMessage.success(`${val ? '启用' : '禁用'}成功`);
}, 500);
});
} catch (error) {
ElMessage.error(error.message);
tempSchedulable.value = isSchedulable.value;
}
})
.catch(() => {
tempSchedulable.value = isSchedulable.value;
});
};
const refresh = async () => {
detail.value = await nodeApi.getNodeDetail({ uid: route.params.uid });
isSchedulable.value = detail.value.isSchedulable;
};
onMounted(async () => {
await refresh();
});
</script>
<style lang="scss">
.node-detail {
display: flex;
height: 100%;
//gap: 50px;
ul {
margin: 0;
padding: 0;
list-style: none;
}
.title {
color: #1d2b3a;
font-family: 'PingFang SC';
font-size: 14px;
font-style: normal;
font-weight: 500;
//line-height: 20px;
margin-bottom: 20px;
}
//.node-detail-left {
// min-width: 800px;
//}
.node-detail-info {
gap: 15px;
font-size: 12px;
display: grid;
grid-template-columns: repeat(3, 1fr);
.label {
display: inline-block;
width: 100px;
height: 20px;
color: #939ea9;
}
.set {
:hover {
cursor: pointer;
color: #324558;
}
}
.cp {
display: flex;
gap: 25px;
}
}
.gauges {
flex: 1;
display: flex;
li {
flex: 1;
}
}
}
.card-gauges {
margin: 0;
padding: 0;
list-style: none;
display: flex;
height: 200px;
li {
flex: 1;
}
}
.line-box {
display: grid;
grid-template-columns: repeat(2, 1fr);
column-gap: 20px;
}
.node-block {
display: flex;
flex-direction: column;
.home-block-content {
flex: 1;
}
}
</style>

@ -0,0 +1,120 @@
import { timeParse } from '@/utils';
export const getRangeOptions = ({ core = [], memory = [] }) => {
return {
legend: {
// data: [],
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
},
formatter: function (params) {
var res = params[0].name + '<br/>';
for (var i = 0; i < params.length; i++) {
res +=
params[i].marker +
params[i].seriesName +
' : ' +
(+params[i].value).toFixed(0) +
`%<br/>`;
}
return res;
},
},
grid: {
top: 37, // 上边距
bottom: 20, // 下边距
left: '7%', // 左边距
right: 10, // 右边距
},
xAxis: {
type: 'category',
data: core.map((item) => timeParse(+item.timestamp)),
axisLabel: {
formatter: function (value) {
return timeParse(value, 'HH:mm');
},
},
},
yAxis: {
type: 'value',
// max: 100,
axisLabel: {
formatter: function (value) {
return `${value} %`;
},
},
},
series: [
{
name: '算力',
data: core,
type: 'line',
areaStyle: {
normal: {
color: {
type: 'linear',
x: 0, // 渐变起始点 0%
y: 0, // 渐变起始点 0%
x2: 0, // 渐变结束点 100%
y2: 1, // 渐变结束点 100%
colorStops: [
{
offset: 0,
color: 'rgba(84, 112, 198, 0.16)', // 渐变起始颜色
},
{
offset: 1,
color: 'rgba(84, 112, 198, 0.00)', // 渐变结束颜色
},
],
global: false, // 缺省为 false
},
},
},
itemStyle: {
color: 'rgb(84, 112, 198)', // 设置线条颜色为橙色
},
lineStyle: {
color: 'rgb(84, 112, 198)', // 设置线条颜色为橙色
},
},
{
name: '显存',
data: memory,
type: 'line',
areaStyle: {
normal: {
color: {
type: 'linear',
x: 0, // 渐变起始点 0%
y: 0, // 渐变起始点 0%
x2: 0, // 渐变结束点 100%
y2: 1, // 渐变结束点 100%
colorStops: [
{
offset: 0,
color: 'rgba(34, 139, 34, 0.16)', // 渐变起始颜色
},
{
offset: 1,
color: 'rgba(34, 139, 34, 0.00)', // 渐变结束颜色
},
],
global: false, // 缺省为 false
},
},
},
itemStyle: {
color: 'rgb(145, 204, 117)', // 设置线条颜色为橙色
},
lineStyle: {
color: 'rgb(145, 204, 117)', // 设置线条颜色为橙色
},
},
],
};
};

@ -0,0 +1,197 @@
<template>
<list-header
description="节点管理用于管理和监控计算节点的状态。它可以启用或禁用节点查看节点上的物理GPU卡以及监控节点上运行的所有任务。"
/>
<preview-bar :handle-click=handleClick />
<table-plus
:api="nodeApi.getNodeList()"
:columns="columns"
:rowAction="rowAction"
:searchSchema="searchSchema"
:hasPagination="false"
style="height: auto"
hideTag
ref="table"
staticPage
>
</table-plus>
</template>
<script setup lang="jsx">
import nodeApi from '~/vgpu/api/node';
import searchSchema from '~/vgpu/views/node/admin/searchSchema';
import { useRouter } from 'vue-router';
import PreviewBar from '~/vgpu/components/previewBar.vue';
import { roundToDecimal } from '@/utils';
import {ElMessage, ElMessageBox} from 'element-plus';
import { ref } from 'vue';
const router = useRouter();
const table = ref();
const handleClick = async (params) => {
const name = params.data.name;
const {list} = await nodeApi.getNodes({filters:{}})
const node = list.find(node => node.name === name);
if (node) {
const uuid = node.uid;
router.push(`/admin/vgpu/node/admin/${uuid}?nodeName=${name}`);
} else {
ElMessage.error('节点未找到');
}
};
const columns = [
{
title: '节点名称',
dataIndex: 'name',
render: ({ uid, name }) => (
<text-plus text={name} to={`/admin/vgpu/node/admin/${uid}?nodeName=${name}`} />
),
},
{
title: '节点 IP',
dataIndex: 'ip',
},
{
title: '节点状态',
dataIndex: 'isSchedulable',
render: ({ isSchedulable, isExternal }) => (
<el-tag disable-transitions type={isExternal ? 'warning' : (isSchedulable ? 'success' : 'danger')}>
{isExternal ? '未纳管' : (isSchedulable ? '可调度' : '禁止调度')}
</el-tag>
)
// filters: [
// {
// text: '',
// value: 'true',
// },
// {
// text: '',
// value: 'false',
// },
// ],
},
{
title: '显卡型号',
dataIndex: 'type',
// filters: (data) => {
// const r = data.reduce((all, item) => {
// return uniq([...all, ...item.type]);
// }, []);
//
// return r.map((item) => ({ text: item, value: item }));
// },
},
{
title: '显卡数量',
dataIndex: 'cardCnt',
},
{
title: 'vGPU',
dataIndex: 'used',
render: ({ vgpuTotal, vgpuUsed, isExternal }) => (
<span>
{isExternal ? '--' : vgpuUsed}/{isExternal ? '--' : vgpuTotal}
</span>
),
},
{
title: '算力(已分配/总量)',
dataIndex: 'used',
render: ({ coreTotal, coreUsed, isExternal }) => (
<span>
{isExternal ? '--' : coreUsed}/{coreTotal}
</span>
),
},
{
title: '显存(已分配/总量)',
dataIndex: 'w',
render: ({ memoryTotal, memoryUsed, isExternal }) => (
<span>
{isExternal ? '--' : roundToDecimal(memoryUsed / 1024, 1)}/
{roundToDecimal(memoryTotal / 1024, 1)} GiB
</span>
),
},
];
const rowAction = [
{
title: '查看详情',
onClick: (row) => {
router.push(`/admin/vgpu/node/admin/${row.uid}?nodeName=${row.name}`);
},
},
// {
// title: '',
// hidden: (row) => !row.isSchedulable,
// onClick: async (row) => {
// ElMessageBox.confirm(``, '', {
// confirmButtonText: '',
// cancelButtonText: '',
// type: 'warning',
// })
// .then(async () => {
// try {
// await nodeApi.stop(
// {
// nodeName: row.name,
// switch: 'on'
// }
// ).then(
// () => {
// setTimeout(() => {
// ElMessage.success('');
// table.value.fetchData();
// }, 500);
// }
// )
// } catch (error) {
// ElMessage.error(error.message);
// }
// })
// .catch(() => {});
// },
// },
// {
// title: '',
// hidden: (row) => row.isSchedulable,
// disabled: (row) => row.isExternal,
// onClick: async (row) => {
// ElMessageBox.confirm(``, '', {
// confirmButtonText: '',
// cancelButtonText: '',
// type: 'warning',
// })
// .then(async () => {
// try {
// await nodeApi.stop(
// {
// nodeName: row.name,
// switch: 'off'
// }
// ).then(
// () => {
// setTimeout(() => {
// ElMessage.success('');
// table.value.fetchData();
// }, 500);
// }
// )
// } catch (error) {
// ElMessage.error(error.message);
// }
// })
// .catch(() => {});
// },
// },
];
</script>
<style></style>

@ -0,0 +1,40 @@
import api from '~/vgpu/api/card';
export default {
items: [
{
label: 'IP',
name: 'ip',
component: 'input',
},
{
label: '节点状态',
name: 'isSchedulable',
component: 'select',
props: {
mode: 'static',
options: [
{
label: '可调度',
value: 'true',
},
{
label: '禁止调度',
value: 'false',
},
],
},
},
{
label: '显卡型号',
name: 'type',
component: 'select',
props: {
mode: 'remote',
api: api.getCardType(),
labelKey: 'type',
valueKey: 'type',
},
},
],
};

@ -0,0 +1,3 @@
<template>
<router-view></router-view>
</template>

@ -0,0 +1,343 @@
<template>
<back-header>
任务管理 > {{ detail.name }}
</back-header>
<block-box>
<div class="task-detail">
<div class="left">
<!-- <el-descriptions column="2" title="详细信息">-->
<!-- <el-descriptions-item-->
<!-- v-for="{ label, value, render } in columns"-->
<!-- :label="label"-->
<!-- >-->
<!-- <component v-if="render" :is="render(detail)" />-->
<!-- <span v-else class="value">{{ detail[value] || '&#45;&#45;' }}</span>-->
<!-- </el-descriptions-item>-->
<!-- </el-descriptions>-->
<div class="title">详细信息</div>
<ul class="node-detail-info">
<li v-for="{ label, value, render } in columns" :key="label">
<span class="label">{{ label }}</span>
<component v-if="render" :is="render(detail)" />
<span v-else class="value">{{ detail[value] }}</span>
</li>
<li class="cp">
<span v-for="{ label, count } in cp" :key="label">
<span class="label">{{ label }}</span>
<span class="value">{{ count }} </span>
</span>
</li>
</ul>
</div>
<div class="right">
<div v-for="item in gaugeConfig" :key="item.title">
<Gauge v-bind="item" />
</div>
</div>
</div>
</block-box>
<block-box v-for="{ title, data } in lineConfig" :key="title" :title="title">
<template #extra>
<time-picker v-model="times" type="datetimerange" size="small" />
</template>
<div style="height: 200px">
<echarts-plus :options="getLineOptions({ data })" />
</div>
</block-box>
</template>
<script setup lang="jsx">
import BackHeader from '@/components/BackHeader.vue';
import {useRoute, useRouter} from 'vue-router';
import { onMounted, ref, watch, watchEffect } from 'vue';
import useInstantVector from '~/vgpu/hooks/useInstantVector';
import cardApi from '~/vgpu/api/card';
import { QuestionFilled } from '@element-plus/icons-vue';
import { roundToDecimal, timeParse, calculateDuration } from '@/utils';
import taskApi from '~/vgpu/api/task';
import BlockBox from '@/components/BlockBox.vue';
import Gauge from '~/vgpu/components/gauge.vue';
import { getLineOptions } from '~/vgpu/components/config';
import EchartsPlus from '@/components/Echarts-plus.vue';
import TimeSelect from '~/vgpu/components/timeSelect.vue';
const route = useRoute();
const router = useRouter();
const detail = ref({});
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000);
const times = ref([start, end]);
const columns = [
{
label: '任务状态',
value: 'status',
render: ({ status }) => {
const enums = {
closed: { text: '已完成', color: '#999' },
success: { text: '运行中', color: '#2563eb' },
unknown: { text: '未知', color: '#FACC15' },
failed: { text: '错误', color: '#EF4444' },
};
const { text, color } = enums[status] || {};
return (
<div
style={{
color,
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: '5px',
}}
>
<div
style={{
height: '7px',
width: '7px',
borderRadius: '50%',
backgroundColor: color,
display: 'inline-block',
}}
></div>{' '}
{text}
{(status === 'unknown' || status === 'failed') && (
<ElPopover placement="top" trigger="hover" popper-style={{ width: '180px' }}>
{{
reference: () => <el-icon color="#939EA9" size="14"><QuestionFilled /></el-icon>,
default: () => (
<span style={{ marginLeft: '5px', }}>
请跳转云平台查看详情
</span>
),
}}
</ElPopover>
)}
</div>
);
},
},
{
label: '所属显卡',
value: 'deviceIds',
render: ({ deviceIds }) => {
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
return <span>--</span>;
}
const text = deviceIds.join(', ');
const maxLength = 25;
const isLongText = text.length > maxLength;
const displayText = isLongText ? `${text.slice(0, maxLength)}...` : text;
return isLongText ? (
<el-tooltip content={text} placement="top">
<span>{displayText}</span>
</el-tooltip>
) : (
<span>{displayText}</span>
);
},
},
{
label: '所属节点',
value: 'nodeName',
render: ({ nodeName }) => <text-plus text={nodeName} copy />,
},
{
label: '显卡类型',
value: 'type',
},
{
label: '可分配算力',
value: 'allocatedCores',
},
{
label: '可分配显存',
value: 'allocatedMem',
render: ({ allocatedMem }) =>
allocatedMem ? (
<span>{roundToDecimal(allocatedMem / 1024, 1)} GiB</span>
) : (
<span>--</span>
),
},
{
label: '应用名称',
value: 'appName',
},
{
label: '任务创建时间',
value: 'createTime',
render: ({ createTime }) => <span>{timeParse(createTime)}</span>,
},
// {
// label: '',
// value: 'createTime',
// render: ({ createTime, status }) =>
// status === 'success' ? <span>{calculateDuration(createTime)}</span> : null,
// },
];
const gaugeConfig = useInstantVector(
[
{
title: '算力使用率',
percent: 0,
query: `avg(sum(hami_container_core_used{container_name="$container",pod_name=~"$pod",namespace_name="$namespace"}) by (instance))`,
totalQuery: `avg(sum(hami_container_vcore_allocated{container_name="$container",pod_name=~"$pod",namespace_name="$namespace"}) by (instance))`,
percentQuery: `avg(sum(hami_container_core_used{container_name="$container",pod_name=~"$pod",namespace_name="$namespace"}) by (instance)) / avg(sum(hami_container_vcore_allocated{container_name="$container",pod_name=~"$pod",namespace_name="$namespace"}) by (instance)) *100`,
total: 0,
used: 0,
unit: '%',
data: [],
},
{
title: '显存使用率',
percent: 0,
query: `avg(sum(hami_container_memory_used{container_name="$container",pod_name=~"$pod",namespace_name="$namespace"}) by (instance))/ 1024`,
totalQuery: `avg(sum(hami_container_vmemory_allocated{container_name="$container",pod_name=~"$pod",namespace_name="$namespace"}) by (instance))/1024`,
percentQuery: `(avg(sum(hami_container_memory_used{container_name="$container",pod_name=~"$pod",namespace_name="$namespace"})/ 1024)/(avg(sum(hami_container_vmemory_allocated{container_name="$container",pod_name=~"$pod",namespace_name="$namespace"}) by (instance))/1024) *100)`,
total: 0,
used: 0,
unit: 'GiB',
data: [],
},
],
(query) =>
query
.replaceAll(`$container`, detail.value.name)
.replaceAll(`$namespace`, detail.value.namespace)
.replaceAll(`$pod`, detail.value.appName),
);
const lineConfig = ref([
{
title: '算力使用趋势(%',
query: `avg(sum(hami_container_core_util{container_name=~"$container",pod_name=~"$pod",namespace_name="$namespace"}) by (instance))`,
data: [],
},
{
title: '显存使用趋势(%',
query: `avg(sum(hami_container_memory_util{container_name=~"$container",pod_name=~"$pod",namespace_name="$namespace"}) by (instance))`,
data: [],
},
]);
const fetchLineData = async () => {
lineConfig.value.map((item, index) =>
cardApi
.getRangeVector({
range: {
start: timeParse(times.value[0]),
end: timeParse(times.value[1]),
step: '1m',
},
query: item.query
.replaceAll(`$container`, detail.value.name)
.replaceAll(`$namespace`, detail.value.namespace)
.replaceAll(`$pod`, detail.value.appName),
})
.then((res) => {
lineConfig.value[index].data = res.data[0]?.values || [];
}),
);
};
watch(detail, async () => {
fetchLineData();
});
watch(times, () => {
fetchLineData();
});
onMounted(async () => {
const { name, podUid } = route.query;
detail.value = await taskApi.getTaskDetail({ name, podUid });
const cards = await cardApi.getCardListReq({filters: {}});
const foundCard = cards.list.find((item) => item.uuid === detail.value.deviceIds[0]);
if (foundCard) {
detail.value.type = foundCard.type;
}
// const start = new Date();
// start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
// const lineReqs = gaugeConfig.value.map((item) =>
// cardApi.getRangeVector({
// range: {
// start: timeParse(start),
// end: timeParse(new Date()),
// step: '1m',
// },
// query: item.query
// .replaceAll(`$container`, detail.value.name)
// .replaceAll(`$namespace`, detail.value.namespace)
// .replaceAll(`$pod`, detail.value.appName),
// }),
// );
//
// const res = await Promise.all(lineReqs);
//
// gaugeConfig.value = gaugeConfig.value.map((item, index) => ({
// ...item,
// data: res[index].data[0]?.values || [],
// }));
});
</script>
<style lang="scss">
.task-detail {
display: grid;
grid-template-columns: repeat(2, 1fr);
.right {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
.title {
color: #1d2b3a;
font-family: 'PingFang SC';
font-size: 14px;
font-style: normal;
font-weight: 500;
//line-height: 20px;
margin-bottom: 20px;
}
.node-detail-info {
gap: 15px;
font-size: 12px;
display: grid;
grid-template-columns: 1fr 1fr;
.label {
display: inline-block;
width: 80px;
height: 20px;
color: #939ea9;
}
.cp {
display: flex;
gap: 25px;
}
}
}
</style>

@ -0,0 +1,211 @@
import { timeParse } from '@/utils';
import { cloneDeep } from 'lodash';
export const getLineOptions = ({ title, data = [], unit }) => {
return {
title: { text: title, left: 'center' },
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
},
},
xAxis: {
type: 'category',
data: data.map((item) => timeParse(+item.timestamp)),
axisLabel: {
formatter: function (value) {
return timeParse(value, 'HH:mm');
},
interval: function (index, value) {
var date = new Date(value);
return date.getMinutes() === 0;
},
},
},
yAxis: {
type: 'value',
axisLabel: {
formatter: function (value) {
return `${value} ${unit}`;
},
},
},
series: [
{
data: data.map((item) => {
if (title === 'GPU 内存使用') {
return (item.value / 1024).toFixed(1);
}
return item.value;
}),
type: 'line',
areaStyle: {
normal: {
color: {
type: 'linear',
x: 0, // 渐变起始点 0%
y: 0, // 渐变起始点 0%
x2: 0, // 渐变结束点 100%
y2: 1, // 渐变结束点 100%
colorStops: [
{
offset: 0,
color: 'rgba(84, 112, 198, 0.16)', // 渐变起始颜色
},
{
offset: 1,
color: 'rgba(84, 112, 198, 0.00)', // 渐变结束颜色
},
],
global: false, // 缺省为 false
},
},
},
},
],
};
};
export const getGaugeOptions = ({ percent = 0, title, unit }) => {
let thisColor = '';
if (percent < 30) {
thisColor = '#16A34A';
} else if (percent >= 30 && percent <= 80) {
thisColor = '#2563EB';
} else {
thisColor = '#DC2626';
}
return {
series: [
{
type: 'gauge',
itemStyle: {
color: thisColor,
// shadowColor: thisColor,
// shadowBlur: 5,
// shadowOffsetX: 2,
// shadowOffsetY: 2,
},
progress: {
show: true,
width: 8,
},
axisLine: {
lineStyle: {
width: 8,
backgroundColor: '#F5F7FA',
},
},
axisTick: {
show: false,
},
axisLabel: {
show: false, // 隐藏刻度标签
},
// splitNumber: 0,
splitLine: {
show: false,
},
anchor: {
show: false,
showAbove: false,
size: 25,
itemStyle: {
borderWidth: 10,
},
},
pointer: {
show: false,
},
title: {
show: false,
},
detail: {
valueAnimation: true,
width: '60%',
lineHeight: 40,
borderRadius: 8,
offsetCenter: [0, '0%'],
// fontSize: 16,
fontWeight: 'bolder',
formatter: `{a|{value}}{b|${unit}}`,
rich: {
a: {
color: '#1D2B3A',
lineHeight: 10,
fontSize: 20,
},
b: {
color: '#1D2B3A',
},
},
},
data: [
{
value: percent.toFixed(1),
},
],
},
],
graphic: [
{
type: 'text',
left: 'center',
bottom: 8,
style: {
text: title,
fill: '#333', // 设置标题文字颜色
fontSize: 14, // 设置标题文字大小
borderRadius: 999,
backgroundColor: '#F5F7FA',
padding: [6, 16],
},
},
],
};
};
export const getTopOptions = (dataSource) => {
const data = cloneDeep(dataSource).reverse();
return {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
legend: {
show: false,
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '0',
containLabel: true,
},
xAxis: {
type: 'value',
boundaryGap: [0, 0.01],
},
yAxis: {
type: 'category',
data: data.map((item) => {
return item.name;
}),
},
series: [
{
name: '',
type: 'bar',
data: data,
},
],
};
};

@ -0,0 +1,82 @@
import api from '~/vgpu/api/card';
export default {
labelWidth: '150px',
labelAlign: 'top',
items: [
{
label: '任务名称',
name: 'name',
component: 'input',
required: true,
},
{
label: '优先级',
name: 'priority',
component: 'radio',
props: {
optionType: 'button',
mode: 'static',
options: [
{
label: '低优先级',
value: '0',
},
{
label: '高优先级',
value: '2',
},
],
},
initialValue: '0',
},
{
label: '实例数',
name: 'replicas',
component: 'inputNumber',
required: true,
initialValue: 1,
},
{
label: '显卡类型',
name: 'gpuType',
component: 'select',
props: {
mode: 'remote',
api: api.getCardList(),
labelKey: 'type',
valueKey: 'type',
},
},
{
label: 'GPU申请数',
name: 'gpuCount',
component: 'inputNumber',
required: true,
initialValue: 1,
props: {
unit: '个',
},
},
{
label: '显存申请数',
name: 'gpuMem',
component: 'inputNumber',
props: {
unit: 'M',
},
},
{
label: '算力',
name: 'gpuCore',
component: 'inputNumber',
props: {
unit: '%',
},
},
],
};

@ -0,0 +1,178 @@
<template>
<list-header
v-if="!hideTitle"
description="任务管理用于监控物理显卡的状态。它用于监控物理显卡的分配使用情况,以及查看物理显卡上运行的所有任务。"
/>
<Top v-if="!hideTitle" />
<table-plus
:api="taskApi.getTaskList({ filters })"
:columns="columns"
:rowAction="rowAction"
:searchSchema="searchSchema"
ref="table"
:style="style"
hideTag
staticPage
>
</table-plus>
<form-plus-drawer
v-model="state.visible"
v-model:form="state.formValues"
:schema="state.schema"
:title="state.title"
@ok="state.ok"
/>
</template>
<script setup lang="jsx">
import taskApi from '~/vgpu/api/task';
import {calculateDuration, roundToDecimal, timeParse} from '@/utils';
import { QuestionFilled } from '@element-plus/icons-vue';
import api from '~/vgpu/api/task';
import {ElMessage, ElMessageBox, ElPopover} from 'element-plus';
import { reactive, ref, defineProps } from 'vue';
import editSchema from './editSchema';
import { mapValues, isNumber, pick } from 'lodash';
import { useRouter } from 'vue-router';
import searchSchema from './searchSchema';
import Top from './top.vue';
const props = defineProps(['hideTitle', 'filters', 'style']);
const table = ref();
const router = useRouter();
const state = reactive({
visible: false,
schema: {},
formValues: {},
title: '',
ok: () => {},
});
const columns = [
{
title: '任务名称',
dataIndex: 'name',
render: ({ name,podUid }) => (
<text-plus text={name} to={`/admin/vgpu/task/admin/detail?name=${name}&podUid=${podUid}`} />
),
},
{
title: '任务状态',
dataIndex: 'status',
render: ({ status, deviceIds }) => {
const enums = {
closed: { text: '已完成', color: '#999' },
success: { text: '运行中', color: '#2563eb' },
unknown: { text: '未知', color: '#FACC15' },
failed: { text: '错误', color: '#EF4444' },
};
const { text, color } = enums[status];
return (
<div
style={{
color,
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '5px',
}}
>
<div
style={{
height: '7px',
width: '7px',
borderRadius: '50%',
backgroundColor: color,
display: 'inline-block',
}}
></div>{' '}
{text}
{(status === 'unknown' || status === 'failed') && (
<ElPopover trigger="hover" popper-style={{ width: '180px' }}>
{{
reference: () => <el-icon color="#939EA9" size="14"><QuestionFilled /></el-icon>,
default: () => (
<span style={{ marginLeft: '5px', }}>
请跳转云平台查看详情
</span>
),
}}
</ElPopover>
)}
</div>
);
},
},
{
title: '所属节点',
dataIndex: 'nodeName',
},
{
title: '分配 vGPU',
dataIndex: 'deviceIds',
render: ({ deviceIds }) => {
return (
<ElPopover trigger="hover" popper-style={{ width: '350px' }}>
{{
reference: () => <el-tag disable-transitions>{deviceIds.length} </el-tag>,
default: () => (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '10px',
width: '100%',
justifyContent: 'center',
}}
>
{deviceIds.map((item) => (
<ElTag type="info">{item}</ElTag>
))}
</div>
),
}}
</ElPopover>
);
},
},
{
title: '分配算力',
dataIndex: 'allocatedCores',
render: ({ allocatedCores }) => `${allocatedCores} `,
},
{
title: '分配显存',
dataIndex: 'allocatedMem',
render: ({ allocatedMem }) =>
`${roundToDecimal(allocatedMem / 1024, 1)} GiB`,
},
{
title: '任务创建时间',
dataIndex: 'createTime',
render: ({ createTime }) => timeParse(createTime),
},
];
const rowAction = [
{
title: '查看详情',
onClick: (row) => {
router.push({
path: '/admin/vgpu/task/admin/detail',
query: pick(row, ['name', 'podUid']),
});
},
},
];
</script>
<style lang="scss"></style>

@ -0,0 +1,60 @@
import nodeApi from '~/vgpu/api/node';
import cardApi from '~/vgpu/api/card';
export default {
items: [
{
label: '任务名称',
name: 'name',
component: 'input',
},
{
label: '节点名称',
name: 'nodeName',
component: 'select',
props: {
mode: 'remote',
api: nodeApi.getNodeList({filters:{}}),
labelKey: 'name',
valueKey: 'name',
},
},
{
label: '任务状态',
name: 'status',
component: 'select',
props: {
mode: 'static',
options: [
{
label: '已完成',
value: 'closed',
},
{
label: '运行中',
value: 'success',
},
{
label: '错误',
value: 'failed',
},
{
label: '未知',
value: 'unknown',
},
],
},
},
{
label: '显卡ID',
name: 'deviceId',
component: 'select',
props: {
mode: 'remote',
api: cardApi.getCardList(),
labelKey: 'uuid',
valueKey: 'uuid',
},
},
],
};

@ -0,0 +1,105 @@
<script setup>
defineProps(['config']);
</script>
<template>
<div class="tabs-container">
<div class="tabs">
<template v-for="({ tab }, index) in config">
<input type="radio" :id="`radio-${index + 1}`" name="tabs" checked="" />
<label class="tab" :for="`radio-${index + 1}`">{{ tab }}</label>
</template>
<!-- <input type="radio" id="radio-2" name="tabs" />-->
<!-- <label class="tab" for="radio-2">UI</label>-->
<!-- <input type="radio" id="radio-3" name="tabs" />-->
<!-- <label class="tab" for="radio-3">World</label>-->
<span class="glider"></span>
</div>
</div>
</template>
<style scoped lang="scss">
.tabs {
display: flex;
position: relative;
background-color: #fff;
box-shadow: 0 0 1px 0 rgba(24, 94, 224, 0.15),
0 6px 12px 0 rgba(24, 94, 224, 0.15);
//padding: 0.75rem;
padding: 2px;
border-radius: 8px;
}
.tabs * {
z-index: 2;
}
.tabs-container input[type='radio'] {
display: none;
}
.tab {
display: flex;
align-items: center;
justify-content: center;
height: 30px;
width: 50px;
font-size: 0.8rem;
color: black;
font-weight: 500;
border-radius: 8px;
cursor: pointer;
transition: color 0.15s ease-in;
}
.notification {
display: flex;
align-items: center;
justify-content: center;
width: 0.8rem;
height: 0.8rem;
position: absolute;
top: 10px;
left: 30%;
font-size: 10px;
margin-left: 0.75rem;
border-radius: 50%;
margin: 0px;
background-color: #e6eef9;
transition: 0.15s ease-in;
}
.tabs-container input[type='radio']:checked + label {
color: #185ee0;
}
.tabs-container input[type='radio']:checked + label > .notification {
background-color: #185ee0;
color: #fff;
margin: 0px;
}
.tabs-container input[id='radio-1']:checked ~ .glider {
transform: translateX(0);
}
.tabs-container input[id='radio-2']:checked ~ .glider {
transform: translateX(100%);
}
.tabs-container input[id='radio-3']:checked ~ .glider {
transform: translateX(200%);
}
.glider {
position: absolute;
display: flex;
height: 30px;
width: 50px;
background-color: #e6eef9;
z-index: 1;
border-radius: 8px;
transition: 0.25s ease-out;
}
</style>

@ -0,0 +1,110 @@
<template>
<div class="task-top-box">
<TabTop class="item" v-for="item in topConfig" :key="item.key" v-bind="item" :onClick="handleChartClick" />
</div>
</template>
<script setup>
import TabTop from '~/vgpu/components/TabTop.vue';
import { useRouter } from 'vue-router';
import nodeApi from '~/vgpu/api/node';
import { ElMessage } from 'element-plus';
const router = useRouter();
const handleChartClick = async (params) => {
const name = params.data.name;
const activeTabKey = params.tabActive;
if (activeTabKey === 'node') {
const { list } = await nodeApi.getNodes({ filters: {} });
const node = list.find(node => node.name === name);
if (node) {
const uuid = node.uid;
router.push(`/admin/vgpu/node/admin/${uuid}?nodeName=${name}`);
} else {
ElMessage.error('节点未找到');
}
} else if (activeTabKey === 'deviceuuid') {
router.push({
path: `/admin/vgpu/card/admin/${name}`,
});
} else {
const [containerName, podUid] = name.split(':');
router.push({
path: '/admin/vgpu/task/admin/detail',
query: {
name: containerName,
podUid: podUid,
},
});
}
};
const topConfig = [
{
title: '任务数量分布 Top5',
key: 'total',
config: [
{
tab: '节点',
key: 'node',
nameKey: 'node',
data: [],
unit: ' ',
query:
'topk(5, count by (node) (sum by (container_pod_uuid, node) (hami_container_vcore_allocated)))',
},
{
tab: '显卡',
key: 'deviceuuid',
data: [],
nameKey: 'deviceuuid',
unit: ' ',
query:
'topk(5, count by (deviceuuid) (sum by (container_pod_uuid, deviceuuid) (hami_container_vcore_allocated)))',
},
],
},
{
title: '任务资源申请 Top5',
key: 'apply',
config: [
{
tab: '算力',
key: 'core',
data: [],
nameKey: 'container_pod_uuid',
unit: ' ',
query: 'topk(5, avg by (container_pod_uuid) (hami_container_vcore_allocated))',
},
{
tab: '显存',
key: 'memory',
data: [],
unit: 'GiB',
nameKey: 'container_pod_uuid',
query:
'topk(5, avg by (container_pod_uuid) (hami_container_vmemory_allocated))/1024',
},
{
tab: 'vGPU',
key: 'vgpu',
data: [],
nameKey: 'container_pod_uuid',
unit: '个',
query: 'topk(5, avg by (container_pod_uuid) (hami_container_vgpu_allocated))',
},
],
},
];
</script>
<style scoped lang="scss">
.task-top-box {
display: flex;
gap: 25px;
.item {
flex: 1;
}
}
</style>

@ -0,0 +1,3 @@
<template>
<router-view></router-view>
</template>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 169 KiB

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.svg">
<title>HAMi</title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="_图层_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 428 89.3"><defs><style>.cls-1{fill:#00a971;}.cls-2{fill:#fff;}</style></defs><g id="_图层_1-2"><g><g><g><polygon class="cls-1" points="68.09 72.39 48.35 61 48.35 83.79 68.09 72.39"/><polygon class="cls-1" points="68.09 16.9 48.35 5.51 48.35 28.3 68.09 16.9"/><polygon class="cls-1" points="20.04 44.65 38.62 33.92 38.62 0 0 22.3 0 67 38.62 89.3 38.62 55.38 20.04 44.65"/></g><polygon class="cls-1" points="38.75 44.65 77.31 66.91 77.31 22.39 38.75 44.65"/></g><g><path class="cls-2" d="M325.52,21.04h-3.94c-.86,0-1.58,.72-1.58,1.58v18.34h-12.68V22.61c0-.86-.64-1.58-1.58-1.58h-3.94c-.86,0-1.58,.72-1.58,1.58v43.99c0,.86,.72,1.58,1.58,1.58h3.94c.93,0,1.58-.72,1.58-1.58v-18.56h12.68v18.56c0,.86,.72,1.58,1.58,1.58h3.94c.93,0,1.58-.72,1.58-1.58V22.61c0-.86-.64-1.58-1.58-1.58Z"/><path class="cls-2" d="M230.51,62.66c0-.86-.72-1.58-1.58-1.58h-17.79s0-12.93,0-12.93h17.79c.86,0,1.58-.64,1.58-1.58v-3.94c0-.86-.72-1.58-1.58-1.58h-17.79s0-12.93,0-12.93h17.79c.86,0,1.58-.64,1.58-1.58v-3.94c0-.86-.72-1.58-1.58-1.58h-19.37s0,0,0,0h-3.94c-.86,0-1.58,.72-1.58,1.58v43.99c0,.86,.72,1.58,1.58,1.58h1.71s0,0,0,0h21.6c.86,0,1.58-.64,1.58-1.58v-3.94Z"/><path class="cls-2" d="M360.85,62.66c0-.86-.72-1.58-1.58-1.58h-17.79s0-12.93,0-12.93h17.79c.86,0,1.58-.64,1.58-1.58v-3.94c0-.86-.72-1.58-1.58-1.58h-17.79s0-12.93,0-12.93h17.79c.86,0,1.58-.64,1.58-1.58v-3.94c0-.86-.72-1.58-1.58-1.58h-19.37s0,0,0,0h-3.94c-.86,0-1.58,.72-1.58,1.58v43.99c0,.86,.72,1.58,1.58,1.58h1.71s0,0,0,0h21.6c.86,0,1.58-.64,1.58-1.58v-3.94Z"/><path class="cls-2" d="M428,62.66c0-.86-.72-1.58-1.58-1.58h-17.79s0-12.93,0-12.93h17.79c.86,0,1.58-.64,1.58-1.58v-3.94c0-.86-.72-1.58-1.58-1.58h-17.79s0-12.93,0-12.93h17.79c.86,0,1.58-.64,1.58-1.58v-3.94c0-.86-.72-1.58-1.58-1.58h-19.37s0,0,0,0h-3.94c-.86,0-1.58,.72-1.58,1.58v43.99c0,.86,.72,1.58,1.58,1.58h1.71s0,0,0,0h21.6c.86,0,1.58-.64,1.58-1.58v-3.94Z"/><path class="cls-2" d="M243.38,32.9v2.69c0,2.05,1.98,3.26,4.25,4.18l9.42,3.96c3.54,1.56,6.02,4.32,6.02,8.57v5.03c0,4.18-8.78,11.19-13.45,11.19-3.19,0-8-2.27-11.75-4.32-.85-.43-1.42-1.42-.92-2.55l1.2-2.41c.5-.99,1.56-1.2,2.48-.71,3.04,1.56,7.08,3.47,8.99,3.47,2.12,0,6.59-3.61,6.59-5.67v-3.12c0-2.41-1.84-3.75-4.39-4.74l-8.78-3.68c-3.4-1.42-6.51-4.32-6.51-8.28v-4.6c0-4.6,9.28-11.26,13.74-11.26,2.97,0,7.79,2.19,10.98,3.89,.99,.5,1.27,1.63,.85,2.48l-1.13,2.41c-.35,.85-1.49,1.13-2.48,.71-2.41-1.13-6.44-2.97-8.21-2.97-2.05,0-6.87,3.4-6.87,5.73Z"/><path class="cls-2" d="M381.38,49.12h-6.66v17.48c0,.93-.65,1.58-1.65,1.58h-3.8c-.86,0-1.65-.64-1.65-1.58V24.33c0-1.65,1.93-3.3,3.51-3.3h11.46c5.23,0,12.17,8.16,12.17,13.53,0,3.58-3.14,8.97-6.51,11.05,2.72,1.43,6.45,5.23,6.45,8.74v12.25c0,.86-.72,1.58-1.58,1.58h-3.94c-.86,0-1.58-.72-1.58-1.58v-11.18c0-2.51-3.51-6.31-6.23-6.31Zm.29-21.92h-6.95v15.76h7.02c2.94,0,6.38-5.23,6.38-8.02s-3.58-7.74-6.45-7.74Z"/><path class="cls-2" d="M282.36,49.12h-6.66v17.48c0,.93-.65,1.58-1.65,1.58h-3.8c-.86,0-1.65-.64-1.65-1.58V24.33c0-1.65,1.93-3.3,3.51-3.3h11.46c5.23,0,11.85,8.41,11.85,13.78,0,3.58-2.65,8.4-5.72,11.19-1.51,1.37-3.9,3.11-7.35,3.11Zm.29-21.92h-6.95v15.76h7.02c2.94,0,5.97-5.23,5.97-8.02s-3.17-7.74-6.04-7.74Z"/><path class="cls-2" d="M150.35,68.6c-4.72,0-13.54-5.97-13.54-11.12V22.92c0-.86,.78-1.57,1.65-1.57h3.78c.92,0,1.64,.71,1.64,1.57V56.55c0,2.5,4.26,5.54,6.48,5.54s6.51-3.42,6.51-5.92V22.53c0-.86,.64-1.57,1.5-1.57h3.92c.86,0,1.65,.71,1.65,1.57V57.1c0,5-8.87,11.5-13.58,11.5Z"/><path class="cls-2" d="M191.41,43.82c3.26-2.15,6.55-6.33,6.55-9.8,0-5.37-7.31-12.99-12.54-12.99h-11.22c-1.58,0-3.51,1.65-3.51,3.3v42.27c0,.93,.79,1.58,1.65,1.58h13.33c5.23,0,12.3-8.8,12.3-14.18,0-3.47-3.29-8.01-6.55-10.19Zm-13.62-16.62h6.71c2.87,0,6.45,4.1,6.45,6.83s-3.44,7.36-6.38,7.36h-6.78v-14.18Zm6.71,34.82h-6.71v-15.76h6.78c2.94,0,6.38,4.95,6.38,7.75s-3.58,8.01-6.45,8.01Z"/><path class="cls-2" d="M114.9,44.87l15.6-18.07c.56-.65,.54-1.61-.16-2.22l-2.98-2.58c-.65-.56-1.66-.49-2.22,.16l-13.64,15.8v-15.35c0-.86-.64-1.58-1.58-1.58h-3.94c-.86,0-1.58,.72-1.58,1.58v43.99c0,.86,.72,1.58,1.58,1.58h3.94c.93,0,1.58-.72,1.58-1.58v-14.83l13.64,15.8c.56,.65,1.57,.72,2.22,.16l2.98-2.58c.71-.61,.72-1.57,.16-2.22l-15.6-18.07Z"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 384 KiB

@ -0,0 +1,11 @@
<svg width="220" height="51" viewBox="0 0 220 51" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="logo">
<path id="Fill 14" fill-rule="evenodd" clip-rule="evenodd" d="M53.0225 50.469H220V0.000488281H53.0225V50.469ZM55.2111 48.2751H217.811V2.19451H55.2111V48.2751Z" fill="#22B4A6"/>
<path id="Fill 15" fill-rule="evenodd" clip-rule="evenodd" d="M45.6185 16.1784H29.6326V24.3154H43.3712V29.4502H29.6326V39.4231H24.055V11.0471H45.6185V16.1784ZM13.9741 14.3434C11.9334 14.3434 10.5598 13.5518 10.5598 11.421C10.5598 9.29374 11.9334 8.5434 13.9741 8.5434C15.971 8.5434 17.3026 9.29374 17.3026 11.421C17.3026 13.5518 15.971 14.3434 13.9741 14.3434ZM11.0186 39.4232H16.8456V17.5984H11.0186V39.4232ZM0 50.4685H53.0214V0H0V50.4685Z" fill="#22B4A6"/>
<path id="Fill 16" fill-rule="evenodd" clip-rule="evenodd" d="M75.2859 34.415C78.9506 34.415 81.6549 33.1232 83.5704 31.6181L86.3193 35.7498C83.3638 38.4651 79.5739 39.8412 75.2456 39.8412C66.7948 39.8412 60.4249 33.5383 60.4249 25.2337C60.4249 16.929 66.7948 10.6288 75.2456 10.6288C79.5739 10.6288 83.3638 12.0057 86.3193 14.721L83.5704 18.8492C81.6549 17.3476 78.9506 16.0523 75.2859 16.0523C70.0419 16.0523 66.4604 20.1024 66.4604 25.2337C66.4604 30.3684 70.0419 34.415 75.2859 34.415Z" fill="#22B4A6"/>
<path id="Fill 17" fill-rule="evenodd" clip-rule="evenodd" d="M91.7866 39.4235V11.0474H97.6137V34.3334H112.311V39.4235H91.7866Z" fill="#22B4A6"/>
<path id="Fill 18" fill-rule="evenodd" clip-rule="evenodd" d="M129.016 34.3739C134.222 34.3739 137.842 30.4081 137.842 25.2777C137.842 20.0613 134.222 16.1788 129.016 16.1788C123.897 16.1788 120.192 20.0613 120.192 25.2777C120.192 30.4081 123.897 34.3739 129.016 34.3739ZM128.974 10.6289C137.344 10.6289 143.588 16.8879 143.588 25.2777C143.588 33.5823 137.344 39.8404 128.974 39.8404C120.69 39.8404 114.446 33.5823 114.446 25.2777C114.446 16.8879 120.69 10.6289 128.974 10.6289Z" fill="#22B4A6"/>
<path id="Fill 19" fill-rule="evenodd" clip-rule="evenodd" d="M154.97 27.0721C154.97 31.5776 157.257 34.4587 161.962 34.4587C166.749 34.4587 169.081 31.4109 169.081 27.0273V11.0472H174.908V27.4459C174.908 34.1226 170.414 39.8822 161.962 39.8822C153.594 39.8822 149.14 34.6658 149.14 27.4459V11.0472H154.97V27.0721Z" fill="#22B4A6"/>
<path id="Fill 20" fill-rule="evenodd" clip-rule="evenodd" d="M187.999 16.1372V34.3332H194.12C199.324 34.3332 202.944 31.1186 202.944 25.2773C202.944 19.3518 199.324 16.1372 194.12 16.1372H187.999ZM182.172 11.0472H194.368C202.737 11.0876 208.981 16.0521 208.981 25.2774C208.981 34.4149 202.737 39.4233 194.368 39.4233H182.172V11.0472Z" fill="#22B4A6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_1" data-name="图层 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 700 130">
<defs>
<style>
.cls-1 {
fill: #fff;
}
.cls-2 {
fill: #50b4ff;
}
</style>
</defs>
<g>
<path class="cls-1" d="M237.33,85.91h-12.6v29.16h-16.56V31.07h33.6c15.48,0,27.96,12.48,27.96,27.84,0,10.56-6.6,20.04-16.2,24.48l18.6,31.68h-17.88l-16.92-29.16Zm-12.6-14.52h17.04c6.24,0,11.4-5.52,11.4-12.48s-5.16-12.36-11.4-12.36h-17.04v24.84Z"/>
<path class="cls-1" d="M276.33,38.39c0-5.16,4.32-9.6,9.48-9.6s9.6,4.44,9.6,9.6-4.32,9.48-9.6,9.48-9.48-4.32-9.48-9.48Zm1.8,16.68h15.48v60h-15.48V55.07Z"/>
<path class="cls-1" d="M349.53,97.55c0,12.96-11.28,19.2-24.12,19.2-12,0-20.88-5.04-25.2-14.28l13.44-7.56c1.68,4.92,5.76,7.8,11.76,7.8,4.92,0,8.28-1.68,8.28-5.16,0-8.76-30.96-3.96-30.96-25.08,0-12.24,10.44-19.08,22.8-19.08,9.72,0,18.12,4.44,22.8,12.72l-13.2,7.2c-1.8-3.84-5.16-6.12-9.6-6.12-3.84,0-6.96,1.68-6.96,4.92,0,8.88,30.96,3.36,30.96,25.44Z"/>
<path class="cls-1" d="M385.65,102.71c6,0,10.8-2.52,13.44-6l12.48,7.2c-5.64,8.16-14.64,12.84-26.16,12.84-20.16,0-32.88-13.8-32.88-31.68s12.84-31.68,31.68-31.68c17.76,0,30.36,14.04,30.36,31.68,0,2.28-.24,4.32-.6,6.36h-45.24c2.16,7.92,8.76,11.28,16.92,11.28Zm13.44-23.28c-1.92-8.64-8.4-12.12-14.88-12.12-8.28,0-13.92,4.44-15.72,12.12h30.6Z"/>
<path class="cls-1" d="M442.65,31.07h18l20.52,64.56,20.4-64.56h18.12l-28.32,84h-20.52l-28.2-84Z"/>
<path class="cls-1" d="M568.88,99.95h-33.48l-5.04,15.12h-17.88l29.4-84h20.52l29.52,84h-18l-5.04-15.12Zm-5.16-15.48l-11.52-34.32-11.52,34.32h23.04Z"/>
<path class="cls-1" d="M590.85,95.99l14.16-8.28c3,7.8,8.88,12.84,19.32,12.84s13.44-4.2,13.44-8.88c0-6.24-5.64-8.64-18.12-12.24-12.84-3.72-25.32-9.12-25.32-25.08s13.2-24.96,27.6-24.96,24.36,7.08,30,18.84l-13.92,8.04c-3-6.36-7.56-10.8-16.08-10.8-6.96,0-11.04,3.6-11.04,8.4,0,5.16,3.24,7.92,15.96,11.76,13.32,4.2,27.48,8.64,27.48,25.8,0,15.72-12.6,25.32-30.48,25.32s-28.44-8.28-33-20.76Z"/>
<path class="cls-1" d="M714.69,46.91h-22.68V115.07h-16.56V46.91h-22.56v-15.84h61.8v15.84Z"/>
</g>
<g>
<path class="cls-2" d="M131.59,134H58.56c-2.42,0-4.65-1.29-5.86-3.39L19.65,73.39c-1.21-2.09-1.21-4.68,0-6.77L52.69,9.39c1.21-2.09,3.44-3.39,5.86-3.39H124.64c2.42,0,4.65,1.29,5.86,3.39l33.04,57.23c1.21,2.09,1.21,4.68,0,6.77l-18.48,32.01c-1.28,2.22-4.48,2.26-5.82,.07l-10.01-16.36c-1.29-2.12-1.33-4.77-.09-6.92l6.06-10.5c.6-1.05,.6-2.34,0-3.39l-20.34-35.23c-.6-1.05-1.72-1.69-2.93-1.69h-40.67c-1.21,0-2.33,.65-2.93,1.69l-20.34,35.23c-.6,1.05-.6,2.34,0,3.39l20.34,35.23c.6,1.05,1.72,1.69,2.93,1.69h27.74l-18.95-32.17,21.87-12.89,35.49,60.24c2.66,4.51-.59,10.21-5.83,10.21Z"/>
<path class="cls-2" d="M131.6,134H61.99v-25.39h37.03l-15.27-25.92H15.86c-2.44,0-4.7-1.32-5.9-3.45L.44,62.35c-1.27-2.26,.36-5.05,2.95-5.05H94.39c2.4,0,4.62,1.27,5.83,3.33l37.21,63.15c2.66,4.51-.59,10.21-5.83,10.21Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@ -0,0 +1,9 @@
<template>
<router-view></router-view>
</template>
<script setup>
defineOptions({
name: 'App',
});
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 169 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 171 KiB

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save