@ -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']
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
  
|
||||||
|
|
||||||
|
## 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
|
||||||
|
```
|
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,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,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,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,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,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,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>
|
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>
|
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 384 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 2.9 KiB |
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<router-view></router-view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineOptions({
|
||||||
|
name: 'App',
|
||||||
|
});
|
||||||
|
</script>
|
After Width: | Height: | Size: 160 KiB |
After Width: | Height: | Size: 96 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 169 KiB |
After Width: | Height: | Size: 171 KiB |