Compare commits

...

15 Commits
main ... v1.3.6

Author SHA1 Message Date
github-actions[bot] 373557014e
Fix: fail to confige the helm value (#522)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 2ec4f22bdd)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-05-26 19:44:15 +08:00
barnettZQG 9676d17d62
Fix: optimize some messages (#498)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
2022-05-11 18:53:39 +08:00
barnettZQG 2014a92a46
Fix: helmValues component support custom the configuration key (#494) (#496)
* Feat: helmValues component support custom the configuration key

Signed-off-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>

* Fix: kv value type update

Signed-off-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>

* Fix: helm select kv update

Signed-off-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>

Co-authored-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>

Co-authored-by: wangbow <18700441876@126.com>
Co-authored-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>
2022-05-11 17:36:57 +08:00
barnettZQG 59552e913e
Fix: Fallback local language to en if not supported (#492)
* Fallback local language to en if not supported (#489)

SOLVES https://github.com/oam-dev/velaux/issues/488

Currently only en and zh are the only supported languages.
If for example in reason of other browser language settings, the locales doesn't exist and the ui is not visible (after login).
As workaround it is possible to change the browser language (bad way).

The solution is to add a fallback language.

Signed-off-by: Manuel Ruck <git@manuelruck.de>

Co-authored-by: Manuel Ruck <git@manuelruck.de>

* Feat: change version to 1.3.3

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

Co-authored-by: ManAnRuck <gitHub@manuelruck.de>
Co-authored-by: Manuel Ruck <git@manuelruck.de>
2022-04-29 15:46:59 +08:00
github-actions[bot] fc01f2d0de
[Backport release-1.3] Fix: can not query instances when the application is abnormal (#485)
* Fix: can not query instances when the application is abnormal

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit bfecba61cd)

* Fix: the app logs shown bug

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 1e083c4ebe)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-04-25 13:48:39 +08:00
github-actions[bot] c894697eab
Fix: the additional parameter does not take effect (#483)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 4dda91b499)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-04-25 11:16:05 +08:00
barnettZQG 6e233efbd7 Chore: change version to 1.3.2
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
2022-04-22 17:33:42 +08:00
github-actions[bot] d7821a8bd2
Feat: support the telemetry data collection (#478)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 2ab397ea25)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-04-22 16:59:21 +08:00
github-actions[bot] c221fa0c35
[Backport release-1.3] Fix: configType page bug & helmReport update (#475)
* Fix: configType page bug & helmReport update

Signed-off-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>
(cherry picked from commit 354687beb8)

* Fix: configType page bug & helmReport update

Signed-off-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>
(cherry picked from commit 39925eb71f)

* Fix: user page placehodlanguage change update

Signed-off-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>
(cherry picked from commit c950955b84)

Co-authored-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>
2022-04-21 23:03:39 +08:00
wangbow 09a2ce95f7
Fix: add singleton local implements (#473)
Signed-off-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>

Co-authored-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>
2022-04-20 10:18:53 +08:00
barnettZQG e3604c1efd
Chore: cherry pick the main branch to 1.3 (#470)
* Chore: change the contributing document (#453)

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Fix: the local cluster name not shown (#455)

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Chore: change the branches (#457)

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Fix: refresh token can not work (#458)

Signed-off-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>

Co-authored-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>

* Fix: support login i18n (#465)

Signed-off-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>

Co-authored-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>

* Feat: admin user first login need update password && email (#462)

* Feat: admin user first login need update password && email

Signed-off-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>

* Fix: admin update paasword & email

Signed-off-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>

* Fix: admin update paasword & email

Signed-off-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>

* Feat: admin user first login need update password && email

Signed-off-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>

* Fix: admin user first login need update password && email update

Signed-off-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>

Co-authored-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>

* Fix: sso dex error, guide users to login page (#467)

* Fix: sso dex error, guide users to login page

Signed-off-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>

* Fix: sso dex error, guide users to login page

Signed-off-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>

Co-authored-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>

Co-authored-by: wangbow <18700441876@126.com>
Co-authored-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>
2022-04-19 17:05:13 +08:00
github-actions[bot] 50a750b9dc
Fix: support login i18n (#468)
Signed-off-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>
(cherry picked from commit 039e3eddd7)

Co-authored-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>
2022-04-19 10:03:18 +08:00
github-actions[bot] 1c086118c3
Fix: refresh token can not work (#460)
Signed-off-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>
(cherry picked from commit 62c579b562)

Co-authored-by: wb-wb665667 <wb-wb665667@alibaba-inc.com>
2022-04-15 14:21:43 +08:00
github-actions[bot] 1e5c64af81
Chore: change the branches (#459)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit dc27841315)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-04-15 13:29:18 +08:00
github-actions[bot] 3ea7661fe7
Fix: the local cluster name not shown (#456)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 317a2f8b73)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-04-15 12:06:27 +08:00
92 changed files with 1044 additions and 837 deletions

View File

@ -2,9 +2,14 @@ name: staticcheck
on:
push:
branches: [ main, release-* ]
branches:
- main
- release-*
workflow_dispatch: {}
pull_request:
branches: [ main, release-* ]
branches:
- main
- release-*
jobs:
detect-noop:

View File

@ -1,22 +1,22 @@
# CONTRIBUTING Guide
## About velaux
## About VelaUX
KubeVela control plane dashboard. Designed as an extensible, application-oriented delivery control panel.
The [KubeVela](https://github.com/oam-dev/kubevela) User Experience (UX) Dashboard, it provides out-of-box application delivery and management platform.
To help us create a safe and positive community experience for all, we require all participants to adhere to the [Code of Conduct](https://github.com/oam-dev/kubevela/blob/master/CODE_OF_CONDUCT.md).
This document is a guide to help you through the process of contributing to velaux.
This document is a guide to help you through the process of contributing to VelaUX.
## Become a contributor
You can contribute to velaux in several ways. Here are some examples:
You can contribute to VelaUX in several ways. Here are some examples:
* Contribute to the velaux codebase.
* Contribute to the VelaUX codebase.
* Report and triage bugs.
* Write technical documentation and blog posts, for users and contributors.
* Organize meetups and user groups in your local area.
* Help others by answering questions about velaux.
* Help others by answering questions about VelaUX.
For more ways to contribute, check out the [Open Source Guides](https://opensource.guide/how-to-contribute/).
@ -27,7 +27,7 @@ Unsure where to begin contributing to KubeVela? Start by browsing issues labeled
- [Good first issue](https://github.com/oam-dev/velaux/labels/good%20first%20issue) issues are generally straightforward to complete.
- [Help wanted](https://github.com/oam-dev/velaux/labels/help%20wanted) 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 [frontend development](docs/contributing/frontend.md) and [backend development](docs/contributing/backend.md).
If you're looking to make a code change, see [how to set up your environment](docs/contributing/velaux.md).
When you're ready to contribute, it's time to [Create a pull request](https://github.com/oam-dev/kubevela/blob/master/contribute/create-pull-request.md).

View File

@ -1,6 +1,6 @@
![alt](docs/images/KubeVela-03.png)
# Velaux
# VelaUX
## Overview
@ -22,7 +22,6 @@ echo "BASE_DOMAIN='http://127.0.0.1:8000'" > .env
## Community
- Slack: [CNCF Slack](https://slack.cncf.io/) #kubevela channel (*English*)
- Gitter: [oam-dev](https://gitter.im/oam-dev/community) (*English*)
- [DingTalk Group](https://page.dingtalk.com/wow/dingtalk/act/en-home): `23310022` (*Chinese*)
- Wechat Group (*Chinese*) : Broker wechat to add you into the user group.

View File

@ -1,115 +0,0 @@
# How to Develop Backend Services
## Preparation
- Install [go](https://golang.org/dl/)
- Install [yarn](https://yarnpkg.com/)
- Install [protoc](https://grpc.io/docs/protoc-installation/) and [protoc-gen-go](https://grpc.io/docs/languages/go/quickstart/#prerequisites)
- Install [mongodb](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/#install-mongodb-community-edition)
## Development
In [frontend development](./frontend.md), developers will use mock backend services first.
But eventually the production rollout of velacp will have both frontend and backend services served.
Developing backend services in velacp has a well-defined architecture and systematic process. We will walk through the steps in the following.
Let's assume we are building a new service "Cluster".
The process goes as:
1. All public APIs, including records stored in database, should be defined in protobuf. In `pkg/proto/`, add a new protobuf definition:
```
touch pkg/proto/cluster.proto
```
Add definitions for cluster service API types:
```protobuf
message Cluster {
string name = 1;
...
}
```
1. Generate go code:
```
make proto
```
1. Add the service endpoints. First create a new service in `pkg/rest/services/`:
```
touch pkg/rest/services//cluster.go
```
Implement the services:
```go
type ClusterService struct {
...
}
func NewClusterService(store storeadapter.ClusterStore) *ClusterService {
return &ClusterService{
...
}
}
func (s *ClusterService) GetClusters(c echo.Context) error {
...
}
```
The new service needs to be registered to in `pkg/rest/rest_server.go`:
```go
func (s *restServer) registerServices() {
...
clusterService := services.NewClusterService(storeadapter.NewClusterStore(s.ds))
s.server.GET("/api/clusters", clusterService.GetClusters)
}
```
1. There is a generic datastore interface defined in `pkg/datastore/datastore.go`. Its mongo backend is implemented in `pkg/datastore/mongodb/mongodb.go`. For each service, you will implement a more specific store adapter to handle its own types and special logic, e.g. ClusterStore.
All specific store adapter is defined in `pkg/datastore/storeadapter/`. Create one for ClusterStore:
```
touch pkg/datastore/storeadapter/clusterstore.go
```
We can see its interface:
```go
type ClusterStore interface {
PutCluster(cluster *model.Cluster) error
ListClusters() ([]*model.Cluster, error)
DelCluster(name string) error
}
```
Its returned model, i.e. `model.Cluster`, is defined in the `pkg/proto/model/cluster.proto`.
1. Once the code is done, build it:
```
make build
```
This will include the frontend as well.
1. Start [mongodb](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/#run-mongodb-community-edition).
Ensure env `MONGO_URL` is set, e.g. "127.0.0.1:27017".
1. Start velacp:
```bash
_bin/velacp server \
--db-url=${MONGO_URL} \
--db-name=vela
```
You can now test the APIs and UIs.

View File

@ -1,85 +0,0 @@
# How to Develop UI Components
## Preparation
- Install [yarn](https://yarnpkg.com/)
## Development
The UI code is under ui/ . First, go to the folder:
```
cd ui/
```
Developing UI components in velacp has a well-defined architecture and systematic process. We will walk through the steps in the following.
Let's assume we are building a new UI page "Cluster".
The process goes as:
1. In `config/routes.ts`, add your route and component.
1. Add a new component in src/pages/:
```
mkdir src/pages/Cluster/
```
Develop and add your component code there.
1. For remote services, create interfaces in `services/kubevela`:
```
touch clusterapi.ts
```
Add your API calls there:
```js
import { request } from "umi";
export async function listClusterNames() {
return request<{ clusters: string[] }>('/api/clusternames');
}
```
For API types, define in `src/services/kubevela/typings.d.ts`:
```js
declare namespace API {
export type ApplicationType = {
name: string;
desc?: string;
updatedAt?: number; // unix milliseconds
components?: ComponentType[];
};
}
```
They should belong to API global namespace.
1. To mock the backend APIs, create mock servers in `mock/`:
```
touch mock/cluster.tx
```
Expose the mock services:
```js
export default {
"GET /api/clusternames": getClusterNames,
"GET /api/clusters": getClusters,
"POST /api/clusters": postClusters,
};
```
1. Run the UI to see the result:
```bash
# run `yarn` first if you haven't installed dependencies
yarn start
```

View File

@ -0,0 +1,30 @@
# Contribute VelaUX
Before start, please make sure you have already started the vela api server environment.
If your api server address is 'http://127.0.0.1:8000', configure the api server address:
```shell
echo "BASE_DOMAIN='http://127.0.0.1:8000'" > .env
```
Make sure you have installed [yarn](https://classic.yarnpkg.com/en/docs/install).
Run this project:
```shell
yarn install
yarn start
```
* Check the code style
```shell
yarn lint
```
* Test the code
```shell
yarn test
```

View File

@ -1,96 +0,0 @@
# Environment
User scenarios:
- Users want to define base application template and add patches based on environment. For example, in dev environment users would use ephemeral disk, while in prod environment use persistent volumes.
- Users want to define environments as the shared-bases for applications. For example, applications might need shared definitions, secrets, health checks, network gateways, etc.
To support the above scenarios, we will add the following concepts:
- AppTemplate that defines the template base as well as the env-based patches:
```yaml
name: example-app-template
base: # the base template
components:
- name: backend
settings:
cmd:
- /bin/myservice
traits:
- name: logging
properties:
rotate: 1d
patch: # kustomize-style overlay patch to base template based on env matching
- envs:
- dev # the name of the Environment
components:
- name: backend
settings:
image: dev/myimage
traits:
- name: scaler
properties:
replicas: 1
- envs:
- prod-beijing
- prod-shenzhen
components:
- name: backend
settings:
image: production/myimage
traits:
- name: scaler
properties:
replicas: 10
```
- Environment that defines a shared-base for applications:
```yaml
name: prod
clusters: # deploy to the following clusters
- prod-cluster
secrets: # The secrets that will be created for applications if not existed.
- name: redis
data:
url: redis-url
password: redis-password
definitions: # The definitions that will be created for applications if not existed.
- type: Component
name: function
source:
git: catalog-url
path: pacakge/function
- type: Trait
name: logging
source:
git: catalog-url
path: package/logging
```
> Note: we will define a definition catalog/package format: https://github.com/hongchaodeng/catalog-example
With the above concepts, the user story goes as:
- The operations/admin team sets up environments first
- The developer users prepare app templates and individual patches for each environment that will have apps to deploy to.
- Users choose a template, then choose an environment, and deploy!
- velacp will render the final Application based on the template and env
- velacp will prepare the necessary secrets, definitions in the environment for the applications
- The Application can use any of the definitions, secrets in this env.
```
Applicaton deployment workflow:
Env -> Controller -> (Secrets + Definitions)
AppTemplate + Env -> velacp -> Application -> Controller
```
Notes:
- AppTemplate will be implemented in velacp. Environment will be implemented as a CRD.

View File

@ -1,133 +0,0 @@
# Environment
DEPRECATED!!!
## 1. Introduction
An environment is a shared infra-base consisting of the same clusters, packages (equiv. capabilities), etc.
to which the applications are deployed.
Below are two examples of `staging-env` and `prod-env` environments:
```yaml
name: staging-env
clusters:
- name: staging-cluster
packages:
- catalog: staging-catalog
package: inmem-logging
---
name: prod-env
clusters:
- name: prod-cluster
packages:
- catalog: prod-catalog
package: loki-logging
```
We are going to explain what an environment includes in the following.
## 2. Clusters
An environment consists of a group of clusters. For example, multiple small clusters from different data centers could form a staging environment to simplify management and serve maximum availability. While each production environment might consist of only one cluster and users need to calibrate high availability for their apps.
When deploying apps to an environment, you might specify only the environment which will deploy to all clusters. Or you might pick some clusters within an environment.
```yaml
# When deploying an app you could choose env and select clusters within the env.
name: example-app
env: env-1
clusters:
- cluster-1
```
The clusters that environments reference to are abstraction over a k8s cluster. It could be an existing one which should have credentials set, or a need-to-be-created one that VelaCP can call over some cloud providers (e.g. ACK, GKE, EKS) to create one.
### Cluster Setup Workflow
Before creating any Environment, users need to create Clusters first. Here is an example:
```yaml
name: prod-cluster
spec:
external: # This is pointing to externally managed clusters without VelaCP reconciling
kubeconfig: "..."
managed: # This would trigger cluster reconciler in VelaCP to create and manage a cluster
provider: ack
parameters:
master:
instanceType: ecs.g6.large
worker:
instanceType: ecs.g6.small
replicas: 3
cni: terway
```
Once the cluster is setup, then create environments to reference it:
```yaml
name: prod-env
clusters:
- prod-cluster
```
## 3. Packages
Within an environment, deployment on all clusters should be consistent. Thus, all packages should be installed the same across clusters.
To provide such guarantee, environment includes `packages` list to ensure those dependencies are declared and installed consistently.
A package is an abstraction over manifests to prepare infrastructure environments. It could be Helm Charts, Kube resources, or Terraform resources. You can use it to set up Operators (Prometheus, ELK, Istio), RBAC rules, OAM Definitions, etc.
### Package Setup Workflow
First of all, the packages should be uploaded to a remote store. We support two kinds of storage backends:
- Git.
- Object storage.
The catalog and package structure must follow predefined format. Here is an example of the structure format: [catalog-example](https://github.com/hongchaodeng/catalog-example).
Before creating the Environment, create the Catalogs first. Here is an example:
```yaml
name: prod-catalog
spec:
git:
url: https://github.com/hongchaodeng/catalog-example
rootdir: catalog/
oss:
url: https://oss.aliyun.com/bucket_name/
```
Finally create the environment:
```yaml
name: prod-env
clusters:
- prod-cluster
packages:
- catalog: prod-catalog
package: grafana
```
By doing this, the environment reconciler from VelaCP would retrieve the packages from the catalog, and then:
- if the package is not installed, install the package
- if the package has been installed and version is older, upgrade the package
Note that the pacakge could be of type of Helm Chart or Kube resources or Terraform resources, no worry about that.
VelaCP will take care of them under the hood and use corresponding tooling to do the installation:
- Kube resources: same as `kubectl apply`
- Helm chart: same as `helm install`/`helm upgrade`
- Terraform resources: same as `terraform apply`
## 4. Env-based Config Patch
You can also do per-environment configuration management based on app templates.
Check out [this doc](env_based_patch.md)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

View File

@ -1,6 +1,6 @@
{
"name": "valaux",
"version": "1.3.1",
"version": "1.3.4",
"private": true,
"scripts": {
"start": "node scripts/start.js",

View File

@ -480,3 +480,6 @@ a {
width: 100%;
margin: 0 auto;
}
.inline-block {
display: inline-block;
}

View File

@ -12,10 +12,20 @@ export type Props = {
const Item: React.FC<Props> = (props) => {
return (
<Row style={{ marginBottom: props.marginBottom || '16px' }}>
<Col span={props.labelSpan ? props.labelSpan : 8}>
<Col
span={props.labelSpan ? props.labelSpan : 8}
style={{
display: 'flex',
justifyItems: 'center',
alignItems: 'center',
}}
>
<span style={{ fontSize: '14px', color: '#a6a6a6' }}>{props.label}:</span>
</Col>
<Col span={props.labelSpan ? 24 - props.labelSpan : 16} style={{ fontSize: '14px' }}>
<Col
span={props.labelSpan ? 24 - props.labelSpan : 16}
style={{ fontSize: '14px', display: 'flex' }}
>
{props.value}
</Col>
</Row>

View File

@ -1,7 +1,7 @@
import React from 'react';
import { connect } from 'dva';
import { routerRedux } from 'dva/router';
import { Field, Grid, Radio, Input, Message } from '@b-design/ui';
import { Field, Grid, Radio, Input, Message, Icon, Switch } from '@b-design/ui';
import { Dialog, Card, Button, Form } from '@b-design/ui';
import Translation from '../../components/Translation';
import locale from '../../utils/locale';
@ -10,6 +10,7 @@ import type { SystemInfo } from '../../interface/system';
import type { LoginUserInfo } from '../../interface/user';
import { updateSystemInfo } from '../../api/config';
import { checkPermission } from '../../utils/permission';
import Item from '../Item';
const { Col, Row } = Grid;
type Props = {
@ -164,10 +165,10 @@ class PlatformSetting extends React.Component<Props, State> {
return domain;
};
render() {
const { onClose, platformSetting } = this.props;
const { onClose, platformSetting, systemInfo } = this.props;
return (
<Dialog
locale={locale.Dialog}
locale={locale().Dialog}
visible={platformSetting}
className={'commonDialog'}
title={i18n.t('Platform Setting')}
@ -191,7 +192,7 @@ class PlatformSetting extends React.Component<Props, State> {
<Form field={this.field} labelCol={{ fixedSpan: 8 }} wrapperCol={{ span: 16 }}>
<Card
style={{ marginBottom: '16px' }}
locale={locale.Card}
locale={locale().Card}
contentHeight="200px"
title={<Translation>User authentication configuration</Translation>}
>
@ -229,14 +230,33 @@ class PlatformSetting extends React.Component<Props, State> {
</Row>
</Card>
{/* <Card
<Card
style={{ marginBottom: '16px' }}
locale={locale.Card}
locale={locale().Card}
contentHeight="200px"
title={<Translation>User experience improvement plan</Translation>}
title={
<span>
<Translation>User experience improvement plan</Translation>
<a target="_blank" href="https://kubevela.io/docs/reference/user-improvement-plan">
<Icon style={{ marginLeft: '4px' }} type="help" />
</a>
</span>
}
>
<Form.Item />
</Card> */}
<Item
label={i18n.t('Contribution')}
value={
<Switch
size="medium"
{...this.field.init('enableCollection', {
rules: [{ required: true }],
initValue: systemInfo.enableCollection,
})}
checked={this.field.getValue('enableCollection')}
/>
}
/>
</Card>
</Form>
</Dialog>
);

View File

@ -19,7 +19,7 @@ class StatusShow extends React.Component<Props> {
const { applicationStatus, onClose, loading, title } = this.props;
return (
<Dialog
locale={locale.Dialog}
locale={locale().Dialog}
visible={true}
className={'commonDialog'}
title={title}
@ -42,11 +42,11 @@ class StatusShow extends React.Component<Props> {
>
<Loading visible={loading} style={{ width: '100%' }}>
<Card
locale={locale.Card}
locale={locale().Card}
contentHeight="200px"
title={<Translation>Applied Resources</Translation>}
>
<Table locale={locale.Table} dataSource={applicationStatus?.appliedResources}>
<Table locale={locale().Table} dataSource={applicationStatus?.appliedResources}>
<Table.Column
dataIndex="name"
width="150px"
@ -76,12 +76,12 @@ class StatusShow extends React.Component<Props> {
</Card>
<If condition={applicationStatus?.conditions}>
<Card
locale={locale.Card}
locale={locale().Card}
style={{ marginTop: '8px' }}
contentHeight="auto"
title={<Translation>Conditions</Translation>}
>
<Table locale={locale.Table} dataSource={applicationStatus?.conditions}>
<Table locale={locale().Table} dataSource={applicationStatus?.conditions}>
<Table.Column
width="150px"
dataIndex="type"
@ -118,13 +118,13 @@ class StatusShow extends React.Component<Props> {
</If>
<If condition={applicationStatus?.services}>
<Card
locale={locale.Card}
locale={locale().Card}
style={{ marginTop: '8px', marginBottom: '16px' }}
contentHeight="auto"
title={<Translation>Component Status</Translation>}
>
<Table
locale={locale.Table}
locale={locale().Table}
className="customTable"
dataSource={applicationStatus?.services}
>

View File

@ -373,7 +373,7 @@ class UISchema extends Component<Props, State> {
>
<Select
disabled={disableEdit}
locale={locale.Select}
locale={locale().Select}
{...init(param.jsonKey, {
initValue: initValue,
rules: convertRule(param.validate),

View File

@ -127,7 +127,7 @@ class Group extends React.Component<Props, State> {
this.setState({ enable: event, closed: false, checked: false });
this.removeJsonKeyValue();
},
locale: locale.Dialog,
locale: locale().Dialog,
});
}
}}

View File

@ -93,7 +93,7 @@ class HelmChartSelect extends Component<Props, State> {
disabled={disabled}
value={value}
dataSource={dataSource}
locale={locale.Select}
locale={locale().Select}
/>
</Loading>
);

View File

@ -101,7 +101,7 @@ class HelmChartVersionSelect extends Component<Props, State> {
disabled={disabled}
value={value}
dataSource={dataSource}
locale={locale.Select}
locale={locale().Select}
/>
</Loading>
);

View File

@ -92,13 +92,24 @@ class HelmRepoSelect extends Component<Props, State> {
return findSecretObj?.secretName || '';
};
convertHelmRepositoryOptions(data: HelmRepo[]): { label: string; value: string }[] {
return (data || []).map((item: { url: string; secretName?: string }) => {
let label = item.url;
if (item.secretName) {
label = `(${item.secretName}) ${item.url}`;
}
return { label: label, value: item.url };
});
}
render() {
const { disabled, value } = this.props;
const { repos, loading, inputRepo } = this.state;
const dataSource = repos.map((repo) => repo.url);
const dataSource = repos;
if (inputRepo) {
dataSource.unshift(inputRepo);
dataSource.unshift({ url: inputRepo, type: 'helm' });
}
const transDataSource = this.convertHelmRepositoryOptions(dataSource);
return (
<Loading visible={loading} style={{ width: '100%' }}>
<Select
@ -110,8 +121,8 @@ class HelmRepoSelect extends Component<Props, State> {
onSearch={this.onSearch}
followTrigger={true}
value={value}
dataSource={dataSource}
locale={locale.Select}
dataSource={transDataSource}
locale={locale().Select}
/>
</Loading>
);

View File

@ -23,22 +23,16 @@ type Props = {
type State = {
items: Item[];
inputValue?: string;
};
type Item = {
key: string;
label: string;
value?: any;
valueType: any;
};
function getEmptyItem() {
return {
key: Date.now().toString(),
label: '',
value: '',
};
}
class KV extends Component<Props, State> {
form: Field;
constructor(props: any) {
@ -48,7 +42,6 @@ class KV extends Component<Props, State> {
};
this.form = new Field(this, {
onChange: (name: string, value: string) => {
this.submit();
const { keyOptions } = this.props;
if (keyOptions && name.indexOf('envKey-') > -1) {
const itemKey = name.substring(name.indexOf('-') + 1);
@ -57,11 +50,13 @@ class KV extends Component<Props, State> {
const newItems = items.map((item) => {
if (item.key == itemKey) {
item.value = keyOptions[value];
item.valueType = this.getValueType(keyOptions[value]);
}
return item;
});
this.setState({ items: newItems });
}
this.submit();
},
});
}
@ -77,17 +72,28 @@ class KV extends Component<Props, State> {
if (value) {
for (const label in value) {
const key = Date.now().toString() + label;
newItems.push({ key: key, label: label, value: value[label] });
newItems.push({
key: key,
label: label,
value: value[label],
valueType: this.getValueType(value[label]),
});
this.form.setValue('envKey-' + key, label);
this.form.setValue('envValue-' + key, value[label]);
}
}
this.setState({ items: newItems });
};
addItem() {
const { items } = this.state;
items.push(getEmptyItem());
items.push({
key: Date.now().toString(),
label: '',
value: '',
valueType: this.getValueType(''),
});
this.setState({ items: [...items] });
}
@ -99,7 +105,7 @@ class KV extends Component<Props, State> {
const index = key.replace('envKey-', '');
let item = items.get(index);
if (!item) {
item = { key: '', label: '' };
item = { key: '', label: '', valueType: 'string' };
}
item.label = values[key];
items.set(index, item);
@ -109,7 +115,7 @@ class KV extends Component<Props, State> {
const index = key.replace('envValue-', '');
let item = items.get(index);
if (!item) {
item = { key: '', label: '' };
item = { key: '', label: '', valueType: 'string' };
}
item.value = values[key];
items.set(index, item);
@ -138,27 +144,51 @@ class KV extends Component<Props, State> {
this.submit();
}
render() {
const { items } = this.state;
const { id, additional, additionalParameter, keyOptions } = this.props;
const { init } = this.form;
let valueType = 'string';
if (additional && additionalParameter) {
// TODO: current only support one parameter
if (additionalParameter.uiType == 'Number') {
valueType = 'number';
}
if (additionalParameter.uiType == 'Switch') {
valueType = 'boolean';
onSearch = (value: string) => {
this.setState({ inputValue: value });
};
getValueType = (value: any) => {
const findValueType = this.matchOutSideValueType();
const valueTypeAdditionalParam = ['number', 'boolean'];
if (valueTypeAdditionalParam.includes(findValueType)) {
return findValueType;
} else {
if (value != undefined) {
return typeof value;
} else {
return 'string';
}
}
};
matchOutSideValueType = () => {
const { additional, additionalParameter } = this.props;
const outSideValueType = [
{ uiType: 'Number', valueType: 'number' },
{ uiType: 'Switch', valueType: 'boolean' },
];
if (additional && additionalParameter && additionalParameter.uiType) {
const matchValueTypeObj = _.find(outSideValueType, (item) => {
return item.uiType === additionalParameter.uiType;
});
return (matchValueTypeObj && matchValueTypeObj.valueType) || 'string';
} else {
return 'string';
}
};
render() {
const { items, inputValue } = this.state;
const { id, keyOptions } = this.props;
const { init } = this.form;
const dataSource = keyOptions ? Object.keys(keyOptions) : [];
if (inputValue) {
dataSource.push(inputValue);
}
return (
<div id={id}>
{items.map((item) => {
if (item.value != undefined) {
valueType = typeof item.value;
}
return (
<Row key={item.key} gutter="20">
<Col span={10}>
@ -171,7 +201,8 @@ class KV extends Component<Props, State> {
{...init(`envKey-${item.key}`)}
label={'Key'}
placeholder={i18n.t('Please select')}
locale={locale.Select}
locale={locale().Select}
onSearch={this.onSearch}
/>
</If>
<If condition={!keyOptions}>
@ -187,17 +218,21 @@ class KV extends Component<Props, State> {
</Col>
<Col span={10}>
<Form.Item>
<If condition={valueType == 'number' || valueType == 'string'}>
<If condition={item.valueType == 'number' || item.valueType == 'string'}>
<Input
disabled={this.props.disabled}
htmlType={valueType == 'number' ? 'number' : ''}
htmlType={item.valueType == 'number' ? 'number' : ''}
{...init(`envValue-${item.key}`)}
label={'Value'}
className="full-width"
placeholder={i18n.t('Please input or select key')}
placeholder={i18n.t(
item.valueType == 'number'
? 'Please input a number'
: 'Please input a value',
)}
/>
</If>
<If condition={valueType == 'boolean'}>
<If condition={item.valueType == 'boolean'}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ lineHeight: '36px', marginRight: '16px' }}>Value:</span>
<Switch

View File

@ -27,7 +27,7 @@ class SecretKeySelect extends React.Component<Props, State> {
const { onChange, value, secretKeys, id, disabled } = this.props;
return (
<Select
locale={locale.Select}
locale={locale().Select}
onChange={onChange}
defaultValue={value}
id={id}

View File

@ -65,7 +65,7 @@ class SecretSelect extends React.Component<Props, State> {
const filters = secrets?.filter((secret) => secret.metadata.labels['app.oam.dev/sync-alias']);
return (
<Select
locale={locale.Select}
locale={locale().Select}
onChange={this.onChange}
value={value}
id={id}

View File

@ -3,10 +3,20 @@ export interface SystemInfo {
velaVersion: string;
gitVersion: string;
};
createTime: string;
installTime: string;
enableCollection: boolean;
loginType?: boolean;
installID: string;
platformID: string;
statisticInfo: {
clusterCount?: string;
appCount?: string;
enableAddonList?: Record<string, string>;
componentDefinitionTopList?: string[];
traitDefinitionTopList?: string[];
workflowDefinitionTopList?: string[];
policyDefinitionTopList?: string[];
updateTime: string;
};
}
export interface UpdateSystemInfo {

View File

@ -102,7 +102,7 @@ class EnvBindPlanDialog extends Component<Props, State> {
createApplicationEnv(params)
.then((res) => {
if (res) {
Message.success(<Translation>Bind Environment Success</Translation>);
Message.success(<Translation>Environment bound successfully</Translation>);
this.props.onOK();
}
})
@ -115,7 +115,7 @@ class EnvBindPlanDialog extends Component<Props, State> {
updateApplicationEnv(params)
.then((res: any) => {
if (res) {
Message.success(<Translation>Bind Environment Success</Translation>);
Message.success(<Translation>Environment bound successfully</Translation>);
this.props.onOK();
}
})
@ -178,7 +178,7 @@ class EnvBindPlanDialog extends Component<Props, State> {
<React.Fragment>
<Dialog
visible={true}
locale={locale.Dialog}
locale={locale().Dialog}
className={'commonDialog'}
style={{ width: '600px' }}
isFullScreen={true}
@ -211,7 +211,7 @@ class EnvBindPlanDialog extends Component<Props, State> {
>
<Select
name="name"
locale={locale.Select}
locale={locale().Select}
disabled={isEdit ? true : false}
dataSource={envOption}
maxLength={32}

View File

@ -54,7 +54,7 @@ class DeployConfigDialog extends Component<Props, State> {
<React.Fragment>
<Dialog
visible={true}
locale={locale.Dialog}
locale={locale().Dialog}
className={'commonDialog deployConfig'}
style={{ width: '600px' }}
isFullScreen={true}

View File

@ -86,7 +86,7 @@ class ApplicationHeader extends Component<Props, State> {
onOk: () => {
this.onDeploy(workflowName, true);
},
locale: locale.Dialog,
locale: locale().Dialog,
});
} else {
handleError(err);
@ -190,7 +190,7 @@ class ApplicationHeader extends Component<Props, State> {
</Row>
<Row wrap={true}>
<Col xl={12} m={12} s={24} className="padding16">
<Card locale={locale.Card}>
<Card locale={locale().Card}>
<Row>
<Col span={6} style={{ padding: '22px 0' }}>
<NumItem
@ -221,7 +221,7 @@ class ApplicationHeader extends Component<Props, State> {
</Col>
<Col xl={12} m={12} s={24} className="padding16">
<If condition={!records || (Array.isArray(records) && records.length === 0)}>
<Card locale={locale.Card}>
<Card locale={locale().Card}>
<Empty
message={<Translation>There is no running workflow</Translation>}
iconWidth={'30px'}

View File

@ -74,7 +74,7 @@ class Menu extends Component<Props, any> {
}
const activeKey = currentPath.substring(currentPath.lastIndexOf('/') + 1);
return (
<Card locale={locale.Card} contentHeight="100px" className="app-menu">
<Card locale={locale().Card} contentHeight="100px" className="app-menu">
{activeItems.map((item) => {
return (
<Link

View File

@ -0,0 +1,162 @@
import React, { Component, Fragment } from 'react';
import { Grid, Form, Input, Field, Button, Message, Icon, Dialog } from '@b-design/ui';
import { updateUser } from '../../../../api/users';
import type { LoginUserInfo } from '../../../../interface/user';
import { checkUserPassword, checkUserEmail } from '../../../../utils/common';
import Translation from '../../../../components/Translation';
import i18n from '../../../../i18n';
type Props = {
userInfo?: LoginUserInfo;
onClose: () => void;
};
type State = {
isLoading: boolean;
isLookPassword: boolean;
};
class EditPlatFormUserDialog extends Component<Props, State> {
field: Field;
constructor(props: Props) {
super(props);
this.field = new Field(this);
this.state = {
isLoading: false,
isLookPassword: false,
};
}
onUpdateUser = async () => {
this.field.validate((error: any, values: any) => {
if (error) {
return;
}
const { userInfo } = this.props;
const { email, password } = values;
const params = {
name: userInfo?.name || '',
alias: userInfo?.alias || '',
email,
password,
};
this.setState({
isLoading: true,
});
updateUser(params)
.then((res) => {
if (res) {
Message.success(<Translation>User updated successfully</Translation>);
this.props.onClose();
}
})
.finally(() => {
this.setState({
isLoading: false,
});
});
});
};
showTitle() {
return i18n.t('Reset the password and email for the administrator account');
}
showClickButtons = () => {
const { isLoading } = this.state;
return [
<Button type="primary" onClick={this.onUpdateUser} loading={isLoading}>
{i18n.t('Update')}
</Button>,
];
};
handleClickLook = () => {
this.setState({
isLookPassword: !this.state.isLookPassword,
});
};
render() {
const init = this.field.init;
const { Row, Col } = Grid;
const FormItem = Form.Item;
const formItemLayout = {
labelCol: {
fixedSpan: 6,
},
wrapperCol: {
span: 20,
},
};
return (
<Fragment>
<Dialog
visible={true}
title={this.showTitle()}
style={{ width: '600px' }}
onOk={this.onUpdateUser}
footerActions={['ok']}
>
<Form {...formItemLayout} field={this.field}>
<Row>
<Col span={24} style={{ padding: '0 8px' }}>
<FormItem label={<Translation>Password</Translation>} required>
<Input
name="password"
htmlType={this.state.isLookPassword ? 'passwordInput' : 'password'}
addonTextAfter={
<Icon
style={{
cursor: 'pointer',
}}
type="eye-fill"
onClick={this.handleClickLook}
/>
}
placeholder={i18n.t('Please input the password').toString()}
{...init('password', {
rules: [
{
required: true,
pattern: checkUserPassword,
message: (
<Translation>
Password should be 8-16 bits and contain at least one number and one
letter
</Translation>
),
},
],
})}
/>
</FormItem>
</Col>
</Row>
<Row>
<Col span={24} style={{ padding: '0 8px' }}>
<FormItem label={<Translation>Email</Translation>} required>
<Input
name="email"
placeholder={i18n.t('Please input a email').toString()}
{...init('email', {
rules: [
{
required: true,
pattern: checkUserEmail,
message: <Translation>Please input a valid email</Translation>,
},
],
})}
/>
</FormItem>
</Col>
</Row>
</Form>
</Dialog>
</Fragment>
);
}
}
export default EditPlatFormUserDialog;

View File

@ -6,13 +6,16 @@ import SwitchLanguage from '../../components/SwitchButton/index';
import { withTranslation } from 'react-i18next';
import { connect } from 'dva';
import logo from '../../assets/kubevela-logo-white.png';
import axios from 'axios';
import type { SystemInfo } from '../../interface/system';
import type { LoginUserInfo } from '../../interface/user';
import { If } from 'tsx-control-statements/components';
import Translation from '../../components/Translation';
import Permission from '../../components/Permission';
import PlatformSetting from '../../components/PlatformSetting';
import EditPlatFormUserDialog from './components/EditPlatFormUserDialog';
import { getBrowserNameAndVersion, isAdminUserCheck } from '../../utils/utils';
import { getData, setData } from '../../utils/cache';
type Props = {
dispatch: ({}) => {};
@ -22,16 +25,22 @@ type Props = {
type State = {
platformSetting: boolean;
isEditAdminUser: boolean;
userInfo?: LoginUserInfo;
};
const TelemetryDataCollectionKey = 'telemetryDataCollection';
const TelemetryDataCollectionServer = 'https://telemetry.kubevela.net/collecting';
@connect((store: any) => {
return { ...store.user };
})
class TopBar extends Component<Props, State> {
loadCount: number;
constructor(props: any) {
constructor(props: Props) {
super(props);
this.state = {
platformSetting: false,
isEditAdminUser: false,
};
this.loadCount = 0;
}
@ -44,12 +53,65 @@ class TopBar extends Component<Props, State> {
loadSystemInfo = () => {
this.props.dispatch({
type: 'user/getSystemInfo',
callback: () => {
this.telemetryDataCollection();
},
});
};
telemetryDataCollection = async () => {
const { systemInfo } = this.props;
if (!getData(TelemetryDataCollectionKey) && systemInfo?.enableCollection) {
try {
axios
.post(TelemetryDataCollectionServer, this.buildTelemetryData())
.catch()
.then(() => {
this.setCache();
});
} catch {}
}
};
buildTelemetryData = () => {
const { systemInfo } = this.props;
return {
platformID: systemInfo?.platformID,
installTime: systemInfo?.installTime,
version:
(systemInfo?.systemVersion?.velaVersion || '') +
+'/' +
(systemInfo?.systemVersion?.gitVersion || ''),
clusterCount: systemInfo?.statisticInfo.clusterCount || '',
appCount: systemInfo?.statisticInfo.appCount || '',
enableAddonList: systemInfo?.statisticInfo.enableAddonList || {},
componentDefinitionTopList: systemInfo?.statisticInfo.componentDefinitionTopList,
traitDefinitionTopList: systemInfo?.statisticInfo.traitDefinitionTopList,
workflowStepDefinitionTopList: systemInfo?.statisticInfo.workflowDefinitionTopList,
policyDefinitionTopList: systemInfo?.statisticInfo.policyDefinitionTopList,
browserInfo: {
language: navigator.language,
nameAndVersion: getBrowserNameAndVersion(),
screenWidth: window.screen.width,
screenHeight: window.screen.height,
},
};
};
setCache = () => {
const now = new Date();
now.setHours(now.getHours() + 24);
setData(TelemetryDataCollectionKey, 'true', now);
};
loadUserInfo = () => {
this.props.dispatch({
type: 'user/getLoginUserInfo',
callback: (res: LoginUserInfo) => {
this.setState({ userInfo: res }, () => {
this.isEditPlatForm();
});
},
});
};
@ -67,10 +129,24 @@ class TopBar extends Component<Props, State> {
this.setState({ platformSetting: true });
};
isEditPlatForm = () => {
const { userInfo } = this.state;
const isAdminUser = isAdminUserCheck(userInfo);
if (isAdminUser && userInfo && !userInfo.email) {
this.setState({
isEditAdminUser: true,
});
}
};
onCloseEditAdminUser = () => {
this.setState({ isEditAdminUser: false });
};
render() {
const { Row, Col } = Grid;
const { userInfo, systemInfo, dispatch } = this.props;
const { platformSetting } = this.state;
const { systemInfo, dispatch } = this.props;
const { platformSetting, isEditAdminUser, userInfo } = this.state;
return (
<div className="layout-topbar" id="layout-topbar">
<Row className="nav-wrapper">
@ -197,6 +273,9 @@ class TopBar extends Component<Props, State> {
/>
)}
</If>
<If condition={isEditAdminUser}>
<EditPlatFormUserDialog userInfo={userInfo} onClose={this.onCloseEditAdminUser} />
</If>
</div>
);
}

View File

@ -106,7 +106,7 @@
"Auto-refresh": "自动刷新",
"Baseline Config": "基础配置",
"Bind Environment": "绑定环境",
"Bind Environment Success": "绑定环境成功",
"Environment bound successfully": "绑定环境成功",
"Bucket": "Bucket",
"CPU": "CPU",
"Cancel": "取消",
@ -418,7 +418,8 @@
"Container": "容器",
"Logs": "日志",
"Select Pod": "查询实例",
"Select Container": "查询容器",
"Select Container": "选择容器",
"Select Component": "选择组件",
"Component Config": "组件配置",
"New Component": "新增组件",
"Components": "组件",
@ -515,5 +516,18 @@
"The email address of administrator is empty": "管理员的电子邮件地址为空",
"Please set a email address for the administrator, it must same as the SSO account.": "请为管理员设置电子邮件地址, 它必须与SSO帐户相同",
"No dex connector configurations": "没有dex连接器配置",
"Before enabling SSO, you must add at least one dex connector configuration.": "在启用SSO之前, 必须至少添加一个dex连接器配置."
}
"Before enabling SSO, you must add at least one dex connector configuration.": "在启用SSO之前, 必须至少添加一个dex连接器配置.",
"Are you sure you want to reclaim the current environment?": "您确定要回收当前环境吗?",
"Are you sure you want to delete the current environment binding?": "是否确实要删除当前环境绑定?",
"Recycle application environment success": "回收应用程序环境成功",
"Environment binding deleted successfully": "成功移除环境绑定",
"Retry": "重试",
"Password should be 8-16 bits and contain at least one number and one letter": "密码应为8-16位, 并至少包含一个数字和一个字母",
"Please input a valid email": "请输入有效的电子邮件地址",
"Please input a email": "请输入电子邮箱地址",
"User updated successfully": "用户更新成功",
"Reset the password and email for the administrator account": "重置管理员帐户的密码和电子邮件地址",
"Please input the alias": "请输入别名",
"User experience improvement plan": "用户体验提升计划",
"Contribution": "贡献"
}

View File

@ -26,18 +26,24 @@ const user: any = {
effects: {
*getLoginUserInfo(
action: { payload: { projectName: string } },
action: { payload: Record<string, never>; callback: (data: LoginUserInfo) => void },
{ call, put }: { call: any; put: any },
) {
const result: LoginUserInfo = yield call(getLoginUserInfo, action.payload);
yield put({ type: 'updateUserInfo', payload: result || {} });
if (action.callback && result) {
action.callback(result);
}
},
*getSystemInfo(
action: { payload: { projectName: string } },
action: { payload: Record<string, never>; callback: (data: SystemInfo) => void },
{ call, put }: { call: any; put: any },
) {
const result: SystemInfo = yield call(loadSystemInfo, action.payload);
yield put({ type: 'updateSystemInfo', payload: result || {} });
if (result && action.callback) {
action.callback(result);
}
},
},
};

View File

@ -65,7 +65,7 @@ class CardContent extends React.Component<Props, State> {
});
return (
<Col xl={6} m={8} s={12} xxs={24} className={`card-content-wraper`} key={name}>
<Card locale={locale.Card} contentHeight="auto">
<Card locale={locale().Card} contentHeight="auto">
<a onClick={() => clickAddon(name)}>
<div className="cluster-card-top flexcenter">
<If condition={icon && icon != 'none'}>

View File

@ -164,7 +164,7 @@ class AddonDetailDialog extends React.Component<Props, State> {
content:
'Please make sure that the Addon is no longer in use and the related application has been recycled.',
onOk: this.disableAddon,
locale: locale.Dialog,
locale: locale().Dialog,
});
return;
}
@ -450,7 +450,7 @@ class AddonDetailDialog extends React.Component<Props, State> {
</If>
<If condition={addonDetailInfo?.dependencies}>
<Card
locale={locale.Card}
locale={locale().Card}
contentHeight="auto"
title={<Translation>Dependencies</Translation>}
>
@ -479,7 +479,7 @@ class AddonDetailDialog extends React.Component<Props, State> {
<If condition={addonDetailInfo?.definitions}>
<Card
contentHeight="auto"
locale={locale.Card}
locale={locale().Card}
title={<Translation>Definitions</Translation>}
style={{ marginTop: '16px' }}
>
@ -488,7 +488,7 @@ class AddonDetailDialog extends React.Component<Props, State> {
Enable the addon to obtain the following extension capabilities
</Translation>
</Message>
<Table locale={locale.Table} dataSource={addonDetailInfo?.definitions}>
<Table locale={locale().Table} dataSource={addonDetailInfo?.definitions}>
<Table.Column
dataIndex="name"
align="left"
@ -514,7 +514,7 @@ class AddonDetailDialog extends React.Component<Props, State> {
</If>
<Card
contentHeight="auto"
locale={locale.Card}
locale={locale().Card}
title={<Translation>Readme</Translation>}
style={{ marginTop: '16px' }}
>

View File

@ -159,7 +159,7 @@ class RegistryManageDialog extends React.Component<Props, State> {
onOk: () => {
this.onDeleteRegistry(name);
},
locale: locale.Dialog,
locale: locale().Dialog,
});
}}
>
@ -211,7 +211,7 @@ class RegistryManageDialog extends React.Component<Props, State> {
return (
<div>
<Dialog
locale={locale.Dialog}
locale={locale().Dialog}
className="commonDialog"
title={<Translation>Registry Management</Translation>}
autoFocus={true}
@ -247,7 +247,7 @@ class RegistryManageDialog extends React.Component<Props, State> {
</div>
</Col>
</Row>
<Table locale={locale.Table} dataSource={registryDataSource}>
<Table locale={locale().Table} dataSource={registryDataSource}>
<Table.Column width="150px" title={<Translation>Name</Translation>} dataIndex="name" />
<Table.Column width="80px" title={<Translation>Type</Translation>} dataIndex="type" />
<Table.Column title={<Translation>URL</Translation>} dataIndex="url" />
@ -296,7 +296,7 @@ class RegistryManageDialog extends React.Component<Props, State> {
help={<Translation>The addon registry type</Translation>}
>
<Select
locale={locale.Select}
locale={locale().Select}
{...init('type', {
rules: [
{

View File

@ -59,7 +59,7 @@ class SelectSearch extends React.Component<Props, State> {
<Row className="app-select-wrapper border-radius-8" wrap={true}>
<Col xl={6} m={8} s={12} xxs={24} style={{ padding: '0 8px' }}>
<Select
locale={locale.Select}
locale={locale().Select}
mode="single"
size="large"
onChange={this.handleChangRegistry}

View File

@ -153,8 +153,7 @@ class ComponentDialog extends React.Component<Props, State> {
if (res) {
Message.success({
duration: 4000,
title: i18n.t('Success'),
content: i18n.t('Create component success.'),
content: i18n.t('Component created successfully'),
});
this.props.onComponentOK();
}
@ -373,7 +372,7 @@ class ComponentDialog extends React.Component<Props, State> {
}
>
<Select
locale={locale.Select}
locale={locale().Select}
showSearch
disabled={isEditComponent ? true : false}
className="select"

View File

@ -31,7 +31,7 @@ class ComponentsList extends Component<Props> {
this.props.onDeleteComponent(name || '');
},
onClose: () => {},
locale: locale.Dialog,
locale: locale().Dialog,
});
};
@ -43,7 +43,7 @@ class ComponentsList extends Component<Props> {
<Row wrap={true}>
{(components || []).map((item: ApplicationComponentBase) => (
<Col xl={8} m={12} s={24} key={item.name} className="padding16">
<Card locale={locale.Card} contentHeight="auto">
<Card locale={locale().Card} contentHeight="auto">
<div className="components-list-nav">
<Permission
request={{

View File

@ -34,7 +34,7 @@ class TraitsList extends Component<Props> {
this.props.onDeleteTrait(traitType || '');
},
onClose: () => {},
locale: locale.Dialog,
locale: locale().Dialog,
});
};
@ -43,7 +43,7 @@ class TraitsList extends Component<Props> {
const { changeTraitStats } = this.props;
return (
<Col xl={12} m={12} s={24} className="padding16 card-trait-wrapper">
<Card locale={locale.Card}>
<Card locale={locale().Card}>
<div className="traits-list-nav">
<div
className="traits-list-title"
@ -75,7 +75,7 @@ class TraitsList extends Component<Props> {
const { Col } = Grid;
return (
<Col xl={12} m={12} s={24} className="padding16 card-add-wrapper">
<Card locale={locale.Card}>
<Card locale={locale().Card}>
<div className="traits-add-operation">
<Icon
type="plus-circle"

View File

@ -245,7 +245,7 @@ class TriggerDialog extends React.Component<Props, State> {
<FormItem label={<Translation>Type</Translation>} required>
<Select
name="type"
locale={locale.Select}
locale={locale().Select}
dataSource={[{ label: 'On Webhook Event', value: 'webhook' }]}
{...init('type', {
initValue: 'webhook',
@ -266,7 +266,7 @@ class TriggerDialog extends React.Component<Props, State> {
<FormItem label={<Translation>PayloadType</Translation>} required>
<Select
name="payloadType"
locale={locale.Select}
locale={locale().Select}
dataSource={payloadTypeOption}
{...init('payloadType', {
initValue: 'custom',
@ -288,7 +288,7 @@ class TriggerDialog extends React.Component<Props, State> {
<FormItem label={<Translation>Execution workflow</Translation>} required>
<Select
name="workflowName"
locale={locale.Select}
locale={locale().Select}
dataSource={workflowOption}
{...init('workflowName', {
rules: [

View File

@ -52,7 +52,7 @@ class TriggerList extends Component<Props, State> {
this.props.onDeleteTrigger(token || '');
},
onClose: () => {},
locale: locale.Dialog,
locale: locale().Dialog,
});
};
@ -108,7 +108,7 @@ class TriggerList extends Component<Props, State> {
<Row wrap={true}>
{(triggers || []).map((item: Trigger) => (
<Col xl={8} m={12} s={24} key={item.type} className="padding16">
<Card free={true} style={{ padding: '16px' }} locale={locale.Card}>
<Card free={true} style={{ padding: '16px' }} locale={locale().Card}>
<div className="trigger-list-nav">
<div className="trigger-list-title">
{item.alias ? `${item.alias}(${item.name})` : item.name}
@ -198,7 +198,7 @@ class TriggerList extends Component<Props, State> {
</Row>
<If condition={showTrigger}>
<Dialog
locale={locale.Dialog}
locale={locale().Dialog}
className="commonDialog"
visible={true}
onClose={this.closeWebhook}

View File

@ -393,7 +393,7 @@ class ApplicationConfig extends Component<Props, State> {
</Row>
<Row>
<Col span={24} className="padding16">
<Card locale={locale.Card} contentHeight="auto">
<Card locale={locale().Card} contentHeight="auto">
<Row wrap={true}>
<Col m={12} xs={24}>
<Item

View File

@ -116,7 +116,7 @@ class ContainerLog extends Component<Props, State> {
return (
<Dialog
className="commonDialog logDialog"
locale={locale.Dialog}
locale={locale().Dialog}
visible={true}
footerActions={[]}
onClose={this.props.onClose}

View File

@ -92,40 +92,40 @@ class Header extends Component<Props, State> {
};
recycleEnv = async () => {
Dialog.confirm({
content: 'Are you sure you want to reclaim the current environment?',
content: i18n.t('Are you sure you want to reclaim the current environment?'),
onOk: () => {
const { applicationDetail, envName, refresh } = this.props;
if (applicationDetail) {
recycleApplicationEnvbinding({ appName: applicationDetail.name, envName: envName }).then(
(re) => {
if (re) {
Message.success('recycle applicationn environment success');
Message.success(i18n.t('Recycle application environment success'));
refresh();
}
},
);
}
},
locale: locale.Dialog,
locale: locale().Dialog,
});
};
deleteEnv = async () => {
Dialog.confirm({
content: 'Are you sure you want to delete the current environment binding?',
content: i18n.t('Are you sure you want to delete the current environment binding?'),
onOk: () => {
const { applicationDetail, envName, updateEnvs } = this.props;
if (applicationDetail) {
deleteApplicationEnvbinding({ appName: applicationDetail.name, envName: envName }).then(
(re) => {
if (re) {
Message.success('delete applicationn environment binding success');
Message.success(i18n.t('Environment binding deleted successfully'));
updateEnvs();
}
},
);
}
},
locale: locale.Dialog,
locale: locale().Dialog,
});
};
@ -173,7 +173,7 @@ class Header extends Component<Props, State> {
<Row className="border-radius-8">
<Col span="4" style={{ marginBottom: '16px' }}>
<Select
locale={locale.Select}
locale={locale().Select}
mode="single"
onChange={this.handleTargetChange}
dataSource={targetOptions}
@ -184,7 +184,7 @@ class Header extends Component<Props, State> {
</Col>
<Col span="4" style={{ marginBottom: '16px', paddingLeft: '16px' }}>
<Select
locale={locale.Select}
locale={locale().Select}
mode="single"
onChange={this.handleComponentChange}
dataSource={componentOptions}

View File

@ -288,7 +288,7 @@ class PodDetail extends React.Component<Props, State> {
hasBorder={false}
primaryKey="name"
loading={loading}
locale={locale.Table}
locale={locale().Table}
>
{containerColumns &&
containerColumns.map((col, key) => <Column {...col} key={key} align={'left'} />)}
@ -300,7 +300,7 @@ class PodDetail extends React.Component<Props, State> {
hasBorder={false}
loading={loading}
primaryKey="time"
locale={locale.Table}
locale={locale().Table}
>
{eventCloumns &&
eventCloumns.map((col, key) => <Column {...col} key={key} align={'left'} />)}

View File

@ -109,12 +109,7 @@ class ApplicationInstanceList extends React.Component<Props, State> {
if (re) {
const status: ApplicationStatus = re.status;
if (status && status.appliedResources) {
const services = status.appliedResources.filter(
(resource) => resource.kind == 'Service',
);
if (services) {
this.loadApplicationEndpoints();
}
this.loadApplicationEndpoints();
}
}
},
@ -220,19 +215,13 @@ class ApplicationInstanceList extends React.Component<Props, State> {
loadAppInstances = async () => {
this.setState({ podList: [] });
const { applicationDetail, envbinding, applicationStatus } = this.props;
const { applicationDetail, envbinding } = this.props;
const {
params: { appName, envName },
} = this.props.match;
const { target, componentName } = this.state;
const envs = envbinding.filter((item) => item.name == envName);
if (
applicationDetail &&
applicationDetail.name &&
envs.length > 0 &&
applicationStatus &&
applicationStatus.services?.length
) {
if (applicationDetail && applicationDetail.name && envs.length > 0) {
if (applicationDetail.applicationType == 'common') {
const param = {
appName: envs[0].appDeployName || appName,
@ -452,7 +441,7 @@ class ApplicationInstanceList extends React.Component<Props, State> {
onCancel: () => {
this.setState({ deployLoading: false });
},
locale: locale.Dialog,
locale: locale().Dialog,
});
} else {
handleError(err);
@ -541,7 +530,7 @@ class ApplicationInstanceList extends React.Component<Props, State> {
<If condition={applicationStatus}>
<If condition={applicationDetail?.applicationType == 'common'}>
<Table
locale={locale.Table}
locale={locale().Table}
className="podlist-table-wraper"
size="medium"
primaryKey={'primaryKey'}
@ -560,7 +549,7 @@ class ApplicationInstanceList extends React.Component<Props, State> {
<If condition={applicationDetail?.applicationType == 'cloud'}>
<Table
size="medium"
locale={locale.Table}
locale={locale().Table}
className="customTable"
dataSource={cloudInstance}
primaryKey={'instanceName'}

View File

@ -390,7 +390,7 @@ class AppDialog extends React.Component<Props, State> {
}
>
<Select
locale={locale.Select}
locale={locale().Select}
showSearch
className="select"
{...init(`componentType`, {
@ -435,7 +435,7 @@ class AppDialog extends React.Component<Props, State> {
},
],
})}
locale={locale.Select}
locale={locale().Select}
mode="multiple"
dataSource={envOptions}
/>

View File

@ -92,7 +92,7 @@ class CardContent extends React.Component<Props, State> {
onOk: () => {
this.onDeleteAppPlan(item.name);
},
locale: locale.Dialog,
locale: locale().Dialog,
});
}}
>
@ -145,7 +145,7 @@ class CardContent extends React.Component<Props, State> {
className={`card-content-wrapper`}
key={`${item.name}`}
>
<Card locale={locale.Card} contentHeight="auto">
<Card locale={locale().Card} contentHeight="auto">
<Link to={`/applications/${name}/config`}>
<div className="appplan-card-top flexcenter">
<If condition={icon && icon != 'none'}>

View File

@ -75,7 +75,7 @@ class ProjectForm extends React.Component<Props, State> {
<div className="cluster-container">
<Select
disabled={disable}
locale={locale.Select}
locale={locale().Select}
className="cluster-params-input"
mode="single"
dataSource={projectList}

View File

@ -114,7 +114,7 @@ class SelectSearch extends React.Component<Props, State> {
<Row className="app-select-wrapper border-radius-8" wrap={true}>
<Col xl={6} m={8} s={12} xxs={24} style={{ padding: '0 8px' }}>
<Select
locale={locale.Select}
locale={locale().Select}
mode="single"
size="large"
onChange={this.onChangeProject}
@ -127,7 +127,7 @@ class SelectSearch extends React.Component<Props, State> {
</Col>
<Col xl={6} m={8} s={12} xxs={24} style={{ padding: '0 8px' }}>
<Select
locale={locale.Select}
locale={locale().Select}
mode="single"
size="large"
onChange={this.onChangeEnv}

View File

@ -119,6 +119,8 @@ class ContainerLog extends Component<Props, State> {
.finally(() => {
this.setState({ loading: false });
});
} else {
this.setState({ loading: false });
}
};

View File

@ -61,9 +61,9 @@ class ApplicationLog extends React.Component<Props, State> {
this.loadApplicationStatus();
}
componentWillReceiveProps(nextProps: any) {
componentWillReceiveProps(nextProps: Props) {
const { params } = nextProps.match;
if (params.envName !== this.state.envName) {
if (params.envName !== this.state.envName || this.props.envbinding != nextProps.envbinding) {
this.setState({ envName: params.envName }, () => {
this.loadApplicationStatus();
});
@ -84,15 +84,14 @@ class ApplicationLog extends React.Component<Props, State> {
};
loadPodInstance = async () => {
const { applicationDetail, envbinding, applicationStatus } = this.props;
const { appName, envName } = this.state;
const { envbinding } = this.props;
const { appName, envName, activeComponentName } = this.state;
const envs = envbinding.filter((item) => item.name === envName);
if (applicationDetail?.name && applicationStatus?.services?.length && envs.length > 0) {
const componentName = applicationStatus.services[0].name;
if (envs.length > 0 && envs[0]) {
const param = {
appName: envs[0].appDeployName || appName,
appNs: envs[0].appDeployNamespace,
name: componentName,
componentName: activeComponentName,
cluster: '',
clusterNs: '',
};
@ -102,12 +101,10 @@ class ApplicationLog extends React.Component<Props, State> {
this.setState(
{
podList: res.podList,
pod: res.podList[0] || {},
activePodName: res.podList[0]?.metadata.name,
activeComponentName: res.podList[0]?.component,
activePodName: '',
},
() => {
this.loadPodDetail();
this.handlePodNameChange(res.podList[0].metadata.name);
},
);
} else {
@ -133,18 +130,26 @@ class ApplicationLog extends React.Component<Props, State> {
Message.warning(res.error);
} else if (res) {
const activeContainerName = (res.containers?.[0] && res.containers[0]?.name) || '';
this.setState({
containers: res.containers,
activeContainerName,
isActiveContainerNameDisabled: activeContainerName ? false : true,
});
this.setState(
{
containers: res.containers,
},
() => {
this.handleContainerNameChange(activeContainerName);
},
);
}
})
.catch(() => {});
}
};
handleComponentNameChange = (value: any) => {
this.setState({ activeComponentName: value });
handleComponentNameChange = (value: string) => {
this.setState(
{ activeComponentName: value, activePodName: '', activeContainerName: '' },
() => {
this.loadPodInstance();
},
);
};
handlePodNameChange = (value: any) => {
const { podList } = this.state;
@ -167,26 +172,10 @@ class ApplicationLog extends React.Component<Props, State> {
};
getComponentNameList = () => {
const { podList } = this.state;
const { components } = this.props;
const componentNameAlias: any = {};
components?.map((c) => {
componentNameAlias[c.name] = c.alias || c.name;
return components?.map((c) => {
return { label: c.alias || c.name, value: c.name };
});
if (podList && podList.length != 0) {
const componentNameList: { label: string; value: string }[] = [];
podList.forEach((item) => {
if (item.component) {
componentNameList.push({
label: componentNameAlias[item.component] || item.component,
value: item.component,
});
}
});
return componentNameList;
} else {
return [];
}
};
getPodNameList = () => {

View File

@ -59,7 +59,7 @@ class Hearder extends React.Component<Props, State> {
<Row className="border-radius-8">
<Col span="6" style={{ padding: '0 8px' }}>
<Select
locale={locale.Select}
locale={locale().Select}
mode="single"
size="small"
onChange={this.handleChangeEnv}
@ -73,7 +73,7 @@ class Hearder extends React.Component<Props, State> {
<Col span="6" style={{ padding: '0 8px' }}>
<Select
locale={locale.Select}
locale={locale().Select}
mode="single"
size="small"
onChange={this.handleChangeStatus}

View File

@ -123,7 +123,7 @@ class TableList extends Component<Props, State> {
return (
<div className="table-version-list margin-top-20">
<Table
locale={locale.Table}
locale={locale().Table}
primaryKey={'version'}
className="customTable"
rowHeight={40}

View File

@ -125,7 +125,7 @@ class ApplicationRevisionList extends React.Component<Props, State> {
getRevisionList={this.getRevisionList}
/>
<Pagination
locale={locale.Pagination}
locale={locale().Pagination}
className="revison-pagenation"
hideOnlyOnePage={true}
total={this.state.revisionsListTotal}

View File

@ -251,7 +251,7 @@ class ApplicationMonitor extends React.Component<Props, State> {
onCancel: () => {
this.setState({ deployLoading: false });
},
locale: locale.Dialog,
locale: locale().Dialog,
});
} else {
handleError(err);
@ -304,11 +304,11 @@ class ApplicationMonitor extends React.Component<Props, State> {
<Loading visible={loading} style={{ width: '100%' }}>
<If condition={applicationStatus}>
<Card
locale={locale.Card}
locale={locale().Card}
contentHeight="200px"
title={<Translation>Applied Resources</Translation>}
>
<Table locale={locale.Table} dataSource={resources}>
<Table locale={locale().Table} dataSource={resources}>
<Table.Column
dataIndex="cluster"
title={<Translation>Cluster</Translation>}
@ -325,9 +325,9 @@ class ApplicationMonitor extends React.Component<Props, State> {
userInfo,
)
) {
return <Link to="/clusters">{v}</Link>;
return <Link to="/clusters">{clusterName}</Link>;
}
return <span>{v}</span>;
return <span>{clusterName}</span>;
}}
/>
<Table.Column
@ -355,7 +355,11 @@ class ApplicationMonitor extends React.Component<Props, State> {
if (row.latest) {
return (
<span>
<Icon style={{ color: 'green', marginRight: '8px' }} type="NEW" />
<Icon
style={{ color: 'green', marginRight: '8px' }}
type="NEW"
title="latest version resource"
/>
<Link to={`/applications/${applicationDetail?.name}/revisions`}>{v}</Link>
</span>
);
@ -369,12 +373,12 @@ class ApplicationMonitor extends React.Component<Props, State> {
</Card>
<If condition={componentStatus}>
<Card
locale={locale.Card}
locale={locale().Card}
style={{ marginTop: '8px', marginBottom: '16px' }}
contentHeight="auto"
title={<Translation>Component Status</Translation>}
>
<Table locale={locale.Table} className="customTable" dataSource={componentStatus}>
<Table locale={locale().Table} className="customTable" dataSource={componentStatus}>
<Table.Column
align="left"
dataIndex="name"
@ -413,12 +417,12 @@ class ApplicationMonitor extends React.Component<Props, State> {
</If>
<If condition={applicationStatus?.conditions}>
<Card
locale={locale.Card}
locale={locale().Card}
style={{ marginTop: '8px' }}
contentHeight="auto"
title={<Translation>Conditions</Translation>}
>
<Table locale={locale.Table} dataSource={applicationStatus?.conditions}>
<Table locale={locale().Table} dataSource={applicationStatus?.conditions}>
<Table.Column
width="150px"
dataIndex="type"

View File

@ -101,7 +101,7 @@ class WorkflowComponent extends Component<Props, State> {
}
});
},
locale: locale.Dialog,
locale: locale().Dialog,
});
};

View File

@ -154,7 +154,7 @@ class WorkflowForm extends Component<Props, State> {
<Col span={24} style={{ padding: '0 8px' }}>
<FormItem label={<Translation>Workflow Type</Translation>} required disabled={edit}>
<Select
locale={locale.Select}
locale={locale().Select}
className="select"
placeholder={t('Please select').toString()}
{...init(`type`, {

View File

@ -1,6 +1,8 @@
import React from 'react';
import { loginSSO } from '../../api/authentication';
import querystring from 'query-string';
import { Dialog, Button } from '@b-design/ui';
import i18n from '../../i18n';
type Props = {
location: {
@ -38,13 +40,29 @@ export default class CallBackPage extends React.Component<Props> {
this.props.history.push('/');
}
})
.catch(() => {
setTimeout(() => {
this.props.history.push('/login');
}, 3000);
.catch((err) => {
let customErrorMessage = '';
if (err.BusinessCode) {
customErrorMessage = `${err.Message}(${err.BusinessCode})`;
} else {
customErrorMessage = 'Please check the network or contact the administrator!';
}
return Dialog.alert({
title: i18n.t('Dex Error'),
content: `${i18n.t(customErrorMessage)}`,
closeable: true,
closeMode: [],
footer: <Button onClick={this.handleClickRetry}>{i18n.t('Retry')}</Button>,
});
});
};
handleClickRetry = () => {
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
};
render() {
return null;
}

View File

@ -153,7 +153,7 @@ class AddClustDialog extends React.Component<Props, State> {
const valueInfo = cluster.kubeConfig || values.kubeConfig || '';
return (
<Dialog
locale={locale.Dialog}
locale={locale().Dialog}
className={'commonDialog'}
title={
editMode ? (

View File

@ -74,7 +74,7 @@ class CardContent extends React.Component<Props, State> {
onOk: () => {
this.onDeleteCluster(item.name);
},
locale: locale.Dialog,
locale: locale().Dialog,
});
}}
>

View File

@ -271,7 +271,7 @@ class CloudServiceDialog extends React.Component<Props, State> {
return (
<React.Fragment>
<Dialog
locale={locale.Dialog}
locale={locale().Dialog}
className="dialog-cluoudService-wraper"
title={<Translation>Connect Kubernetes Cluster From Cloud</Translation>}
autoFocus={true}
@ -298,7 +298,7 @@ class CloudServiceDialog extends React.Component<Props, State> {
<Form {...formItemLayout} field={this.field} className="cloud-server-wraper">
<FormItem label={<Translation>Provider</Translation>} required={true}>
<Select
locale={locale.Select}
locale={locale().Select}
mode="single"
size="large"
dataSource={providerList}
@ -348,7 +348,7 @@ class CloudServiceDialog extends React.Component<Props, State> {
<If condition={!choseInput}>
<Table
locale={locale.Table}
locale={locale().Table}
dataSource={cloudClusters}
hasBorder={false}
loading={tableLoading}
@ -356,7 +356,7 @@ class CloudServiceDialog extends React.Component<Props, State> {
{columns && columns.map((col, key) => <Column {...col} key={key} align={'left'} />)}
</Table>
<Pagination
locale={locale.Pagination}
locale={locale().Pagination}
hideOnlyOnePage={true}
total={total}
size="small"

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Message, Grid, Dialog, Form, Input, Field, Select, Loading } from '@b-design/ui';
import { Message, Grid, Dialog, Form, Input, Field, Select, Loading, Button } from '@b-design/ui';
import { checkName } from '../../../../utils/common';
import type { Target } from '../../../../interface/target';
import Translation from '../../../../components/Translation';
@ -25,6 +25,7 @@ type State = {
targets?: Target[];
namespaces?: { label: string; value: string }[];
targetLoading: boolean;
submitLoading: boolean;
};
class EnvDialog extends React.Component<Props, State> {
@ -41,6 +42,7 @@ class EnvDialog extends React.Component<Props, State> {
});
this.state = {
targetLoading: false,
submitLoading: false,
};
}
@ -84,6 +86,7 @@ class EnvDialog extends React.Component<Props, State> {
if (error) {
return;
}
this.setState({ submitLoading: true });
const { isEdit } = this.props;
const { name, alias, description, targets, namespace, project } = values;
const params = {
@ -97,6 +100,7 @@ class EnvDialog extends React.Component<Props, State> {
if (isEdit) {
updateEnv(params).then((res) => {
this.setState({ submitLoading: false });
if (res) {
Message.success(<Translation>Environment updated successfully</Translation>);
this.props.onOK();
@ -105,6 +109,7 @@ class EnvDialog extends React.Component<Props, State> {
});
} else {
createEnv(params).then((res) => {
this.setState({ submitLoading: false });
if (res) {
Message.success(<Translation>Environment created successfully</Translation>);
this.props.onOK();
@ -161,7 +166,7 @@ class EnvDialog extends React.Component<Props, State> {
};
const { visible, isEdit, projects } = this.props;
const { targetLoading } = this.state;
const { targetLoading, submitLoading } = this.state;
const projectList = (projects || []).map((project) => {
return {
label: project.alias ? `${project.alias}(${project.name})` : project.name,
@ -171,7 +176,7 @@ class EnvDialog extends React.Component<Props, State> {
return (
<div>
<Dialog
locale={locale.Dialog}
locale={locale().Dialog}
className={'commonDialog'}
height="auto"
title={
@ -188,6 +193,13 @@ class EnvDialog extends React.Component<Props, State> {
onCancel={this.onClose}
onClose={this.onClose}
footerActions={['cancel', 'ok']}
footer={
<div>
<Button onClick={this.onOk} type="primary" loading={submitLoading}>
<Translation>Confirm</Translation>
</Button>
</div>
}
footerAlign="center"
>
<Form {...formItemLayout} field={this.field}>
@ -297,7 +309,7 @@ class EnvDialog extends React.Component<Props, State> {
<Loading visible={targetLoading} style={{ width: '100%' }}>
<FormItem label={<Translation>Target</Translation>} required>
<Select
locale={locale.Select}
locale={locale().Select}
className="select"
mode="multiple"
placeholder={i18n.t('Please select a target').toString()}

View File

@ -138,7 +138,7 @@ class TableList extends Component<Props> {
onOk: () => {
this.onDelete(record);
},
locale: locale.Dialog,
locale: locale().Dialog,
});
}}
>
@ -173,7 +173,7 @@ class TableList extends Component<Props> {
return (
<div className="table-delivery-list margin-top-20">
<Table
locale={locale.Table}
locale={locale().Table}
className="customTable"
size="medium"
dataSource={list}

View File

@ -78,7 +78,7 @@ class Namespace extends React.Component<Props, State> {
<If condition={!showNameSpaceInput}>
<div className="cluster-container">
<Select
locale={locale.Select}
locale={locale().Select}
className="cluster-params-input"
mode="single"
dataSource={namespaces}

View File

@ -136,7 +136,7 @@ class targetList extends React.Component<Props, State> {
<Pagination
className="delivery-target-pagenation"
total={envTotal}
locale={locale.Pagination}
locale={locale().Pagination}
size="medium"
pageSize={this.state.pageSize}
current={this.state.page}

View File

@ -249,7 +249,7 @@ class CreateIntegration extends React.Component<Props, State> {
required={true}
>
<Select
locale={locale.Select}
locale={locale().Select}
showSearch
className="select"
placeholder={i18n.t('Please select').toString()}

View File

@ -74,6 +74,10 @@ class Integrations extends Component<Props, State> {
this.setState({
list: res,
});
} else {
this.setState({
list: [],
});
}
})
.finally(() => {
@ -104,7 +108,7 @@ class Integrations extends Component<Props, State> {
});
}
},
locale: locale.Dialog,
locale: locale().Dialog,
});
};
@ -218,7 +222,7 @@ class Integrations extends Component<Props, State> {
</Button>
</Permission>
</div>
<Table locale={locale.Table} dataSource={list} hasBorder={false} loading={isLoading}>
<Table locale={locale().Table} dataSource={list} hasBorder={false} loading={isLoading}>
{columns && columns.map((col, key) => <Column {...col} key={key} align={'left'} />)}
</Table>

View File

@ -2,7 +2,7 @@
display: flex;
align-items: center;
justify-items: center;
height: 100vh;
height: calc(100vh - 48px);
background: #f7f7f7;
.login-wrapper {
width: 400px;
@ -60,3 +60,47 @@
}
}
}
.login-topbar {
width: 100%;
padding: 0 16px;
background-color: #0064c8;
background-image: var(--btn-pure-primary-bg-image);
box-shadow: 5px 0 8px #888;
-webkit-app-region: drag;
.nav-wrapper {
position: relative;
width: 100%;
height: 48px;
.logo {
display: flex;
align-items: center;
margin-left: 9px;
border-radius: 3px;
img {
height: 30px;
}
}
}
.right {
display: flex;
float: right;
height: 48px;
margin-left: auto;
overflow: hidden;
.vela-item {
display: flex;
align-items: center;
justify-items: center;
height: 48px;
padding: 0 8px;
color: #fff;
cursor: pointer;
transition: all 0.3s;
a {
color: #fff;
cursor: pointer;
}
}
}
}

View File

@ -1,10 +1,13 @@
import React, { Component } from 'react';
import React, { Component, Fragment } from 'react';
import { If } from 'tsx-control-statements/components';
import { Card, Button, Input, Form, Field, Icon } from '@b-design/ui';
import { Card, Button, Input, Form, Field, Icon, Grid } from '@b-design/ui';
import { getDexConfig, loginLocal, getLoginType } from '../../api/authentication';
import Translation from '../../components/Translation';
import { checkName, checkUserPassword } from '../../utils/common';
import SwitchLanguage from '../../components/SwitchButton/index';
import Logo from '../../assets/kubevela-logo.png';
import LogoWhite from '../../assets/kubevela-logo-white.png';
import i18n from '../../i18n';
import './index.less';
@ -130,80 +133,102 @@ export default class LoginPage extends Component<Props, State> {
},
};
const { loginType, loginErrorMessage } = this.state;
const { Row, Col } = Grid;
return (
<div className="full">
<div className="login-wrapper">
<If condition={loginType === 'dex'}>
<div />
</If>
<If condition={loginType === 'local'}>
<div className="login-card-wrapper">
<Card contentHeight={'auto'}>
<div className="logo-img-wrapper">
<img src={Logo} />
</div>
<h3 className="login-title-description">
<Translation>Make shipping applications more enjoyable</Translation>
</h3>
<Form onSubmit={this.handleSubmit} {...formItemLayout} field={this.field}>
<FormItem
label={<Translation className="label-title">Username</Translation>}
labelAlign="top"
required
>
<Input
name="username"
placeholder={i18n.t('Please input the username').toString()}
{...init('username', {
rules: [
{
required: true,
pattern: checkName,
message: <Translation>Please input the username</Translation>,
},
],
})}
/>
</FormItem>
<FormItem
label={<Translation className="label-title">Password</Translation>}
labelAlign="top"
required
>
<Input
name="password"
htmlType="password"
placeholder={i18n.t('Please input the password').toString()}
{...init('password', {
rules: [
{
required: true,
pattern: checkUserPassword,
message: (
<Translation>
The password should be 8-16 bits and contain at least one number and
one letter
</Translation>
),
},
],
})}
/>
</FormItem>
</Form>
<If condition={loginErrorMessage}>
<div className="logo-error-wrapper">
<Icon type="warning1" /> <Translation>{loginErrorMessage}</Translation>
</div>
</If>
<Button type="primary" onClick={this.handleSubmit}>
<Translation>Sign in</Translation>
</Button>
</Card>
<Fragment>
<div className="login-topbar">
<Row className="nav-wrapper">
<Col span="4" className="logo">
<img src={LogoWhite} title={'Make shipping applications more enjoyable.'} />
</Col>
<div style={{ flex: '1 1 0%' }} />
<div className="right">
<div className="vela-item">
<a title="KubeVela Documents" href="https://kubevela.io" target="_blank">
<Icon size={14} type="help1" />
</a>
</div>
<div className="vela-item">
<SwitchLanguage />
</div>
</div>
</If>
</Row>
</div>
</div>
<div className="full">
<div className="login-wrapper">
<If condition={loginType === 'dex'}>
<div />
</If>
<If condition={loginType === 'local'}>
<div className="login-card-wrapper">
<Card contentHeight={'auto'}>
<div className="logo-img-wrapper">
<img src={Logo} />
</div>
<h3 className="login-title-description">
<Translation>Make shipping applications more enjoyable</Translation>
</h3>
<Form onSubmit={this.handleSubmit} {...formItemLayout} field={this.field}>
<FormItem
label={<Translation className="label-title">Username</Translation>}
labelAlign="top"
required
>
<Input
name="username"
placeholder={i18n.t('Please input the username').toString()}
{...init('username', {
rules: [
{
required: true,
pattern: checkName,
message: <Translation>Please input the username</Translation>,
},
],
})}
/>
</FormItem>
<FormItem
label={<Translation className="label-title">Password</Translation>}
labelAlign="top"
required
>
<Input
name="password"
htmlType="password"
placeholder={i18n.t('Please input the password').toString()}
{...init('password', {
rules: [
{
required: true,
pattern: checkUserPassword,
message: (
<Translation>
The password should be 8-16 bits and contain at least one number
and one letter
</Translation>
),
},
],
})}
/>
</FormItem>
</Form>
<If condition={loginErrorMessage}>
<div className="logo-error-wrapper">
<Icon type="warning1" /> <Translation>{loginErrorMessage}</Translation>
</div>
</If>
<Button type="primary" onClick={this.handleSubmit}>
<Translation>Sign in</Translation>
</Button>
</Card>
</div>
</If>
</div>
</div>
</Fragment>
);
}
}

View File

@ -111,7 +111,7 @@ class SelectSearch extends React.Component<Props, State> {
<Row wrap={true}>
<Col xl={6} m={8} s={12} xxs={24} style={{ padding: '0 8px' }}>
<Select
locale={locale.Select}
locale={locale().Select}
mode="single"
size="large"
onChange={this.onChangeEnv}
@ -124,7 +124,7 @@ class SelectSearch extends React.Component<Props, State> {
</Col>
<Col xl={6} m={8} s={12} xxs={24} style={{ padding: '0 8px' }}>
<Select
locale={locale.Select}
locale={locale().Select}
mode="single"
size="large"
onChange={this.onChangeTarget}

View File

@ -181,7 +181,7 @@ class MemberDialog extends React.Component<Props, State> {
},
],
})}
locale={locale.Select}
locale={locale().Select}
mode="tag"
dataSource={rolesList}
/>

View File

@ -133,7 +133,7 @@ class ProjectMembers extends Component<Props, State> {
.catch();
}
},
locale: locale.Dialog,
locale: locale().Dialog,
});
};
@ -283,7 +283,7 @@ class ProjectMembers extends Component<Props, State> {
<section className="margin-top-20 member-list-wrapper">
<Table
locale={locale.Table}
locale={locale().Table}
dataSource={memberList}
hasBorder={false}
loading={isLoading}
@ -294,7 +294,7 @@ class ProjectMembers extends Component<Props, State> {
<Pagination
className="margin-top-20 text-align-right"
total={total}
locale={locale.Pagination}
locale={locale().Pagination}
hideOnlyOnePage={true}
size="medium"
pageSize={pageSize}

View File

@ -149,7 +149,7 @@ class ProjectRoles extends Component<Props, State> {
});
}
},
locale: locale.Dialog,
locale: locale().Dialog,
});
};

View File

@ -68,7 +68,7 @@ class General extends Component<Props, State> {
return (
<Fragment>
<div className="general-wrapper">
<Card locale={locale.Card} contentHeight="auto" className="card-wrapper">
<Card locale={locale().Card} contentHeight="auto" className="card-wrapper">
<section className="card-title-wrapper">
<span className="card-title">
<Translation>General</Translation>

View File

@ -122,7 +122,7 @@ class Integrations extends Component<Props, State> {
</section>
<section className="card-content-table">
<Table
locale={locale.Table}
locale={locale().Table}
dataSource={configList}
hasBorder={true}
loading={isLoading}

View File

@ -104,7 +104,7 @@ class Targets extends Component<Props, State> {
</Permission>
</section>
<section className="card-content-table">
<Table locale={locale.Table} dataSource={list} hasBorder={true} loading={isLoading}>
<Table locale={locale().Table} dataSource={list} hasBorder={true} loading={isLoading}>
{columns && columns.map((col, key) => <Column {...col} key={key} align={'left'} />)}
</Table>
</section>

View File

@ -104,7 +104,7 @@ class Projects extends Component<Props, State> {
.catch();
}
},
locale: locale.Dialog,
locale: locale().Dialog,
});
};
@ -253,14 +253,14 @@ class Projects extends Component<Props, State> {
</Permission>,
]}
/>
<Table locale={locale.Table} dataSource={list} hasBorder={false} loading={isLoading}>
<Table locale={locale().Table} dataSource={list} hasBorder={false} loading={isLoading}>
{columns && columns.map((col, key) => <Column {...col} key={key} align={'left'} />)}
</Table>
<Pagination
className="margin-top-20 text-align-right"
total={total}
locale={locale.Pagination}
locale={locale().Pagination}
size="medium"
pageSize={pageSize}
current={page}

View File

@ -197,7 +197,7 @@ class RolesDialog extends React.Component<Props, State> {
},
],
})}
locale={locale.Select}
locale={locale().Select}
mode="tag"
dataSource={permPoliciesList}
/>

View File

@ -93,7 +93,7 @@ class Roles extends Component<Props, State> {
});
}
},
locale: locale.Dialog,
locale: locale().Dialog,
});
};
@ -231,14 +231,14 @@ class Roles extends Component<Props, State> {
</Permission>,
]}
/>
<Table locale={locale.Table} dataSource={list} hasBorder={false} loading={isLoading}>
<Table locale={locale().Table} dataSource={list} hasBorder={false} loading={isLoading}>
{columns && columns.map((col, key) => <Column {...col} key={key} align={'left'} />)}
</Table>
<Pagination
className="margin-top-20 text-align-right"
total={total}
locale={locale.Pagination}
locale={locale().Pagination}
hideOnlyOnePage={true}
size="medium"
pageSize={pageSize}

View File

@ -103,7 +103,7 @@ class TableList extends Component<Props> {
onOk: () => {
this.onDelete(record);
},
locale: locale.Dialog,
locale: locale().Dialog,
});
}}
>
@ -137,7 +137,7 @@ class TableList extends Component<Props> {
return (
<div className="table-delivery-list margin-top-20">
<Table
locale={locale.Table}
locale={locale().Table}
className="customTable"
size="medium"
dataSource={list}

View File

@ -100,7 +100,7 @@ class Namespace extends React.Component<Props, State> {
<div>
<div className="cluster-container">
<Select
locale={locale.Select}
locale={locale().Select}
className="cluster-params-input"
mode="single"
disabled={disabled}
@ -122,7 +122,7 @@ class Namespace extends React.Component<Props, State> {
</div>
<Dialog
locale={locale.Dialog}
locale={locale().Dialog}
className={'namespaceDialogWraper'}
title={<Translation>Create Namespace</Translation>}
autoFocus={true}

View File

@ -241,7 +241,7 @@ class DeliveryDialog extends React.Component<Props, State> {
return (
<div>
<Dialog
locale={locale.Dialog}
locale={locale().Dialog}
className={'commonDialog'}
height="auto"
title={
@ -346,7 +346,7 @@ class DeliveryDialog extends React.Component<Props, State> {
<Col span={12} style={{ padding: '0 8px' }}>
<FormItem label={<Translation>Cluster</Translation>} required>
<Select
locale={locale.Select}
locale={locale().Select}
className="select"
disabled={isEdit}
placeholder={t('Please select').toString()}

View File

@ -147,7 +147,7 @@ class targetList extends React.Component<Props, State> {
<Pagination
className="delivery-target-pagenation"
total={total}
locale={locale.Pagination}
locale={locale().Pagination}
size="medium"
pageSize={this.state.pageSize}
current={this.state.page}

View File

@ -253,7 +253,7 @@ class CreateUser extends React.Component<Props, State> {
},
],
})}
locale={locale.Select}
locale={locale().Select}
mode="multiple"
dataSource={rolesListSelect}
/>

View File

@ -1,6 +1,7 @@
import React from 'react';
import { Grid, Icon, Input } from '@b-design/ui';
import i18n from '../../../../i18n';
import { withTranslation } from 'react-i18next';
import './index.less';
const { Row, Col } = Grid;
@ -74,4 +75,4 @@ class SelectSearch extends React.Component<Props, State> {
}
}
export default SelectSearch;
export default withTranslation()(SelectSearch);

View File

@ -193,7 +193,7 @@ class Users extends Component<Props, State> {
.catch();
}
},
locale: locale.Dialog,
locale: locale().Dialog,
});
};
@ -445,7 +445,7 @@ class Users extends Component<Props, State> {
<section className="margin-top-20 user-list-wrapper">
<Table
locale={locale.Table}
locale={locale().Table}
dataSource={dataSource}
hasBorder={false}
loading={isLoading}
@ -456,7 +456,7 @@ class Users extends Component<Props, State> {
<Pagination
className="margin-top-20 text-align-right"
total={total}
locale={locale.Pagination}
locale={locale().Pagination}
hideOnlyOnePage={true}
size="medium"
pageSize={pageSize}

View File

@ -18,7 +18,7 @@ export function isMock() {
export function getDomain(): { MOCK: string | undefined; APIBASE: string | undefined } {
const { MOCK, BASE_DOMAIN } = process.env;
return {
MOCK: MOCK,
MOCK: MOCK || '',
APIBASE: BASE_DOMAIN || '',
};
}

View File

@ -1,83 +1,189 @@
export default {
Timeline: {
expand: 'Expand',
fold: 'Fold',
},
Balloon: {
close: 'Close',
},
Card: {
expand: 'Expand',
fold: 'Fold',
},
Dialog: {
close: 'Close',
ok: 'Confirm',
cancel: 'Cancel',
},
Drawer: {
close: 'Close',
},
Message: {
closeAriaLabel: 'Close',
},
Pagination: {
prev: 'Prev',
next: 'Next',
goTo: 'Go To',
page: 'Page',
go: 'Go',
total: 'Page {current} of {total} pages.',
labelPrev: 'Prev page, current page {current}',
labelNext: 'Next page, current page {current}',
inputAriaLabel: 'Please enter the page to jump to',
selectAriaLabel: 'Please select page size',
pageSize: 'Page Size:',
},
Input: {
clear: 'Clear',
},
List: {
empty: 'No Data',
},
Select: {
selectPlaceholder: 'Please select',
autoCompletePlaceholder: 'Please enter',
notFoundContent: 'No Options',
maxTagPlaceholder: '{selected}/{total} items have been selected.',
selectAll: 'Select All',
},
Table: {
empty: 'No Data',
ok: 'Confirm',
reset: 'Reset',
asc: 'Asc',
desc: 'Desc',
expanded: 'Expanded',
folded: 'Folded',
filter: 'Filter',
selectAll: 'Select All',
},
Upload: {
card: {
import { getLanguage } from '../utils/common';
const localeData = {
en: {
Timeline: {
expand: 'Expand',
fold: 'Fold',
},
Balloon: {
close: 'Close',
},
Card: {
expand: 'Expand',
fold: 'Fold',
},
Dialog: {
close: 'Close',
ok: 'Confirm',
cancel: 'Cancel',
},
Drawer: {
close: 'Close',
},
Message: {
closeAriaLabel: 'Close',
},
Pagination: {
prev: 'Prev',
next: 'Next',
goTo: 'Go To',
page: 'Page',
go: 'Go',
total: 'Page {current} of {total} pages.',
labelPrev: 'Prev page, current page {current}',
labelNext: 'Next page, current page {current}',
inputAriaLabel: 'Please enter the page to jump to',
selectAriaLabel: 'Please select page size',
pageSize: 'Page Size:',
},
Input: {
clear: 'Clear',
},
List: {
empty: 'No Data',
},
Select: {
selectPlaceholder: 'Please select',
autoCompletePlaceholder: 'Please enter',
notFoundContent: 'No Options',
maxTagPlaceholder: '{selected}/{total} items have been selected.',
selectAll: 'Select All',
},
Table: {
empty: 'No Data',
ok: 'Confirm',
reset: 'Reset',
asc: 'Asc',
desc: 'Desc',
expanded: 'Expanded',
folded: 'Folded',
filter: 'Filter',
selectAll: 'Select All',
},
Upload: {
card: {
cancel: 'Cancel',
delete: 'Delete',
},
upload: {
delete: 'Delete',
},
},
Search: {
buttonText: 'Search',
},
Tag: {
delete: 'Delete',
},
upload: {
delete: 'Delete',
Switch: {
on: 'On',
off: 'Off',
},
Tab: {
closeAriaLabel: 'Close',
},
},
Search: {
buttonText: 'Search',
},
Tag: {
delete: 'Delete',
},
Switch: {
on: 'On',
off: 'Off',
},
Tab: {
closeAriaLabel: 'Close',
zh: {
Timeline: {
expand: '展开',
fold: '收起',
},
Balloon: {
close: '关闭',
},
Card: {
expand: '展开',
fold: '收起',
},
Dialog: {
close: '关闭',
ok: '确认',
cancel: '取消',
},
Drawer: {
close: '关闭',
},
Message: {
closeAriaLabel: '关闭标签',
},
Pagination: {
prev: '前一页',
next: '下一页',
goTo: '去往',
page: '分页',
go: '去',
total: 'Page {current} of {total} pages.',
labelPrev: '前一页, 当前页 {current}',
labelNext: '下一页, 当前页 {current}',
inputAriaLabel: '请输入要跳转到的页面',
selectAriaLabel: '请选择页面展示的数量',
pageSize: '每页显示多少条:',
},
Input: {
clear: '清空',
},
List: {
empty: '没有数据',
},
Select: {
selectPlaceholder: '请选择',
autoCompletePlaceholder: '请输入',
notFoundContent: '没有下拉项',
maxTagPlaceholder: '{selected}/{total} 条目已选择.',
selectAll: '全选',
},
Table: {
empty: '没有数据',
ok: '确认',
reset: '重置',
asc: '生序',
desc: '降序',
expanded: '展开',
folded: '收起',
filter: '过滤',
selectAll: '全选',
},
Upload: {
card: {
cancel: '取消',
delete: '删除',
},
upload: {
delete: '删除',
},
},
Search: {
buttonText: '搜索',
},
Tag: {
delete: '删除',
},
Switch: {
on: '打开',
off: '关闭',
},
Tab: {
closeAriaLabel: '关闭',
},
},
};
class SingletonLocal {
private constructor() {}
private static instance: SingletonLocal | null = null;
public static getInstance(): SingletonLocal {
this.instance = this.instance || new SingletonLocal();
return this.instance;
}
private local: any;
public setLocal(value: any) {
this.local = value;
}
public getLocal() {
return () => {
const language = getLanguage();
return this.local[language] ?? this.local.en;
};
}
}
SingletonLocal.getInstance().setLocal(localeData);
export default SingletonLocal.getInstance().getLocal();

View File

@ -1,5 +1,7 @@
import type { Endpoint } from '../interface/observation';
import type { ComponentDefinitionsBase } from '../interface/application';
import type { LoginUserInfo } from '../interface/user';
import _ from 'lodash';
type SelectGroupType = {
label: string;
@ -142,3 +144,60 @@ export function getSelectLabel(
export function getMatchParamObj(match: { params: any }, name: string) {
return match.params && match.params[name];
}
export function isAdminUserCheck(userInfo: LoginUserInfo | undefined) {
const platformPermissions = userInfo?.platformPermissions || [];
const findAdminUser = _.find(platformPermissions, (item) => {
return item.name === 'admin';
});
if (findAdminUser) {
return true;
} else {
return false;
}
}
/**
* Get browser name agent version
* return browser name version
* */
export function getBrowserNameAndVersion() {
const agent = navigator.userAgent.toLowerCase();
const regStr_ie = /msie [\d.]+/gi;
const regStr_ff = /firefox\/[\d.]+/gi;
const regStr_chrome = /chrome\/[\d.]+/gi;
const regStr_saf = /safari\/[\d.]+/gi;
let browserNV: any;
//IE
if (agent.indexOf('msie') > 0) {
browserNV = agent.match(regStr_ie);
}
//firefox
if (agent.indexOf('firefox') > 0) {
browserNV = agent.match(regStr_ff);
}
//Chrome
if (agent.indexOf('chrome') > 0) {
browserNV = agent.match(regStr_chrome);
}
//Safari
if (agent.indexOf('safari') > 0 && agent.indexOf('chrome') < 0) {
browserNV = agent.match(regStr_saf);
}
browserNV = browserNV.toString();
//other
if ('' == browserNV) {
browserNV = 'Is not a standard browser';
}
//Here does not display "/"
if (browserNV.indexOf('firefox') != -1 || browserNV.indexOf('chrome') != -1) {
browserNV = browserNV.replace('/', '');
}
//Here does not display space
if (browserNV.indexOf('msie') != -1) {
//msie replace IE & trim space
browserNV = browserNV.replace('msie', 'ie').replace(/\s/g, '');
}
//return eg:ie9.0 firefox34.0 chrome37.0
return browserNV;
}