mirror of https://github.com/sunface/rust-course
parent
a74fbe8581
commit
2701a556b0
@ -0,0 +1,8 @@
|
|||||||
|
# create-react-app does a check on several packages to make sure the
|
||||||
|
# versions required by CRA are the package versions available:
|
||||||
|
# https://github.com/facebook/create-react-app/blob/dea19fdb30c2e896ed8ac75b68a612b0b92b2406/packages/react-scripts/scripts/utils/verifyPackageTree.js#L23-L29
|
||||||
|
# The repo is set up to use yarn workspaces and keeps all
|
||||||
|
# packages/jaeger-ui packages local to packages/jaeger-ui. But, the
|
||||||
|
# check CRA does not detect this. So, the following env-var skips the
|
||||||
|
# check.
|
||||||
|
SKIP_PREFLIGHT_CHECK=true
|
@ -1,4 +0,0 @@
|
|||||||
ENV = 'development'
|
|
||||||
|
|
||||||
# base api
|
|
||||||
REACT_APP_ADMIN_BASE_API = '/dev-api'
|
|
@ -1,4 +0,0 @@
|
|||||||
ENV = 'production'
|
|
||||||
|
|
||||||
# base api
|
|
||||||
REACT_APP_ADMIN_BASE_API = '/pro-api'
|
|
@ -0,0 +1,2 @@
|
|||||||
|
build
|
||||||
|
coverage
|
@ -1,23 +1,23 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
node_modules
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
build
|
||||||
|
|
||||||
|
# reports
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env.local
|
npm-debug.log
|
||||||
.env.development.local
|
.vscode
|
||||||
.env.test.local
|
.idea
|
||||||
.env.production.local
|
yarn-error.log
|
||||||
|
lerna-debug.log
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
npm-debug.log*
|
src/styles/light.json
|
||||||
yarn-debug.log*
|
src/styles/dark.json
|
||||||
yarn-error.log*
|
|
@ -1,60 +1,57 @@
|
|||||||
const {
|
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
override,
|
//
|
||||||
addDecoratorsLegacy,
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
fixBabelImports,
|
// you may not use this file except in compliance with the License.
|
||||||
addLessLoader,
|
// You may obtain a copy of the License at
|
||||||
addWebpackAlias
|
//
|
||||||
// addWebpackPlugin
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
} = require('customize-cra')
|
//
|
||||||
const themeVariables = require('./src/styles/theme/themeVariables')
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
const AntDesignThemePlugin = require('antd-theme-webpack-plugin')
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
const fs = require('fs');
|
||||||
|
const { fixBabelImports, addLessLoader, override ,addWebpackPlugin} = require('customize-cra');
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
const { getLessVars } = require('antd-theme-generator')
|
||||||
|
const AntDesignThemePlugin = require('antd-theme-webpack-plugin')
|
||||||
|
|
||||||
|
const themeVariables = getLessVars(path.join(__dirname, './src/styles/vars.less'))
|
||||||
|
const defaultVars = getLessVars('./node_modules/antd/lib/style/themes/default.less')
|
||||||
|
const darkVars = { ...getLessVars('./node_modules/antd/lib/style/themes/dark.less'), '@primary-color': defaultVars['@primary-color'] };
|
||||||
|
const lightVars = { ...getLessVars('./node_modules/antd/lib/style/themes/compact.less'), '@primary-color': defaultVars['@primary-color'] };
|
||||||
|
|
||||||
|
|
||||||
|
const newDarkVars = {...darkVars, ...themeVariables}
|
||||||
|
const newLightVars = {...lightVars, ...themeVariables}
|
||||||
|
fs.writeFileSync('./src/styles/dark.json', JSON.stringify(newDarkVars));
|
||||||
|
fs.writeFileSync('./src/styles/light.json', JSON.stringify(newLightVars));
|
||||||
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
antDir: path.join(__dirname, './node_modules/antd'), // antd包位置
|
antDir: path.join(__dirname, './node_modules/antd'), // antd包位置
|
||||||
stylesDir: path.join(__dirname, './src/styles'), //主题文件所在文件夹
|
stylesDir: path.join(__dirname, './src'), //主题文件所在文件夹
|
||||||
varFile: path.join(__dirname, './src/styles/vars.less'), // 自定义默认的主题色
|
varFile: path.join(__dirname, './src/styles/vars.less'), // 自定义默认的主题色
|
||||||
mainLessFile: path.join(__dirname, './src/styles/main.less'), // 项目中其他自定义的样式(如果不需要动态修改其他样式,该文件可以为空)
|
themeVariables: Array.from(new Set([
|
||||||
outputFilePath: path.join(__dirname, './public/color.less'), //提取的less文件输出到什么地方
|
...Object.keys(darkVars),
|
||||||
themeVariables: themeVariables, //要改变的主题变量
|
...Object.keys(lightVars),
|
||||||
indexFileName: './public/index.html', // index.html所在位置
|
...Object.keys(themeVariables),
|
||||||
generateOnce: false // 是否只生成一次
|
])),
|
||||||
}
|
generateOnce: false // 是否只生成一次,
|
||||||
const addTheme = () => (config) => {
|
|
||||||
config.plugins.push(new AntDesignThemePlugin(options))
|
|
||||||
return config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = override(
|
module.exports = override(
|
||||||
// 使用 Day.js 替换 momentjs 优化打包大小
|
fixBabelImports('import', {
|
||||||
// addWebpackPlugin(
|
libraryName: 'antd',
|
||||||
|
libraryDirectory: 'es',
|
||||||
// ),
|
style: true
|
||||||
// 装饰器语法
|
}),
|
||||||
addDecoratorsLegacy(),
|
// 主题
|
||||||
// 自动加载antd
|
addWebpackPlugin(new AntDesignThemePlugin(options)),
|
||||||
fixBabelImports('import', {
|
addLessLoader({
|
||||||
libraryName: 'antd',
|
javascriptEnabled: true,
|
||||||
libraryDirectory: 'es',
|
})
|
||||||
style: true
|
|
||||||
}),
|
|
||||||
// less
|
|
||||||
addLessLoader({
|
|
||||||
javascriptEnabled: true,
|
|
||||||
localIdentName: '[local]--[hash:base64:5]'
|
|
||||||
}),
|
|
||||||
// 路径别名
|
|
||||||
addWebpackAlias({
|
|
||||||
'@': path.resolve(__dirname, 'src'),
|
|
||||||
'@library': path.resolve(__dirname, 'src/library'),
|
|
||||||
'@layouts': path.resolve(__dirname, 'src/layouts'),
|
|
||||||
'@utils': path.resolve(__dirname, 'src/library/utils'),
|
|
||||||
'@pages': path.resolve(__dirname, 'src/pages'),
|
|
||||||
'@store': path.resolve(__dirname, 'src/store'),
|
|
||||||
'@styles': path.resolve(__dirname, 'src/styles'),
|
|
||||||
'@components': path.resolve(__dirname, 'src/components'),
|
|
||||||
'@http': path.resolve(__dirname, 'src/library/utils/http')
|
|
||||||
}),
|
|
||||||
// 主题
|
|
||||||
addTheme()
|
|
||||||
)
|
)
|
@ -1,31 +0,0 @@
|
|||||||
[react 文档](https://zh-hans.reactjs.org/)
|
|
||||||
|
|
||||||
[Eslint 参考资料](https://blog.csdn.net/walid1992/article/details/54633760)
|
|
||||||
|
|
||||||
[mock 文档](http://mockjs.com/)
|
|
||||||
|
|
||||||
[json-server 参考资料](https://blog.csdn.net/div_ma/article/details/80579971)
|
|
||||||
|
|
||||||
[mockjs 参考资料](https://blog.csdn.net/div_ma/article/details/80592996)
|
|
||||||
|
|
||||||
[mock json-server 虚拟数据服务搭建](https://blog.csdn.net/qq_41629150/article/details/99645632)
|
|
||||||
|
|
||||||
## 安装依赖
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
|
|
||||||
npm install json-server -g
|
|
||||||
|
|
||||||
yarn global add json-server
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用 concurrently 并行地运行多个命令
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
|
|
||||||
npm i concurrently --save
|
|
||||||
|
|
||||||
yarn add concurrently
|
|
||||||
|
|
||||||
```
|
|
@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
HOST: 'localhost', // 定义ip地址
|
|
||||||
PORT: '3023', // 定义端口号
|
|
||||||
DB_FILE: './db.js', // 定义批量模拟数据文件
|
|
||||||
API: '/api'//创建根api名 这里的 /mock 如同 后端真实/api
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
let Mock = require('mockjs')
|
|
||||||
let Random = Mock.Random
|
|
||||||
module.exports = function() {
|
|
||||||
let data = {
|
|
||||||
news: []
|
|
||||||
}
|
|
||||||
|
|
||||||
let images = [1, 2, 3].map( x =>Random.image('200x100', Random.color(), Random.word(2, 6)))
|
|
||||||
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
|
|
||||||
let content = Random.cparagraph(0, 10)
|
|
||||||
|
|
||||||
data.news.push({
|
|
||||||
id: i,
|
|
||||||
title: Random.cword(8, 20),
|
|
||||||
desc: content.substr(0, 40),
|
|
||||||
tag: Random.cword(2, 6),
|
|
||||||
views: Random.integer(100, 5000),
|
|
||||||
images: images.slice(0, Random.integer(1, 3))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
Before Width: | Height: | Size: 318 B |
@ -1,114 +0,0 @@
|
|||||||
body {
|
|
||||||
display: flex;
|
|
||||||
min-height: 100vh;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
|
||||||
Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
color: #3b4252;
|
|
||||||
letter-spacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 960px;
|
|
||||||
margin: auto;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
header a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
header a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav ul {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav li.title {
|
|
||||||
flex-grow: 5;
|
|
||||||
text-align: left;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
color: #3b4252;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav li {
|
|
||||||
flex-grow: 1;
|
|
||||||
align-self: center;
|
|
||||||
text-align: right;
|
|
||||||
color: #4c566a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-heart {
|
|
||||||
color: deeppink;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
margin-top: 4rem;
|
|
||||||
border-top: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin-top: 4rem;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
i {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #5e81ac;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #81a1c1;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
border: 0;
|
|
||||||
padding: 0 1em 0.5em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
td:first-child {
|
|
||||||
width: 1%;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style-position: inside;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
list-style-type: none;
|
|
||||||
margin-bottom: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
padding: 0.2rem;
|
|
||||||
margin: 0rem 0.2rem;
|
|
||||||
border-radius: 0.2rem;
|
|
||||||
background: #e5e9f0;
|
|
||||||
}
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
@ -1,4 +0,0 @@
|
|||||||
const API = '/api'
|
|
||||||
module.exports = {
|
|
||||||
[`${API}` + '/*'] : '/$1' // 路由请求由/mock/*接管
|
|
||||||
}
|
|
@ -1,74 +1,162 @@
|
|||||||
{
|
{
|
||||||
"name": "imdev-ui",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"name": "apm-ui",
|
||||||
|
"version": "1.9.0",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"homepage": ".",
|
||||||
|
"workspaces": {
|
||||||
|
"nohoist": [
|
||||||
|
"customize-cra",
|
||||||
|
"customize-cra/**",
|
||||||
|
"react-scripts",
|
||||||
|
"react-scripts/**",
|
||||||
|
"react-app-rewired",
|
||||||
|
"react-app-rewired/**"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/match-sorter": "^2.3.0",
|
||||||
|
"@types/react-window": "^1.8.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "1.12.0",
|
||||||
|
"@typescript-eslint/parser": "1.12.0",
|
||||||
|
"@typescript-eslint/typescript-estree": "1.12.0",
|
||||||
|
"babel-eslint": "10.0.1",
|
||||||
|
"enzyme": "^3.8.0",
|
||||||
|
"enzyme-adapter-react-16": "^1.2.0",
|
||||||
|
"enzyme-to-json": "^3.3.0",
|
||||||
|
"eslint": "5.14.1",
|
||||||
|
"eslint-config-airbnb": "17.1.0",
|
||||||
|
"eslint-config-prettier": "4.0.0",
|
||||||
|
"eslint-config-react-app": "3.0.7",
|
||||||
|
"eslint-plugin-flowtype": "^3.4.2",
|
||||||
|
"eslint-plugin-import": "2.16.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "6.2.1",
|
||||||
|
"eslint-plugin-react": "7.12.4",
|
||||||
|
"http-proxy-middleware": "^0.19.1",
|
||||||
|
"husky": "1.3.1",
|
||||||
|
"jsdom": "13.2.0",
|
||||||
|
"npm-run-all": "4.1.5",
|
||||||
|
"prettier": "1.18.2",
|
||||||
|
"react-test-renderer": "^15.6.1",
|
||||||
|
"rxjs-compat": "6.4.0",
|
||||||
|
"sinon": "7.3.2",
|
||||||
|
"source-map-explorer": "^1.6.0"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/dark-theme": "^0.2.2",
|
"@ant-design/icons": "^4.1.0",
|
||||||
"@antv/data-set": "^0.10.2",
|
"@types/classnames": "^2.2.7",
|
||||||
"@antv/g2plot": "^0.11.5",
|
"@types/deep-freeze": "^0.1.1",
|
||||||
"@types/jest": "^24.0.25",
|
"@types/history": "^4.7.2",
|
||||||
"@types/js-cookie": "^2.2.4",
|
"@types/js-cookie": "^2.2.6",
|
||||||
"@types/node": "^13.1.7",
|
"@types/lodash": "^4.14.123",
|
||||||
"@types/qs": "^6.9.0",
|
"@types/memoize-one": "4.1.1",
|
||||||
"@types/react": "^16.9.17",
|
"@types/moment": "^2.13.0",
|
||||||
"@types/react-color": "^3.0.1",
|
"@types/object-hash": "^1.3.0",
|
||||||
"@types/react-dom": "^16.9.4",
|
"@types/react-copy-to-clipboard": "^4.2.6",
|
||||||
"@types/react-responsive": "^8.0.2",
|
"@types/react-grid-layout": "^0.17.1",
|
||||||
"@types/react-router-dom": "^5.1.2",
|
"@types/react-icons": "2.2.7",
|
||||||
"antd": "3.26.6",
|
"@types/react-redux": "^5.0.6",
|
||||||
"antd-dayjs-webpack-plugin": "^0.0.7",
|
"@types/react-router-dom": "^5.1.4",
|
||||||
"axios": "^0.19.0",
|
"@types/react-virtualized-select": "^3.0.7",
|
||||||
"babel-plugin-import": "^1.12.2",
|
"@types/recompose": "^0.30.5",
|
||||||
"bizcharts": "^3.5.6",
|
"@types/redux-actions": "2.2.1",
|
||||||
|
"antd": "^4.3.0",
|
||||||
|
"antd-theme-generator": "^1.2.3",
|
||||||
|
"antd-theme-webpack-plugin": "^1.3.4",
|
||||||
|
"apm-plexus": "^0.2.0",
|
||||||
|
"axios": "^0.19.2",
|
||||||
|
"babel-plugin-import": "1.13.0",
|
||||||
|
"classnames": "^2.2.5",
|
||||||
|
"combokeys": "^3.0.0",
|
||||||
|
"copy-to-clipboard": "^3.1.0",
|
||||||
"customize-cra": "^0.9.1",
|
"customize-cra": "^0.9.1",
|
||||||
|
"deep-freeze": "^0.0.1",
|
||||||
|
"drange": "^2.0.0",
|
||||||
|
"fuzzy": "^0.1.3",
|
||||||
|
"global": "^4.3.2",
|
||||||
|
"history": "^4.6.3",
|
||||||
|
"is-promise": "^2.1.0",
|
||||||
|
"isomorphic-fetch": "^2.2.1",
|
||||||
"js-cookie": "^2.2.1",
|
"js-cookie": "^2.2.1",
|
||||||
"less": "^3.10.3",
|
"json-markup": "^1.1.0",
|
||||||
|
"less": "^3.11.1",
|
||||||
"less-loader": "^5.0.0",
|
"less-loader": "^5.0.0",
|
||||||
"mobx": "^5.15.0",
|
"less-vars-to-js": "^1.2.1",
|
||||||
"mobx-react": "^6.1.4",
|
"lodash": "^4.17.15",
|
||||||
"react": "^16.12.0",
|
"lru-memoize": "^1.1.0",
|
||||||
|
"match-sorter": "^3.1.1",
|
||||||
|
"memoize-one": "^5.0.0",
|
||||||
|
"mobx": "^5.15.4",
|
||||||
|
"mobx-react": "^6.2.2",
|
||||||
|
"moment": "^2.18.1",
|
||||||
|
"prop-types": "^15.5.10",
|
||||||
|
"query-string": "^6.3.0",
|
||||||
|
"raven-js": "^3.22.1",
|
||||||
|
"react": "^16.3.2",
|
||||||
"react-app-rewired": "^2.1.5",
|
"react-app-rewired": "^2.1.5",
|
||||||
"react-color": "^2.17.3",
|
"react-circular-progressbar": "^2.0.3",
|
||||||
"react-dom": "^16.12.0",
|
"react-color": "^2.14.1",
|
||||||
|
"react-dimensions": "^1.3.0",
|
||||||
|
"react-dom": "^16.13.1",
|
||||||
|
"react-ga": "^2.4.1",
|
||||||
|
"react-grid-layout": "^0.18.3",
|
||||||
|
"react-helmet": "^5.1.3",
|
||||||
|
"react-icons": "2.2.7",
|
||||||
"react-intl": "^3.9.3",
|
"react-intl": "^3.9.3",
|
||||||
"react-loadable": "^5.5.0",
|
"react-redux": "^5.0.6",
|
||||||
"react-responsive": "^8.0.1",
|
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-scripts": "3.2.0",
|
"react-router-redux": "5.0.0-alpha.6",
|
||||||
"typescript": "^3.7.4"
|
"react-scripts": "3.4.1",
|
||||||
|
"react-virtualized-select": "^3.1.0",
|
||||||
|
"react-vis": "^1.7.2",
|
||||||
|
"react-vis-force": "^0.3.1",
|
||||||
|
"react-window": "^1.8.3",
|
||||||
|
"recompose": "^0.25.0",
|
||||||
|
"redux": "^3.7.2",
|
||||||
|
"redux-actions": "^2.2.1",
|
||||||
|
"redux-form": "^7.0.3",
|
||||||
|
"redux-promise-middleware": "^4.3.0",
|
||||||
|
"reselect": "^3.0.1",
|
||||||
|
"store": "^2.0.12",
|
||||||
|
"ts-key-enum": "^2.0.0",
|
||||||
|
"tween-functions": "^1.2.0",
|
||||||
|
"typescript": "3.5.3",
|
||||||
|
"u-basscss": "2.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"analyze": "source-map-explorer build/static/js/main.*",
|
||||||
|
"build": "REACT_APP_VSN_STATE=$(../../scripts/get-tracking-version.js) react-app-rewired build",
|
||||||
|
"coverage": "yarn run test --coverage",
|
||||||
|
"start:ga-debug": "REACT_APP_GA_DEBUG=1 REACT_APP_VSN_STATE=$(../../scripts/get-tracking-version.js) react-app-rewired start",
|
||||||
"start": "react-app-rewired start",
|
"start": "react-app-rewired start",
|
||||||
"build": "react-app-rewired build",
|
"test-dev": "react-app-rewired test --env=jsdom",
|
||||||
"test": "react-app-rewired test",
|
"test": "CI=1 react-app-rewired test --env=jsdom --color",
|
||||||
"sm": "concurrently \"yarn start\" \"yarn run mock\" ",
|
"tsc-lint": "tsc --build",
|
||||||
"mock": "cd mocks && node ./server.js"
|
"tsc-lint-debug": "tsc --listFiles",
|
||||||
|
"prettier": "prettier --write '{.,scripts}/*.{js,json,md,ts,tsx}' 'packages/*/{src,demo/src}/**/!(layout.worker.bundled|react-vis).{css,js,json,md,ts,tsx}' 'packages/*/*.{css,js,json,md,ts,tsx}'",
|
||||||
|
"prettier-lint": "prettier --list-different '{.,scripts}/*.{js,json,md,ts,tsx}' 'packages/*/{src,demo/src}/**/!(layout.worker.bundled|react-vis).{css,js,json,md,ts,tsx}' 'packages/*/*.{css,js,json,md,ts,tsx}'",
|
||||||
|
"eslint": "eslint --cache 'scripts/*.{js,ts,tsx}' 'packages/*/src/**/*.{js,ts,tsx}' 'packages/*/*.{js,ts,tsx}'",
|
||||||
|
"lint": "npm-run-all -ln --parallel prettier-lint tsc-lint eslint check-license"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"prettier": {
|
||||||
"extends": "react-app"
|
"printWidth": 110,
|
||||||
|
"proseWrap": "never",
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"jest": {
|
||||||
"production": [
|
"collectCoverageFrom": [
|
||||||
">0.2%",
|
"!src/setup*.js",
|
||||||
"not dead",
|
"!src/utils/DraggableManager/demo/*.tsx",
|
||||||
"not op_mini all"
|
"!src/utils/test/**/*.js",
|
||||||
],
|
"!src/demo/**/*.js"
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"browserslist": [
|
||||||
"@babel/plugin-proposal-decorators": "^7.7.4",
|
">0.5%",
|
||||||
"antd-theme-webpack-plugin": "^1.3.0",
|
"not dead",
|
||||||
"babel-eslint": "^10.0.3",
|
"not ie <= 11",
|
||||||
"concurrently": "^5.0.0",
|
"not op_mini all"
|
||||||
"eslint": "^6.7.2",
|
]
|
||||||
"eslint-plugin-react": "^7.14.3",
|
|
||||||
"eslint-plugin-react-hooks": "^2.0.1",
|
|
||||||
"json-server": "^0.15.1",
|
|
||||||
"mockjs": "^1.0.1-beta3"
|
|
||||||
},
|
|
||||||
"proxy": "http://localhost:3023"
|
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 1.4 KiB |
@ -1,51 +1,43 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8">
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="description" content="Web site created using create-react-app"/>
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<link rel="apple-touch-icon" href="logo192.png" />
|
|
||||||
|
|
||||||
<title>React App</title>
|
<!-- prevent caching of this HTML by any server, Go or otherwise -->
|
||||||
|
<meta http-equiv="cache-control" content="max-age=0" />
|
||||||
|
<meta http-equiv="cache-control" content="no-cache" />
|
||||||
|
<meta http-equiv="expires" content="0" />
|
||||||
|
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
|
||||||
|
<meta http-equiv="pragma" content="no-cache" />
|
||||||
|
|
||||||
|
<!-- NOTE: The document MUST have a <base> element. package.json#homepage is set to "." as part of resolving https://github.com/jaegertracing/jaeger-ui/issues/42 and therefore static assets are linked via relative URLs. This will break on many document URLs, e.g. /trace/abc, unless a valid base URL is provided. The base href defaults to "/" but the query-service can inject an override. -->
|
||||||
|
<base href="/" data-inject-target="BASE_URL" />
|
||||||
|
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||||
|
<title>APM UI</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<link rel="stylesheet/less" type="text/css" href="/color.less" />
|
||||||
<script>
|
<script>
|
||||||
window.less = {
|
window.less = {
|
||||||
async: false,
|
async: false,
|
||||||
env: 'development'//production development
|
env: 'production'
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
</head>
|
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/less.js/2.7.2/less.min.js"></script>
|
||||||
<body>
|
|
||||||
<link rel="stylesheet/less" type="text/css" href="/color.less" />
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="apm-ui-root"></div>
|
||||||
<script type="text/javascript" src="/less.min.js"></script>
|
|
||||||
|
|
||||||
<!-- <script>
|
<!--
|
||||||
var userAgent = navigator.userAgent; //取得浏览器的userAgent字符串
|
This HTML file is a template.
|
||||||
var isOpera = userAgent.indexOf("Opera") > -1;
|
If you open it directly in the browser, you will see an empty page.
|
||||||
console.log(userAgent)
|
|
||||||
//判断是否Opera浏览器
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
if (isOpera) {
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
console.log("Opera");
|
|
||||||
};
|
To begin the development, run `npm start` in this folder.
|
||||||
//判断是否Firefox浏览器
|
To create a production bundle, use `npm run build`.
|
||||||
if (userAgent.indexOf("Firefox") > -1) {
|
-->
|
||||||
console.log("FF");
|
|
||||||
}
|
|
||||||
//判断是否Chrome浏览器
|
|
||||||
if (userAgent.indexOf("Chrome") > -1){
|
|
||||||
console.log("Chrome");
|
|
||||||
}
|
|
||||||
//判断是否Safari浏览器
|
|
||||||
if (userAgent.indexOf("Safari") > -1) {
|
|
||||||
console.log("Safari");
|
|
||||||
}
|
|
||||||
//判断是否IE浏览器
|
|
||||||
if (!!window.ActiveXObject || "ActiveXObject" in window) {
|
|
||||||
console.log("IE");
|
|
||||||
};
|
|
||||||
</script> -->
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Before Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 22 KiB |
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"short_name": "React App",
|
|
||||||
"name": "Create React App Sample",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "favicon.ico",
|
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
|
||||||
"type": "image/x-icon"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo192.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "192x192"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo512.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#ffffff"
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
# https://www.robotstxt.org/robotstxt.html
|
|
||||||
User-agent: *
|
|
@ -1,33 +0,0 @@
|
|||||||
@import '../../styles/main.less';
|
|
||||||
|
|
||||||
.languages{
|
|
||||||
display: flex;
|
|
||||||
&>div{
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0px 0px 4px 1px rgba(220, 223, 225, 0.5);
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
margin-right: 14px;
|
|
||||||
border-radius: 50%;
|
|
||||||
transition: all .2s;
|
|
||||||
}
|
|
||||||
&>div:hover{
|
|
||||||
transform: scale(1.3);
|
|
||||||
}
|
|
||||||
&>div:last-child{
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
.selected{
|
|
||||||
position: absolute;
|
|
||||||
top: 6%;
|
|
||||||
left: 6%;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.icon{
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { Icon } from 'antd'
|
|
||||||
import { inject, observer } from 'mobx-react'
|
|
||||||
import style from './index.module.less'
|
|
||||||
import {ISystem} from '../../store/system'
|
|
||||||
const Languages = inject('system')(observer((props:{system:ISystem} & any) =>{
|
|
||||||
let {system}:{system:ISystem} = props
|
|
||||||
|
|
||||||
const IconFont = Icon.createFromIconfontCN({
|
|
||||||
scriptUrl: '//at.alicdn.com/t/font_1585712_tvew52du1cn.js'
|
|
||||||
})
|
|
||||||
|
|
||||||
function setLocale(locale:string){
|
|
||||||
system.setLocale(locale)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={style.languages}>
|
|
||||||
<div onClick={()=>{setLocale('en_GB')}}>
|
|
||||||
<IconFont type="icon-yingguo" className={`${style.icon}` } />
|
|
||||||
<div className={`${system.locale === 'en_GB'?style.selected:''}`}></div>
|
|
||||||
</div>
|
|
||||||
<div onClick={()=>{setLocale('zh_CN')}}>
|
|
||||||
<IconFont type="icon-china" className={`${style.icon}`} />
|
|
||||||
<div className={`${system.locale === 'zh_CN'?style.selected:''}`}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default Languages
|
|
@ -0,0 +1,14 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// site-prefix.js must be the first import of the main webpack entrypoint
|
||||||
|
// becaue it configures the webpack publicPath.
|
||||||
|
/* eslint-disable import/first */
|
||||||
|
import './site-prefix';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { document } from 'global';
|
||||||
|
|
||||||
|
import { Provider } from 'mobx-react'
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/order, import/no-unresolved
|
||||||
|
import UIApp from './pages/App';
|
||||||
|
import './styles/main.less'
|
||||||
|
import 'react-grid-layout/css/styles.css'
|
||||||
|
|
||||||
|
// these need to go after the App import
|
||||||
|
/* eslint-disable import/first */
|
||||||
|
import 'u-basscss/css/flexbox.css';
|
||||||
|
import 'u-basscss/css/layout.css';
|
||||||
|
import 'u-basscss/css/margin.css';
|
||||||
|
import 'u-basscss/css/padding.css';
|
||||||
|
import 'u-basscss/css/position.css';
|
||||||
|
import 'u-basscss/css/typography.css';
|
||||||
|
|
||||||
|
const UI_ROOT_ID = 'apm-ui-root';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
import stores from './store'
|
||||||
|
import ConfigProvider from './components/ConfigProvider'
|
||||||
|
import Intl from './components/Intl'
|
||||||
|
import * as serviceWorker from './serviceWorker';
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<Provider {...stores}>
|
||||||
|
<Intl>
|
||||||
|
<ConfigProvider>
|
||||||
|
<UIApp />
|
||||||
|
</ConfigProvider>
|
||||||
|
</Intl>
|
||||||
|
</Provider>
|
||||||
|
, document.getElementById(UI_ROOT_ID));
|
||||||
|
|
||||||
|
|
||||||
|
// If you want your app to work offline and load faster, you can change
|
||||||
|
// unregister() to register() below. Note this comes with some pitfalls.
|
||||||
|
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||||
|
serviceWorker.unregister();
|
@ -1,17 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom'
|
|
||||||
import * as serviceWorker from './serviceWorker'
|
|
||||||
import App from './pages/App'
|
|
||||||
import { BrowserRouter as Router} from 'react-router-dom'
|
|
||||||
import { Provider } from 'mobx-react'
|
|
||||||
import stores from './store'
|
|
||||||
|
|
||||||
ReactDOM.render(
|
|
||||||
<Router>
|
|
||||||
<Provider {...stores}>
|
|
||||||
<App />
|
|
||||||
</Provider>
|
|
||||||
</Router>
|
|
||||||
, document.getElementById('root'))
|
|
||||||
|
|
||||||
serviceWorker.unregister()
|
|
@ -1,7 +0,0 @@
|
|||||||
@import '../../styles/main.less';
|
|
||||||
|
|
||||||
.breadcrumb{
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 6px 20px;
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
@import '../../styles/main.less';
|
|
||||||
|
|
||||||
.drawer{
|
|
||||||
:global {
|
|
||||||
.ant-col{
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.row {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.column {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #3b4859;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.sub_title {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #8998ac;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { Drawer, Row, Switch } from 'antd'
|
|
||||||
import { inject, observer } from 'mobx-react'
|
|
||||||
import style from './index.module.less'
|
|
||||||
import { CirclePicker,ColorResult } from 'react-color'
|
|
||||||
import { modifyVars } from '../../library/utils/modifyVars'
|
|
||||||
import Languages from '../../components/Languages'
|
|
||||||
import { FormattedMessage as Message } from 'react-intl'
|
|
||||||
import {ISystem} from '../../store/system'
|
|
||||||
const DrawerWrapper = inject('system')(observer((props:{system:ISystem} & any) =>{
|
|
||||||
let {system}:{system:ISystem} = props
|
|
||||||
|
|
||||||
let primary = (color:ColorResult)=>{
|
|
||||||
system.setPrimary(color.hex)
|
|
||||||
modifyVars(system.dark, color.hex)
|
|
||||||
// modifyVars({'@primary-color': color.hex})
|
|
||||||
}
|
|
||||||
let dark = ()=>{
|
|
||||||
system.setDark()
|
|
||||||
modifyVars(system.dark, system.primary)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<Drawer
|
|
||||||
title={<Message id='setting'/>}
|
|
||||||
width={380}
|
|
||||||
onClose={system.setDrawer}
|
|
||||||
visible={system.drawer}
|
|
||||||
bodyStyle={{ paddingBottom: 80 }}
|
|
||||||
className={`${style.drawer} ${system.dark?style.dark:''}`}
|
|
||||||
>
|
|
||||||
<Row>
|
|
||||||
<div className={style.row}>
|
|
||||||
<div><Message id='model'/></div>
|
|
||||||
<div><Switch defaultChecked onClick={ dark } /></div>
|
|
||||||
</div>
|
|
||||||
<div className={style.row}>
|
|
||||||
<div><Message id='themes'/></div>
|
|
||||||
<div>
|
|
||||||
<CirclePicker color={system.primary} onChangeComplete={ primary }/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={style.row}>
|
|
||||||
<div><Message id='languages'/></div>
|
|
||||||
<div>
|
|
||||||
<Languages />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Row>
|
|
||||||
</Drawer>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default DrawerWrapper
|
|
@ -0,0 +1,15 @@
|
|||||||
|
.header{
|
||||||
|
height:100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-sizing: border-box;
|
||||||
|
&>div{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
&>div{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,56 +0,0 @@
|
|||||||
@import '../../styles/main.less';
|
|
||||||
|
|
||||||
.header{
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
box-sizing: border-box;
|
|
||||||
&>div{
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
&>div{
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 64px;
|
|
||||||
width: 50px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.menu_icon{
|
|
||||||
display: block;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
.icon{
|
|
||||||
display: block;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plummer{
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pointer{
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.messagePopover{
|
|
||||||
width: 400px;
|
|
||||||
}
|
|
||||||
.userPopover{
|
|
||||||
width: 200px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
&>div{
|
|
||||||
width: 100%;
|
|
||||||
height: 48px;
|
|
||||||
line-height: 48px;
|
|
||||||
cursor: pointer;
|
|
||||||
& > i{
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
&:hover{
|
|
||||||
&>span, &>i{
|
|
||||||
color: @dropdown-selected-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,86 +1,53 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Layout, Icon, Badge, Avatar, Popover } from 'antd'
|
|
||||||
import { useHistory } from 'react-router-dom'
|
|
||||||
import { inject, observer } from 'mobx-react'
|
import { inject, observer } from 'mobx-react'
|
||||||
import { removeToken } from '../../library/utils/auth'
|
import { ISystem } from '../../store/system'
|
||||||
import style from './index.module.less'
|
import { Layout, DatePicker } from 'antd'
|
||||||
import {IUser} from '../../store/user'
|
import { MenuFoldOutlined } from '@ant-design/icons';
|
||||||
import {ISystem} from '../../store/system'
|
import './index.less'
|
||||||
import {isMobile} from '../../pages/Responsive'
|
import moment from 'moment';
|
||||||
|
import BreadcrumbWrapper from '../Breadcrumb'
|
||||||
|
|
||||||
const { Header } = Layout
|
const { Header } = Layout
|
||||||
|
|
||||||
const HeaderWrapper = inject('system', 'user')(observer((props:{user:IUser,system:ISystem} & any) =>{
|
function Index(props: { system: ISystem }) {
|
||||||
let history = useHistory()
|
let { system } = props
|
||||||
let {system, user}:{system:ISystem,user:IUser} = props
|
const { RangePicker } = DatePicker;
|
||||||
|
function changeDate(_: any, dateString: any) {
|
||||||
const onClickLogout = ()=>{
|
system.setStartDate(dateString[0])
|
||||||
removeToken()
|
system.setEndDate(dateString[1])
|
||||||
history.push('/login')
|
|
||||||
}
|
}
|
||||||
const userPopover = (
|
|
||||||
<div className={style.userPopover}>
|
|
||||||
<div onClick={onClickLogout}><Icon type="logout" /><span>退出</span></div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
const messagePopover = (
|
|
||||||
<div className={style.messagePopover}>
|
|
||||||
<div>ss</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Header>
|
|
||||||
{
|
|
||||||
isMobile() ?
|
|
||||||
<>dsa</>
|
|
||||||
:
|
|
||||||
<div className={style.header}>
|
|
||||||
<div>
|
|
||||||
<Icon type={system.collapsed?'menu-unfold':'menu-fold'} className={style.menu_icon} onClick={()=>{system.setCollapsed()}} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
{/* <AutoComplete
|
|
||||||
className="certain-category-search"
|
|
||||||
dropdownClassName="certain-category-search-dropdown"
|
|
||||||
dropdownMatchSelectWidth={false}
|
|
||||||
dropdownStyle={{ width: 300 }}
|
|
||||||
size="large"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
placeholder="input here"
|
|
||||||
optionLabelProp="value"
|
|
||||||
>
|
|
||||||
<Input suffix={<Icon type="search" className={`${style.icon}`} />} />
|
|
||||||
</AutoComplete> */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Badge dot>
|
|
||||||
<Popover placement="bottomRight" content={messagePopover}>
|
|
||||||
<Icon type="bell" className={style.icon} />
|
|
||||||
</Popover>
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
return (
|
||||||
<Popover className={`${style.pointer}`} placement="bottomRight" content={userPopover}>
|
<Header className="site-layout-background" style={{ height: '46px', lineHeight: '46px', padding: '0 20px' ,position: 'fixed', zIndex: 1, width: 'calc(100% - 83px)'}} >
|
||||||
<Badge dot>
|
<div className='header'>
|
||||||
<Avatar icon="user" src={user.avatar}/>
|
<div>
|
||||||
</Badge>
|
<BreadcrumbWrapper />
|
||||||
</Popover>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
|
<div>
|
||||||
<div>
|
<RangePicker
|
||||||
<Icon type="align-left" className={style.icon} onClick={()=>{system.setDrawer()}} />
|
className="date-picker"
|
||||||
</div>
|
showTime={{ format: 'HH:mm' }}
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
</div>
|
onChange={changeDate}
|
||||||
</div>
|
value={[moment(system.startDate), moment(system.endDate)]}
|
||||||
}
|
ranges={{
|
||||||
</Header>
|
'5m': [moment().subtract(5, 'm'), moment()],
|
||||||
</>
|
'30m': [moment().subtract(30, 'm'), moment()],
|
||||||
|
'1h': [moment().subtract(1, 'h'), moment()],
|
||||||
|
'6h': [moment().subtract(6, 'h'), moment()],
|
||||||
|
'1d': [moment().subtract(1, 'd'), moment()],
|
||||||
|
'3d': [moment().subtract(3, 'd'), moment()],
|
||||||
|
'7d': [moment().subtract(7, 'd'), moment()],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Header>
|
||||||
)
|
)
|
||||||
}))
|
}
|
||||||
|
|
||||||
|
let HeaderWrapper = inject('system')(observer(Index))
|
||||||
|
|
||||||
export default HeaderWrapper
|
export default HeaderWrapper as any
|
@ -1,33 +0,0 @@
|
|||||||
.logo{
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
font-size: 18px;
|
|
||||||
&>div{
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.title{
|
|
||||||
flex: 1;
|
|
||||||
font-weight:900;
|
|
||||||
color: #3B4859;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding-left: 19px;
|
|
||||||
&>span:first-of-type{
|
|
||||||
color: #3B7CFF;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.title_hidden{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.icon{
|
|
||||||
height: 64px;
|
|
||||||
width:64px;
|
|
||||||
justify-content: center;
|
|
||||||
transform: rotate(180deg);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.icon_transition{
|
|
||||||
transform: rotate(0);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { Icon } from 'antd'
|
|
||||||
import style from './index.module.less'
|
|
||||||
import { inject, observer } from 'mobx-react'
|
|
||||||
import {ISystem} from '../../store/system'
|
|
||||||
|
|
||||||
let Logo = inject('system')(observer((props:{system:ISystem}) => {
|
|
||||||
let {system} = props
|
|
||||||
return(
|
|
||||||
<div className={style.logo}>
|
|
||||||
{/* ${display?style.title:style.title_hidden} */}
|
|
||||||
<div>
|
|
||||||
dsa
|
|
||||||
</div>
|
|
||||||
<div onClick={()=>{system.setCollapsed()}} className={`${style.icon} ${system.collapsed?style.icon_transition:''}`}>
|
|
||||||
<Icon type="menu-unfold" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default Logo
|
|
@ -1,70 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { useLocation } from 'react-router-dom'
|
|
||||||
import { Menu, Icon } from 'antd'
|
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import { inject, observer } from 'mobx-react'
|
|
||||||
import { isEmpty } from '../../library/utils/validate'
|
|
||||||
import style from './index.module.less'
|
|
||||||
import { FormattedMessage as Message } from 'react-intl'
|
|
||||||
const { SubMenu } = Menu
|
|
||||||
const MenuWrapper = inject('system')(observer((props) =>{
|
|
||||||
let {system, routers} = props
|
|
||||||
let location = useLocation()
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={style.menu}>
|
|
||||||
<div className={style.logo}>
|
|
||||||
</div>
|
|
||||||
<Menu
|
|
||||||
mode={`${system.collapsed ? 'vertical' : 'inline'}`}
|
|
||||||
theme={`${system.theme}`}
|
|
||||||
selectedKeys={[location.pathname]}
|
|
||||||
forceSubMenuRender={true}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
routers.map((route) => {
|
|
||||||
if(isEmpty(route.children)){
|
|
||||||
return (
|
|
||||||
<Menu.Item key={`${route.path}`}>
|
|
||||||
<Link to={route.path}>
|
|
||||||
<Icon type={route.icon}/>
|
|
||||||
<span><Message id={route.title}/></span>
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
)
|
|
||||||
}else{
|
|
||||||
let items = []
|
|
||||||
route.children.map((r) => {
|
|
||||||
items.push(
|
|
||||||
<Menu.Item key={r.path}>
|
|
||||||
<Link to={r.path}>
|
|
||||||
{
|
|
||||||
isEmpty(r.icon)?'':<Icon type={r.icon}/>
|
|
||||||
}
|
|
||||||
<span><Message id={r.title}/></span>
|
|
||||||
</Link>
|
|
||||||
</Menu.Item>
|
|
||||||
)
|
|
||||||
return ''
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<SubMenu key={`${route.path}`} title={
|
|
||||||
<span>
|
|
||||||
<Icon type={route.icon} />
|
|
||||||
<span><Message id={route.title}/></span>
|
|
||||||
</span>
|
|
||||||
}>
|
|
||||||
{items}
|
|
||||||
</SubMenu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</Menu>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default MenuWrapper
|
|
@ -1,7 +0,0 @@
|
|||||||
.menu{
|
|
||||||
.logo{
|
|
||||||
height: 32px;
|
|
||||||
background: #aaa;
|
|
||||||
margin: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,116 @@
|
|||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import { useLocation, Link } from 'react-router-dom'
|
||||||
|
import { Menu, Layout } from 'antd'
|
||||||
|
import { inject, observer } from 'mobx-react'
|
||||||
|
import './index.less'
|
||||||
|
import { FormattedMessage as Message } from 'react-intl'
|
||||||
|
import { GlobalOutlined, UserOutlined } from '@ant-design/icons';
|
||||||
|
import { isEmpty } from '../../library/utils/validate'
|
||||||
|
import { logout } from '../../library/utils/account';
|
||||||
|
import { createFromIconfontCN } from '@ant-design/icons';
|
||||||
|
import darkVars from '../../styles/dark.json';
|
||||||
|
import lightVars from '../../styles/light.json';
|
||||||
|
|
||||||
|
const { Sider } = Layout
|
||||||
|
|
||||||
|
const { SubMenu } = Menu
|
||||||
|
const SiderWrapper = inject('system')(observer(props => {
|
||||||
|
const { system, routers } = props
|
||||||
|
useEffect(() => {
|
||||||
|
let vars = system.theme === "light" ? lightVars : darkVars;
|
||||||
|
vars = { ...vars, '@white': '#fff', '@black': '#000' };
|
||||||
|
window.less.modifyVars(vars)
|
||||||
|
},[system.theme])
|
||||||
|
|
||||||
|
|
||||||
|
const location = useLocation()
|
||||||
|
const MyIcon = createFromIconfontCN({
|
||||||
|
scriptUrl: '//at.alicdn.com/t/font_1402269_m6v7u5uwb2.js',
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Sider collapsed={system.collapsed} className="sider" style={{
|
||||||
|
overflow: 'auto',
|
||||||
|
height: '100vh',
|
||||||
|
position: 'fixed',
|
||||||
|
left: 0,
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<div className="logo" />
|
||||||
|
<Menu
|
||||||
|
mode={`${system.collapsed ? 'vertical' : 'inline'}`}
|
||||||
|
selectedKeys={[location.pathname]}
|
||||||
|
forceSubMenuRender="true"
|
||||||
|
className="sider-menu"
|
||||||
|
theme="dark"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
routers.map(route => {
|
||||||
|
if (isEmpty(route.children)) {
|
||||||
|
return (
|
||||||
|
<Menu.Item key={`${route.path}`}>
|
||||||
|
<Link to={route.path}>
|
||||||
|
|
||||||
|
<route.icon />
|
||||||
|
<span><Message id={route.title} /></span>
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const items = []
|
||||||
|
route.children.map(r => {
|
||||||
|
if (r.inMenu !== false) {
|
||||||
|
items.push(
|
||||||
|
<Menu.Item key={r.path}>
|
||||||
|
<Link to={r.path}>
|
||||||
|
{
|
||||||
|
// isEmpty(r.icon) ? '' : <r.icon />
|
||||||
|
}
|
||||||
|
<span><Message id={r.title} /></span>
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<SubMenu key={`${route.path}`} title={
|
||||||
|
<span>
|
||||||
|
<route.icon />
|
||||||
|
<span><Message id={route.title} /></span>
|
||||||
|
</span>
|
||||||
|
}>
|
||||||
|
{items}
|
||||||
|
</SubMenu>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<Menu.Item key="set-locale1" style={{ position: 'absolute', bottom: '95px', right: '32px' }}>
|
||||||
|
{
|
||||||
|
system.theme === 'light' ?
|
||||||
|
<MyIcon type="icon-moon" onClick={() => {
|
||||||
|
system.setTheme('dark')
|
||||||
|
|
||||||
|
}} /> :
|
||||||
|
<MyIcon type="icon-sun" onClick={() => {
|
||||||
|
system.setTheme('light')
|
||||||
|
}} style={{ fontSize: '18px' }} />
|
||||||
|
}
|
||||||
|
|
||||||
|
<span><Message id='changeTheme' /></span>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="set-locale" style={{ position: 'absolute', bottom: '55px', right: '32px', fontSize: '16px' }}>
|
||||||
|
<GlobalOutlined onClick={() => { system.setLocale() }} />
|
||||||
|
<span><Message id='languages' /></span>
|
||||||
|
</Menu.Item>
|
||||||
|
<SubMenu key="user-setting" icon={<UserOutlined />} style={{ position: 'absolute', bottom: '20px', right: '0px', fontSize: '16px' }}>
|
||||||
|
<Menu.Item key="logout" onClick={logout}>Logout</Menu.Item>
|
||||||
|
</SubMenu>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</Sider>
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
|
||||||
|
export default SiderWrapper
|
@ -0,0 +1,11 @@
|
|||||||
|
.sider{
|
||||||
|
box-shadow: 0 0 5px #404040;
|
||||||
|
.ant-layout-sider-children {
|
||||||
|
ul {
|
||||||
|
border:none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-menu-item.ant-menu-item-selected {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +0,0 @@
|
|||||||
import Layout from '@/layouts/Layout'
|
|
||||||
export {
|
|
||||||
Layout
|
|
||||||
}
|
|
@ -0,0 +1,33 @@
|
|||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import { withRouter, useHistory } from 'react-router-dom'
|
||||||
|
import { Layout} from 'antd'
|
||||||
|
import './index.less'
|
||||||
|
import { getToken } from '../library/utils/auth'
|
||||||
|
import { isEmpty } from '../library/utils/validate'
|
||||||
|
import Routers from '../routes'
|
||||||
|
import SiderWrapper from './Sider'
|
||||||
|
import HeaderWrapper from './Header'
|
||||||
|
import ContentWrapper from './Content'
|
||||||
|
|
||||||
|
const Index = () => {
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(isEmpty(getToken())){
|
||||||
|
history.push('/login')
|
||||||
|
}
|
||||||
|
return () => { }
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout className="layouts" style={{ minHeight: '100vh' }}>
|
||||||
|
<SiderWrapper routers={Routers} />
|
||||||
|
<Layout style={{ marginLeft: 83 }}>
|
||||||
|
<HeaderWrapper />
|
||||||
|
<ContentWrapper routers={Routers} />
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(Index)
|
@ -1,35 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import Element from '../routes/modules/Element'
|
|
||||||
|
|
||||||
const Dashboard = React.lazy(() => import('../../pages/Index/Dashboard'))
|
|
||||||
const Charts = React.lazy(() => import('../../pages/Index/Charts'))
|
|
||||||
|
|
||||||
export type Router = {
|
|
||||||
path: string,
|
|
||||||
title: string,
|
|
||||||
icon: string,
|
|
||||||
component?: any,
|
|
||||||
children?:any
|
|
||||||
}
|
|
||||||
const Routers:Router[] = [
|
|
||||||
{
|
|
||||||
path: '/home/dashboard',
|
|
||||||
title: 'dashboard',
|
|
||||||
icon: 'dashboard',
|
|
||||||
component: Dashboard
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/home/charts',
|
|
||||||
title: 'chart',
|
|
||||||
icon: 'pie-chart',
|
|
||||||
component: Charts
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/home/element',
|
|
||||||
title: 'element',
|
|
||||||
icon: 'build',
|
|
||||||
children: Element
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
export default Routers
|
|
@ -1,19 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
const A = React.lazy(() => import('../../../pages/Index/Demo/A'))
|
|
||||||
const B = React.lazy(() => import('../../../pages/Index/Demo/B'))
|
|
||||||
|
|
||||||
const Demo = [
|
|
||||||
{
|
|
||||||
path: '/a',
|
|
||||||
title: 'A',
|
|
||||||
icon: 'bar-chart',
|
|
||||||
component: A
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/b',
|
|
||||||
title: 'B',
|
|
||||||
icon: 'bar-chart',
|
|
||||||
component: B
|
|
||||||
}
|
|
||||||
]
|
|
||||||
export default Demo
|
|
@ -1,34 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
const Accordion = React.lazy(() => import('../../../pages/Index/Elements/Accordion'))
|
|
||||||
const Paginations = React.lazy(() => import('../../../pages/Index/Elements/Paginations'))
|
|
||||||
const DatePickers = React.lazy(() => import('../../../pages/Index/Elements/DatePickers'))
|
|
||||||
const Icons = React.lazy(() => import('../../../pages/Index/Elements/Icons'))
|
|
||||||
|
|
||||||
const Demo = [
|
|
||||||
{
|
|
||||||
path: '/home/element/accordion',
|
|
||||||
title: 'accordion',
|
|
||||||
icon: 'pic-center',
|
|
||||||
component: Accordion
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/home/element/paginations',
|
|
||||||
title: 'paginations',
|
|
||||||
icon: 'pic-center',
|
|
||||||
component: Paginations
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/home/element/datePickers',
|
|
||||||
title: 'datePickers',
|
|
||||||
icon: 'calendar',
|
|
||||||
component: DatePickers
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/home/element/icons',
|
|
||||||
title: 'icons',
|
|
||||||
icon: 'smile',
|
|
||||||
component: Icons
|
|
||||||
}
|
|
||||||
]
|
|
||||||
export default Demo
|
|
@ -0,0 +1,8 @@
|
|||||||
|
import {removeToken } from './auth';
|
||||||
|
import storage from './localStorage'
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
storage.set("lastPath", window.location.pathname+window.location.search)
|
||||||
|
removeToken()
|
||||||
|
window.location.href="/login"
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
import Cookies from 'js-cookie'
|
||||||
|
|
||||||
|
const TokenKey = 'X-Token'
|
||||||
|
|
||||||
|
export function getToken() {
|
||||||
|
return Cookies.get(TokenKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setToken(token) {
|
||||||
|
return Cookies.set(TokenKey, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeToken() {
|
||||||
|
return Cookies.remove(TokenKey)
|
||||||
|
}
|
@ -1,15 +0,0 @@
|
|||||||
import Cookies from 'js-cookie'
|
|
||||||
|
|
||||||
const TokenKey = 'admin-token'
|
|
||||||
|
|
||||||
export function getToken() {
|
|
||||||
return Cookies.get(TokenKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setToken(token:string) {
|
|
||||||
return Cookies.set(TokenKey, token)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeToken() {
|
|
||||||
return Cookies.remove(TokenKey)
|
|
||||||
}
|
|
@ -0,0 +1,99 @@
|
|||||||
|
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
const COLORS_HEX = [
|
||||||
|
'#17B8BE',
|
||||||
|
'#F8DCA1',
|
||||||
|
'#B7885E',
|
||||||
|
'#FFCB99',
|
||||||
|
'#F89570',
|
||||||
|
'#829AE3',
|
||||||
|
'#E79FD5',
|
||||||
|
'#1E96BE',
|
||||||
|
'#89DAC1',
|
||||||
|
'#B3AD9E',
|
||||||
|
'#12939A',
|
||||||
|
'#DDB27C',
|
||||||
|
'#88572C',
|
||||||
|
'#FF9833',
|
||||||
|
'#EF5D28',
|
||||||
|
'#162A65',
|
||||||
|
'#DA70BF',
|
||||||
|
'#125C77',
|
||||||
|
'#4DC19C',
|
||||||
|
'#776E57',
|
||||||
|
];
|
||||||
|
|
||||||
|
// TS needs the precise return type
|
||||||
|
function strToRgb(s: string): [number, number, number] {
|
||||||
|
if (s.length !== 7) {
|
||||||
|
return [0, 0, 0];
|
||||||
|
}
|
||||||
|
const r = s.slice(1, 3);
|
||||||
|
const g = s.slice(3, 5);
|
||||||
|
const b = s.slice(5);
|
||||||
|
return [parseInt(r, 16), parseInt(g, 16), parseInt(b, 16)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ColorGenerator {
|
||||||
|
colorsHex: string[];
|
||||||
|
colorsRgb: [number, number, number][];
|
||||||
|
cache: Map<string, number>;
|
||||||
|
currentIdx: number;
|
||||||
|
|
||||||
|
constructor(colorsHex: string[] = COLORS_HEX) {
|
||||||
|
this.colorsHex = colorsHex;
|
||||||
|
this.colorsRgb = colorsHex.map(strToRgb);
|
||||||
|
this.cache = new Map();
|
||||||
|
this.currentIdx = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getColorIndex(key: string): number {
|
||||||
|
let i = this.cache.get(key);
|
||||||
|
if (i == null) {
|
||||||
|
i = this.currentIdx;
|
||||||
|
this.cache.set(key, this.currentIdx);
|
||||||
|
this.currentIdx = ++this.currentIdx % this.colorsHex.length;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will assign a color to an arbitrary key.
|
||||||
|
* If the key has been used already, it will
|
||||||
|
* use the same color.
|
||||||
|
*/
|
||||||
|
getColorByKey(key: string) {
|
||||||
|
const i = this._getColorIndex(key);
|
||||||
|
return this.colorsHex[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the RGB values associated with a key. Adds the key and associates
|
||||||
|
* it with a color if the key is not recognized.
|
||||||
|
* @return {number[]} An array of three ints [0, 255] representing a color.
|
||||||
|
*/
|
||||||
|
getRgbColorByKey(key: string): [number, number, number] {
|
||||||
|
const i = this._getColorIndex(key);
|
||||||
|
return this.colorsRgb[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.cache.clear();
|
||||||
|
this.currentIdx = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ColorGenerator();
|
||||||
|
|
@ -0,0 +1,67 @@
|
|||||||
|
import _round from 'lodash/round';
|
||||||
|
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export const ONE_MILLISECOND = 1000;
|
||||||
|
|
||||||
|
const TODAY = 'Today';
|
||||||
|
const YESTERDAY = 'Yesterday';
|
||||||
|
export const STANDARD_DATE_FORMAT = 'YYYY-MM-DD';
|
||||||
|
export const STANDARD_DATETIME_FORMAT = 'MMMM D YYYY, HH:mm:ss.SSS';
|
||||||
|
/**
|
||||||
|
* Humanizes the duration based on the inputUnit
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* 5000ms => 5s
|
||||||
|
* 1000μs => 1ms
|
||||||
|
*/
|
||||||
|
export function formatDuration(duration: number, inputUnit: string = 'microseconds'): string {
|
||||||
|
let d = duration;
|
||||||
|
if (inputUnit === 'microseconds') {
|
||||||
|
d = duration / 1000;
|
||||||
|
}
|
||||||
|
let units = 'ms';
|
||||||
|
if (d >= 1000) {
|
||||||
|
units = 's';
|
||||||
|
d /= 1000;
|
||||||
|
}
|
||||||
|
return _round(d, 2) + units;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatRelativeDate(value: any, fullMonthName: boolean = false) {
|
||||||
|
const m = moment.isMoment(value) ? value : moment(value);
|
||||||
|
const monthFormat = fullMonthName ? 'MMMM' : 'MMM';
|
||||||
|
const dt = new Date();
|
||||||
|
if (dt.getFullYear() !== m.year()) {
|
||||||
|
return m.format(`${monthFormat} D, YYYY`);
|
||||||
|
}
|
||||||
|
const mMonth = m.month();
|
||||||
|
const mDate = m.date();
|
||||||
|
const date = dt.getDate();
|
||||||
|
if (mMonth === dt.getMonth() && mDate === date) {
|
||||||
|
return TODAY;
|
||||||
|
}
|
||||||
|
dt.setDate(date - 1);
|
||||||
|
if (mMonth === dt.getMonth() && mDate === dt.getDate()) {
|
||||||
|
return YESTERDAY;
|
||||||
|
}
|
||||||
|
return m.format(`${monthFormat} D`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} timestamp
|
||||||
|
* @param {number} initialTimestamp
|
||||||
|
* @param {number} totalDuration
|
||||||
|
* @return {number} 0-100 percentage
|
||||||
|
*/
|
||||||
|
export function getPercentageOfDuration(duration: number, totalDuration: number) {
|
||||||
|
return (duration / totalDuration) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDate(duration: number) {
|
||||||
|
return moment(duration / ONE_MILLISECOND).format(STANDARD_DATE_FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDatetime(duration: number) {
|
||||||
|
return moment(duration / ONE_MILLISECOND).format(STANDARD_DATETIME_FORMAT);
|
||||||
|
}
|
@ -1,29 +0,0 @@
|
|||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
axios.defaults.baseURL = 'https://www.baidu.com'
|
|
||||||
}else{
|
|
||||||
axios.defaults.baseURL = 'https://www.production.com'
|
|
||||||
}
|
|
||||||
|
|
||||||
// let loadingInstance = null //这里是loading
|
|
||||||
//使用create方法创建axios实例
|
|
||||||
export const Service = axios.create({
|
|
||||||
timeout: 7000, // 请求超时时间
|
|
||||||
method: 'post',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json;charset=UTF-8'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 添加请求拦截器
|
|
||||||
Service.interceptors.request.use(config => {
|
|
||||||
return config
|
|
||||||
})
|
|
||||||
// 添加响应拦截器
|
|
||||||
Service.interceptors.response.use(response => {
|
|
||||||
// loadingInstance.close()
|
|
||||||
// console.log(response)
|
|
||||||
return response.data
|
|
||||||
}, error => {
|
|
||||||
return Promise.reject(error)
|
|
||||||
})
|
|
@ -1,9 +0,0 @@
|
|||||||
import {Light, Dark} from '../../styles/theme'
|
|
||||||
|
|
||||||
export function modifyVars(model:boolean, primary:string){
|
|
||||||
if(model){
|
|
||||||
window.less.modifyVars(Dark(primary))
|
|
||||||
}else{
|
|
||||||
window.less.modifyVars(Light(primary))
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,48 @@
|
|||||||
|
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import sitePrefix from './site-prefix';
|
||||||
|
|
||||||
|
const origin = process.env.NODE_ENV === 'test' ? global.location.origin : window.location.origin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the URL prefix from `sitePrefix` and use it for all subsequent calls
|
||||||
|
* to `prefixUrl()`. `sitePrefix` should be an absolute URL, e.g. with an origin.
|
||||||
|
* `pathPrefix` is just the path portion and should not have a trailing slash:
|
||||||
|
*
|
||||||
|
* - `"http://localhost:3000/"` to `""`
|
||||||
|
* - `"http://localhost:3000/abc/"` to `"/abc"`
|
||||||
|
* - `"http://localhost:3000/abc/def/"` to `"/abc/def"`
|
||||||
|
*/
|
||||||
|
// exported for tests
|
||||||
|
export function getPathPrefix(orig, sitePref) {
|
||||||
|
const o = orig == null ? '' : orig.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||||
|
const s = sitePref == null ? '' : sitePref;
|
||||||
|
const rx = new RegExp(`^${o}|/$`, 'ig');
|
||||||
|
return s.replace(rx, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathPrefix = getPathPrefix(origin, sitePrefix);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the path prefix to the URL. See [site-prefix.js](../site-prefix.js) and
|
||||||
|
* the `<base>` tag in [index.html](../../public/index.html) for details.
|
||||||
|
*
|
||||||
|
* @param {string} value The URL to have the prefix added to.
|
||||||
|
* @return {string} The resultant URL.
|
||||||
|
*/
|
||||||
|
export default function prefixUrl(value) {
|
||||||
|
const s = value == null ? '' : String(value);
|
||||||
|
return `${pathPrefix}${s}`;
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright (c) 2018 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Per the resolution of https://github.com/jaegertracing/jaeger-ui/issues/42,
|
||||||
|
// package.json#homepage is set to "." and the document MUST have a <base>
|
||||||
|
// element to define a usable base URL.
|
||||||
|
const baseNode = document.querySelector('base');
|
||||||
|
if (!baseNode && process.env.NODE_ENV !== 'test') {
|
||||||
|
throw new Error('<base> element not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sitePrefix = baseNode ? baseNode.href : `${global.location.origin}/`;
|
||||||
|
|
||||||
|
// Configure the webpack publicPath to match the <base>:
|
||||||
|
// https://webpack.js.org/guides/public-path/#on-the-fly
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
window.__webpack_public_path__ = sitePrefix;
|
||||||
|
|
||||||
|
export default sitePrefix;
|
@ -0,0 +1,33 @@
|
|||||||
|
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
// import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import ErrorMessage from '../Trace/components/common/ErrorMessage';
|
||||||
|
// import prefixUrl from '../../utils/prefix-url';
|
||||||
|
|
||||||
|
type NotFoundProps = {
|
||||||
|
error: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function NotFound({ error }: NotFoundProps) {
|
||||||
|
return (
|
||||||
|
<section className="ub-m3">
|
||||||
|
<h1>Error</h1>
|
||||||
|
{error && <ErrorMessage error={error} />}
|
||||||
|
{/* <Link to={prefixUrl('/')}>Back home</Link> */}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Ant's default font-family, Helvetica Neue For Number, is inconcistent in thickness for a given weight */
|
||||||
|
.font-reset,
|
||||||
|
html > body {
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, 'Segoe UI', Roboto, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #11939a;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #00474e;
|
||||||
|
}
|
@ -0,0 +1,62 @@
|
|||||||
|
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { Route, Switch,BrowserRouter as Router } from 'react-router-dom';
|
||||||
|
import { ConnectedRouter } from 'react-router-redux';
|
||||||
|
|
||||||
|
import NotFound from './NotFound';
|
||||||
|
// eslint-disable-next-line import/order, import/no-unresolved
|
||||||
|
import TracePage from '../Trace/components/TracePage';
|
||||||
|
// eslint-disable-next-line import/order, import/no-unresolved
|
||||||
|
import Login from '../Login'
|
||||||
|
|
||||||
|
import { ROUTE_PATH as tracePath } from '../Trace/components/TracePage/url';
|
||||||
|
import configureStore from '../Trace/utils/configure-store';
|
||||||
|
import processScripts from '../Trace/utils/config/process-scripts';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
import Layouts from '../../layouts/index.jsx'
|
||||||
|
|
||||||
|
const history = require("history").createBrowserHistory()
|
||||||
|
|
||||||
|
|
||||||
|
export default class UIApp extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.store = configureStore(history);
|
||||||
|
processScripts();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Provider store={this.store}>
|
||||||
|
<ConnectedRouter history={history}>
|
||||||
|
<Router>
|
||||||
|
<Switch>
|
||||||
|
<Route path={tracePath} component={TracePage} />
|
||||||
|
<Route path="/ui" component={Layouts} />
|
||||||
|
<Route path="/login" exact component={Login} />
|
||||||
|
<Route component={NotFound} />
|
||||||
|
</Switch>
|
||||||
|
</Router>
|
||||||
|
</ConnectedRouter>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,35 +0,0 @@
|
|||||||
import React, { useEffect } from 'react'
|
|
||||||
import {Route, Switch, Redirect} from 'react-router-dom'
|
|
||||||
import '@/styles/main.less'
|
|
||||||
import Home from '../../pages/Index'
|
|
||||||
import Login from '../../pages/Login'
|
|
||||||
import ConfigProvider from '../../components/ConfigProvider'
|
|
||||||
import Intl from '../../components/Intl'
|
|
||||||
|
|
||||||
let App = () => {
|
|
||||||
//npm install --save rc-form-hooks
|
|
||||||
//
|
|
||||||
useEffect(() => {
|
|
||||||
// console.log("modify vars")
|
|
||||||
// alert(1)
|
|
||||||
// modifyVars(system.dark, system.primary)
|
|
||||||
return () => {}
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Intl>
|
|
||||||
<ConfigProvider>
|
|
||||||
<Switch>
|
|
||||||
<Route path="/home" component={Home} />
|
|
||||||
<Route path="/login" exact component={Login} />
|
|
||||||
<Redirect to="/home"/>
|
|
||||||
</Switch>
|
|
||||||
</ConfigProvider>
|
|
||||||
</Intl>
|
|
||||||
|
|
||||||
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
|
@ -1,97 +0,0 @@
|
|||||||
import React, { useState, useEffect} from 'react'
|
|
||||||
import { inject, observer } from 'mobx-react'
|
|
||||||
import { Spin, Row, Col, Card } from 'antd'
|
|
||||||
import {
|
|
||||||
Chart,
|
|
||||||
Geom,
|
|
||||||
Axis,
|
|
||||||
Tooltip
|
|
||||||
} from 'bizcharts'
|
|
||||||
|
|
||||||
const Dashboard = inject('system')(observer(() =>{
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(false)
|
|
||||||
return () => {}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const data = [
|
|
||||||
{
|
|
||||||
year: '1991',
|
|
||||||
value: 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
year: '1992',
|
|
||||||
value: 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
year: '1993',
|
|
||||||
value: 3.5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
year: '1994',
|
|
||||||
value: 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
year: '1995',
|
|
||||||
value: 4.9
|
|
||||||
},
|
|
||||||
{
|
|
||||||
year: '1996',
|
|
||||||
value: 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
year: '1997',
|
|
||||||
value: 7
|
|
||||||
},
|
|
||||||
{
|
|
||||||
year: '1998',
|
|
||||||
value: 9
|
|
||||||
},
|
|
||||||
{
|
|
||||||
year: '1999',
|
|
||||||
value: 13
|
|
||||||
}
|
|
||||||
]
|
|
||||||
const cols = {
|
|
||||||
value: {
|
|
||||||
min: 0
|
|
||||||
},
|
|
||||||
year: {
|
|
||||||
range: [0, 1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Spin spinning={loading} tip='Loading...'>
|
|
||||||
<div className='container'>
|
|
||||||
<Row gutter={24}>
|
|
||||||
<Col xs={24} sm={24} md={12} lg={12} xl={12} className='gutter-row'>
|
|
||||||
<Card style={{borderRadius:'10px'}}>
|
|
||||||
<Chart padding="auto" height={278} data={data} scale={cols} forceFit>
|
|
||||||
<Axis name='year' />
|
|
||||||
<Axis name='value' />
|
|
||||||
<Tooltip
|
|
||||||
crosshairs={{
|
|
||||||
type: 'y'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Geom type='line' position='year*value' size={2} />
|
|
||||||
<Geom
|
|
||||||
type='point'
|
|
||||||
position='year*value'
|
|
||||||
size={4}
|
|
||||||
shape={'circle'}
|
|
||||||
style={{
|
|
||||||
stroke: '#fff',
|
|
||||||
lineWidth: 1
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Chart>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
</Spin>
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
export default Dashboard
|
|
@ -1,20 +0,0 @@
|
|||||||
import React, { useState, useEffect} from 'react'
|
|
||||||
import { inject, observer } from 'mobx-react'
|
|
||||||
import { Spin } from 'antd'
|
|
||||||
const A = inject('system')(observer(() =>{
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(false)
|
|
||||||
return () => {}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Spin spinning={loading} tip="Loading...">
|
|
||||||
<div className='container'>
|
|
||||||
s
|
|
||||||
</div>
|
|
||||||
</Spin>
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
export default A
|
|
@ -1,94 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
|
||||||
import { Spin } from 'antd'
|
|
||||||
function B() {
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(false)
|
|
||||||
return () => {}
|
|
||||||
}, [])
|
|
||||||
return (
|
|
||||||
<Spin spinning={loading} tip="Loading...">
|
|
||||||
<div>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
<p>ds</p>
|
|
||||||
</div>
|
|
||||||
</Spin>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default B
|
|
@ -1,19 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { Spin } from 'antd'
|
|
||||||
|
|
||||||
function DatePickers() {
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(false)
|
|
||||||
return () => {}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Spin spinning={loading} tip="Loading...">
|
|
||||||
<div className='container'>
|
|
||||||
DatePicker
|
|
||||||
</div>
|
|
||||||
</Spin>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default DatePickers
|
|
@ -1,19 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { Spin } from 'antd'
|
|
||||||
|
|
||||||
function Icons() {
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(false)
|
|
||||||
return () => {}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Spin spinning={loading} tip="Loading...">
|
|
||||||
<div className='container'>
|
|
||||||
Icons
|
|
||||||
</div>
|
|
||||||
</Spin>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default Icons
|
|
@ -1,45 +0,0 @@
|
|||||||
import React, { useEffect } from 'react'
|
|
||||||
import { withRouter, useHistory } from 'react-router-dom'
|
|
||||||
import { Layout } from 'antd'
|
|
||||||
import style from './index.module.less'
|
|
||||||
import { inject, observer } from 'mobx-react'
|
|
||||||
import MenuWrapper from '../../layouts/Menu'
|
|
||||||
import DrawerWrapper from '../../layouts/Drawer'
|
|
||||||
import ContentWrapper from '../../layouts/Content'
|
|
||||||
import HeaderWrapper from '../../layouts/Header'
|
|
||||||
import { getToken } from '../../library/utils/auth'
|
|
||||||
import { isEmpty } from '../../library/utils/validate'
|
|
||||||
import Routers from '../../library/routes'
|
|
||||||
import {isDesktop} from '../../pages/Responsive'
|
|
||||||
import { ISystem } from '../../store/system'
|
|
||||||
|
|
||||||
const { Sider } = Layout
|
|
||||||
|
|
||||||
let Index = inject('system')(observer((props:{system:ISystem}) => {
|
|
||||||
let {system} = props
|
|
||||||
let history = useHistory()
|
|
||||||
useEffect(() => {
|
|
||||||
if(isEmpty(getToken())){
|
|
||||||
history.push('/login')
|
|
||||||
}
|
|
||||||
return () => {}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout className={`${style.app} ${system.dark?style.dark:''}`}>
|
|
||||||
{isDesktop() &&
|
|
||||||
<Sider collapsed={system.collapsed}>
|
|
||||||
<MenuWrapper routers={Routers}/>
|
|
||||||
</Sider>
|
|
||||||
}
|
|
||||||
<Layout>
|
|
||||||
<HeaderWrapper />
|
|
||||||
<ContentWrapper routers={Routers}/>
|
|
||||||
<DrawerWrapper />
|
|
||||||
</Layout>
|
|
||||||
</Layout>
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default withRouter(Index as any)
|
|
@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
.login{
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login .rectangle{
|
||||||
|
|
||||||
|
width: 360px;
|
||||||
|
box-shadow: 1px 2px 4px 0px;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Form, Input, Button } from 'antd';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
import { inject, observer } from 'mobx-react'
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/order, import/no-unresolved
|
||||||
|
import storage from '../../library/utils/localStorage'
|
||||||
|
|
||||||
|
import request from '../../library/utils/http'
|
||||||
|
import { isEmpty } from '../../library/utils/validate'
|
||||||
|
import { setToken } from '../../library/utils/auth';
|
||||||
|
|
||||||
|
|
||||||
|
function FormBox(props) {
|
||||||
|
const {account:acc} = props
|
||||||
|
const layout = {
|
||||||
|
labelCol: { span: 8 },
|
||||||
|
wrapperCol: { span: 16 },
|
||||||
|
};
|
||||||
|
const tailLayout = {
|
||||||
|
wrapperCol: { offset: 8, span: 16 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
const onFinish = values => {
|
||||||
|
request({
|
||||||
|
url: '/web/login',
|
||||||
|
method: 'POST',
|
||||||
|
params: {
|
||||||
|
username:values.username,
|
||||||
|
password: values.password
|
||||||
|
}
|
||||||
|
}).then(res => {
|
||||||
|
setToken(res.data.data.token)
|
||||||
|
acc.setInfo(res.data.data.account)
|
||||||
|
setTimeout(()=> {
|
||||||
|
const oldPath = storage.get('lastPath')
|
||||||
|
if (!isEmpty(oldPath)) {
|
||||||
|
storage.remove('lastPath')
|
||||||
|
history.push(oldPath)
|
||||||
|
} else {
|
||||||
|
history.push('/ui/dashboard')
|
||||||
|
}
|
||||||
|
},200)
|
||||||
|
})
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFinishFailed = () => {
|
||||||
|
// console.log('Failed:', errorInfo);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="login">
|
||||||
|
<div className="rectangle">
|
||||||
|
<Form
|
||||||
|
{...layout}
|
||||||
|
name="basic"
|
||||||
|
initialValues={{ remember: true }}
|
||||||
|
onFinish={onFinish}
|
||||||
|
onFinishFailed={onFinishFailed}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="Username"
|
||||||
|
name="username"
|
||||||
|
rules={[{ required: true, message: 'Please input your username!' }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Password"
|
||||||
|
name="password"
|
||||||
|
rules={[{ required: true, message: 'Please input your password!' }]}
|
||||||
|
>
|
||||||
|
<Input.Password />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
|
||||||
|
<Form.Item {...tailLayout}>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const Login = inject('account')(observer(FormBox))
|
||||||
|
|
||||||
|
export default Login
|
@ -1,22 +0,0 @@
|
|||||||
|
|
||||||
.app{
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background: #f3f6f9;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
.rectangle{
|
|
||||||
width: 360px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
box-shadow: 0px 2px 4px 0px
|
|
||||||
#d0d0d0;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
:global {
|
|
||||||
.ant-form-item{
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,106 +0,0 @@
|
|||||||
import React, { useState } from 'react'
|
|
||||||
import { Button, Form, Input, Icon, message, Spin } from 'antd'
|
|
||||||
import {WrappedFormUtils} from 'antd/lib/form/Form'
|
|
||||||
import { useHistory } from 'react-router-dom'
|
|
||||||
import style from './index.module.less'
|
|
||||||
import { post } from '../../library/utils/http/index'
|
|
||||||
import { setToken } from '../../library/utils/auth'
|
|
||||||
import storage from '../../library/utils/localStorage'
|
|
||||||
import { inject, observer } from 'mobx-react'
|
|
||||||
import { IUser } from '../../store/user'
|
|
||||||
function FormBox(props: { user: IUser, form: WrappedFormUtils }) {
|
|
||||||
const [loading, setloading] = useState(false)
|
|
||||||
let { user } = props
|
|
||||||
let history = useHistory()
|
|
||||||
const [btnLoading, setBtnLoading] = useState(false)
|
|
||||||
const { getFieldDecorator } = props.form
|
|
||||||
function handleSubmit(e: React.FormEvent<HTMLElement>) {
|
|
||||||
e.preventDefault()
|
|
||||||
props.form.validateFields((err: any, values: any) => {
|
|
||||||
setBtnLoading(true)
|
|
||||||
post('/api/login',
|
|
||||||
{
|
|
||||||
username: 'admin',
|
|
||||||
password: '123456'
|
|
||||||
},
|
|
||||||
function (res: any) {
|
|
||||||
if (res.err === 1) {
|
|
||||||
message.error(res.msg)
|
|
||||||
} else {
|
|
||||||
setToken(res.data.token)
|
|
||||||
delete res.data.token
|
|
||||||
storage.set('info', res.data)
|
|
||||||
setloading(true)
|
|
||||||
user.setInfo(res.data)
|
|
||||||
setTimeout(() => {
|
|
||||||
setloading(false)
|
|
||||||
history.push('/home/dashboard')
|
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function (err: any) {
|
|
||||||
console.log(err)
|
|
||||||
message.error('ds')
|
|
||||||
},
|
|
||||||
function () {
|
|
||||||
setBtnLoading(false)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// if (!err) {
|
|
||||||
// history.push("/index");
|
|
||||||
// }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Spin spinning={loading}>
|
|
||||||
<Form className="login-form">
|
|
||||||
<Form.Item>
|
|
||||||
{getFieldDecorator('username', {
|
|
||||||
initialValue: 'dsa',
|
|
||||||
rules: [{ required: true, message: '用户名不能为空' }]
|
|
||||||
})(
|
|
||||||
<Input
|
|
||||||
prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
|
|
||||||
placeholder="Username"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item>
|
|
||||||
{getFieldDecorator('password', {
|
|
||||||
initialValue: 'dsa',
|
|
||||||
rules: [{ required: true, message: '密码不能为空' }]
|
|
||||||
})(
|
|
||||||
<Input.Password
|
|
||||||
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
|
|
||||||
type="password"
|
|
||||||
placeholder="Password"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item labelAlign='right' labelCol={{ span: 3, offset: 12 }}>
|
|
||||||
<Button onClick={handleSubmit} type="primary" shape="round" loading={btnLoading ? true : false}>
|
|
||||||
登录
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Spin>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const FormBoxWrapper = Form.create({ name: 'normal_login' })(inject('user')(observer(FormBox)))
|
|
||||||
|
|
||||||
let Login = inject('user')(observer(() => {
|
|
||||||
//npm install --save rc-form-hooks
|
|
||||||
// https://www.jianshu.com/p/fc59cb61f7cc
|
|
||||||
return (
|
|
||||||
<div className={style.app}>
|
|
||||||
<div className={style.rectangle}>
|
|
||||||
<FormBoxWrapper />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default Login
|
|
@ -1,19 +0,0 @@
|
|||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
|
||||||
import React from 'react'
|
|
||||||
import {useMediaQuery,MediaQueryProps} from 'react-responsive'
|
|
||||||
import MediaQuery from 'react-responsive'
|
|
||||||
|
|
||||||
const Desktop = (props:MediaQueryProps) => <MediaQuery {...props} minWidth={768} />
|
|
||||||
const Mobile = (props:MediaQueryProps) => <MediaQuery {...props} maxWidth={767} />
|
|
||||||
const isMobile = () => useMediaQuery({
|
|
||||||
query: '(max-device-width: 991px)'
|
|
||||||
})
|
|
||||||
const isDesktop = () => useMediaQuery({
|
|
||||||
query: '(min-device-width: 992px)'
|
|
||||||
})
|
|
||||||
export {
|
|
||||||
Desktop,
|
|
||||||
Mobile,
|
|
||||||
isMobile,
|
|
||||||
isDesktop
|
|
||||||
}
|
|
@ -0,0 +1,59 @@
|
|||||||
|
import React,{useEffect} from 'react';
|
||||||
|
import './index.less';
|
||||||
|
import { Card } from 'antd';
|
||||||
|
import { ISystem } from '../../store/system'
|
||||||
|
import { inject, observer } from 'mobx-react'
|
||||||
|
import GridLayout from 'react-grid-layout';
|
||||||
|
|
||||||
|
const { Meta } = Card;
|
||||||
|
|
||||||
|
function Index(props: {system:ISystem}) {
|
||||||
|
let {system} = props
|
||||||
|
useEffect(() => {
|
||||||
|
|
||||||
|
},[system.startDate,system.endDate])
|
||||||
|
|
||||||
|
const layout = [
|
||||||
|
{i: 'a', x: 0, y: 0, w: 3, h: 3},
|
||||||
|
{i: 'b', x: 3, y: 0, w: 3, h: 3},
|
||||||
|
{i: 'c', x: 6, y: 0, w: 3, h: 3}
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div className="test">
|
||||||
|
<GridLayout className="layout" layout={layout} cols={12} width={1200}>
|
||||||
|
<div key="a">
|
||||||
|
<Card
|
||||||
|
hoverable
|
||||||
|
style={{ width: '100%',height:'100%' }}
|
||||||
|
// cover={<img alt="example" src="https://os.alipayobjects.com/rmsportal/QBnOOoLaAfKPirc.png" />}
|
||||||
|
>
|
||||||
|
<Meta title="Europe Street beat" description="www.instagram.com" />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div key="b">
|
||||||
|
<Card
|
||||||
|
hoverable
|
||||||
|
style={{ width: '100%',height:'100%' }}
|
||||||
|
// cover={<img alt="example" src="https://os.alipayobjects.com/rmsportal/QBnOOoLaAfKPirc.png" />}
|
||||||
|
>
|
||||||
|
<Meta title="Europe Street beat" description="www.instagram.com" />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div key="c">
|
||||||
|
<Card
|
||||||
|
hoverable
|
||||||
|
style={{ width: '100%',height:'100%' }}
|
||||||
|
// cover={<img alt="example" src="https://os.alipayobjects.com/rmsportal/QBnOOoLaAfKPirc.png" />}
|
||||||
|
>
|
||||||
|
<Meta title="Europe Street beat" description="www.instagram.com" />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</GridLayout>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Test = inject('system')(observer(Index))
|
||||||
|
|
||||||
|
export default Test;
|
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright (c) 2019 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import * as fileReaderActions from './file-reader-api';
|
||||||
|
import readJsonFile from '../utils/readJsonFile';
|
||||||
|
|
||||||
|
jest.mock('../utils/readJsonFile');
|
||||||
|
|
||||||
|
describe('actions/file-reader-api', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
readJsonFile.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loadJsonTraces calls readJsonFile', () => {
|
||||||
|
const arg = 'example-arg';
|
||||||
|
fileReaderActions.loadJsonTraces(arg);
|
||||||
|
expect(readJsonFile.mock.calls).toEqual([[arg]]);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,24 @@
|
|||||||
|
// Copyright (c) 2019 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
|
||||||
|
import readJsonFile from '../utils/readJsonFile';
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export const loadJsonTraces = createAction(
|
||||||
|
'@FILE_READER_API/LOAD_JSON',
|
||||||
|
fileList => readJsonFile(fileList),
|
||||||
|
fileList => ({ fileList })
|
||||||
|
);
|
@ -0,0 +1,64 @@
|
|||||||
|
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import JaegerAPI from '../api/jaeger';
|
||||||
|
|
||||||
|
export const fetchTrace = createAction(
|
||||||
|
'@JAEGER_API/FETCH_TRACE',
|
||||||
|
id => JaegerAPI.fetchTrace(id),
|
||||||
|
id => ({ id })
|
||||||
|
);
|
||||||
|
|
||||||
|
export const fetchMultipleTraces = createAction(
|
||||||
|
'@JAEGER_API/FETCH_MULTIPLE_TRACES',
|
||||||
|
ids => JaegerAPI.searchTraces({ traceID: ids }),
|
||||||
|
ids => ({ ids })
|
||||||
|
);
|
||||||
|
|
||||||
|
export const archiveTrace = createAction(
|
||||||
|
'@JAEGER_API/ARCHIVE_TRACE',
|
||||||
|
id => JaegerAPI.archiveTrace(id),
|
||||||
|
id => ({ id })
|
||||||
|
);
|
||||||
|
|
||||||
|
export const searchTraces = createAction(
|
||||||
|
'@JAEGER_API/SEARCH_TRACES',
|
||||||
|
query => JaegerAPI.searchTraces(query),
|
||||||
|
query => ({ query })
|
||||||
|
);
|
||||||
|
|
||||||
|
export const fetchServices = createAction('@JAEGER_API/FETCH_SERVICES', () => JaegerAPI.fetchServices());
|
||||||
|
|
||||||
|
export const fetchServiceOperations = createAction(
|
||||||
|
'@JAEGER_API/FETCH_SERVICE_OPERATIONS',
|
||||||
|
serviceName => JaegerAPI.fetchServiceOperations(serviceName),
|
||||||
|
serviceName => ({ serviceName })
|
||||||
|
);
|
||||||
|
|
||||||
|
export const fetchServiceServerOps = createAction(
|
||||||
|
'@JAEGER_API/FETCH_SERVICE_SERVER_OP',
|
||||||
|
serviceName => JaegerAPI.fetchServiceServerOps(serviceName),
|
||||||
|
serviceName => ({ serviceName })
|
||||||
|
);
|
||||||
|
|
||||||
|
export const fetchDeepDependencyGraph = createAction(
|
||||||
|
'@JAEGER_API/FETCH_DEEP_DEPENDENCY_GRAPH',
|
||||||
|
query => JaegerAPI.fetchDeepDependencyGraph(query),
|
||||||
|
query => ({ query })
|
||||||
|
);
|
||||||
|
|
||||||
|
export const fetchDependencies = createAction('@JAEGER_API/FETCH_DEPENDENCIES', () =>
|
||||||
|
JaegerAPI.fetchDependencies()
|
||||||
|
);
|
@ -0,0 +1,152 @@
|
|||||||
|
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
/* eslint-disable import/first */
|
||||||
|
jest.mock('node-fetch', () => () =>
|
||||||
|
Promise.resolve({
|
||||||
|
status: 200,
|
||||||
|
data: () => Promise.resolve({ data: null }),
|
||||||
|
json: () => Promise.resolve({ data: null }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
import sinon from 'sinon';
|
||||||
|
import isPromise from 'is-promise';
|
||||||
|
|
||||||
|
import * as jaegerApiActions from './jaeger-api';
|
||||||
|
import JaegerAPI from '../api/jaeger';
|
||||||
|
|
||||||
|
describe('actions/jaeger-api', () => {
|
||||||
|
const query = { param: 'value' };
|
||||||
|
const id = 'my-trace-id';
|
||||||
|
const ids = [id, id];
|
||||||
|
let mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mock = sinon.mock(JaegerAPI);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/FETCH_TRACE should fetch the trace by id', () => {
|
||||||
|
mock.expects('fetchTrace').withExactArgs(id);
|
||||||
|
jaegerApiActions.fetchTrace(id);
|
||||||
|
expect(() => mock.verify()).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/FETCH_TRACE should return the promise', () => {
|
||||||
|
const { payload } = jaegerApiActions.fetchTrace(id);
|
||||||
|
expect(isPromise(payload)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/FETCH_TRACE should attach the id as meta', () => {
|
||||||
|
const { meta } = jaegerApiActions.fetchTrace(id);
|
||||||
|
expect(meta.id).toBe(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/FETCH_MULTIPLE_TRACES should fetch traces by ids', () => {
|
||||||
|
mock.expects('searchTraces').withExactArgs(sinon.match.has('traceID', ids));
|
||||||
|
jaegerApiActions.fetchMultipleTraces(ids);
|
||||||
|
expect(() => mock.verify()).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/FETCH_MULTIPLE_TRACES should return the promise', () => {
|
||||||
|
const { payload } = jaegerApiActions.fetchMultipleTraces(ids);
|
||||||
|
expect(isPromise(payload)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/FETCH_MULTIPLE_TRACES should attach the ids as meta', () => {
|
||||||
|
const { meta } = jaegerApiActions.fetchMultipleTraces(ids);
|
||||||
|
expect(meta.ids).toBe(ids);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/ARCHIVE_TRACE should archive the trace by id', () => {
|
||||||
|
mock.expects('archiveTrace').withExactArgs(id);
|
||||||
|
jaegerApiActions.archiveTrace(id);
|
||||||
|
expect(() => mock.verify()).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/ARCHIVE_TRACE should return the promise', () => {
|
||||||
|
const { payload } = jaegerApiActions.archiveTrace(id);
|
||||||
|
expect(isPromise(payload)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/ARCHIVE_TRACE should attach the id as meta', () => {
|
||||||
|
const { meta } = jaegerApiActions.archiveTrace(id);
|
||||||
|
expect(meta.id).toBe(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/SEARCH_TRACES should fetch the trace by id', () => {
|
||||||
|
mock.expects('searchTraces').withExactArgs(query);
|
||||||
|
jaegerApiActions.searchTraces(query);
|
||||||
|
expect(() => mock.verify()).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/SEARCH_TRACES should return the promise', () => {
|
||||||
|
const { payload } = jaegerApiActions.searchTraces(query);
|
||||||
|
expect(isPromise(payload)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/SEARCH_TRACES should attach the query as meta', () => {
|
||||||
|
const { meta } = jaegerApiActions.searchTraces(query);
|
||||||
|
expect(meta.query).toEqual(query);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/FETCH_SERVICES should return a promise', () => {
|
||||||
|
const { payload } = jaegerApiActions.fetchServices();
|
||||||
|
expect(isPromise(payload)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/FETCH_SERVICE_OPERATIONS should call the JaegerAPI', () => {
|
||||||
|
const called = mock
|
||||||
|
.expects('fetchServiceOperations')
|
||||||
|
.once()
|
||||||
|
.withExactArgs('service');
|
||||||
|
jaegerApiActions.fetchServiceOperations('service');
|
||||||
|
expect(called.verify()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/FETCH_SERVICE_SERVER_OP should call the JaegerAPI', () => {
|
||||||
|
const called = mock
|
||||||
|
.expects('fetchServiceServerOps')
|
||||||
|
.once()
|
||||||
|
.withExactArgs('service');
|
||||||
|
jaegerApiActions.fetchServiceServerOps('service');
|
||||||
|
expect(called.verify()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/FETCH_DEEP_DEPENDENCY_GRAPH should fetch the graph by params', () => {
|
||||||
|
mock.expects('fetchDeepDependencyGraph').withExactArgs(query);
|
||||||
|
jaegerApiActions.fetchDeepDependencyGraph(query);
|
||||||
|
expect(() => mock.verify()).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/FETCH_DEEP_DEPENDENCY_GRAPH should return the promise', () => {
|
||||||
|
const { payload } = jaegerApiActions.fetchDeepDependencyGraph(query);
|
||||||
|
expect(isPromise(payload)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/FETCH_DEEP_DEPENDENCY_GRAPH should attach the query as meta', () => {
|
||||||
|
const { meta } = jaegerApiActions.fetchDeepDependencyGraph(query);
|
||||||
|
expect(meta.query).toEqual(query);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('@JAEGER_API/FETCH_DEPENDENCIES should call the JaegerAPI', () => {
|
||||||
|
const called = mock.expects('fetchDependencies').once();
|
||||||
|
jaegerApiActions.fetchDependencies();
|
||||||
|
expect(called.verify()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,111 @@
|
|||||||
|
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import fetch from 'isomorphic-fetch';
|
||||||
|
import moment from 'moment';
|
||||||
|
import queryString from 'query-string';
|
||||||
|
|
||||||
|
import prefixUrl from '../utils/prefix-url';
|
||||||
|
|
||||||
|
// export for tests
|
||||||
|
export function getMessageFromError(errData, status) {
|
||||||
|
if (errData.code != null && errData.msg != null) {
|
||||||
|
if (errData.code === status) {
|
||||||
|
return errData.msg;
|
||||||
|
}
|
||||||
|
return `${errData.code} - ${errData.msg}`;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(errData);
|
||||||
|
} catch (_) {
|
||||||
|
return String(errData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJSON(url, options = {}) {
|
||||||
|
const { query = null, ...init } = options;
|
||||||
|
init.credentials = 'same-origin';
|
||||||
|
const queryStr = query ? `?${queryString.stringify(query)}` : '';
|
||||||
|
return fetch(`${url}${queryStr}`, init).then(response => {
|
||||||
|
if (response.status < 400) {
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
return response.text().then(bodyText => {
|
||||||
|
let data;
|
||||||
|
let bodyTextFmt;
|
||||||
|
let errorMessage;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(bodyText);
|
||||||
|
bodyTextFmt = JSON.stringify(data, null, 2);
|
||||||
|
} catch (_) {
|
||||||
|
data = null;
|
||||||
|
bodyTextFmt = null;
|
||||||
|
}
|
||||||
|
if (data && Array.isArray(data.errors) && data.errors.length) {
|
||||||
|
errorMessage = data.errors.map(err => getMessageFromError(err, response.status)).join('; ');
|
||||||
|
} else {
|
||||||
|
errorMessage = bodyText || `${response.status} - ${response.statusText}`;
|
||||||
|
}
|
||||||
|
if (typeof errorMessage === 'string') {
|
||||||
|
errorMessage = errorMessage.trim();
|
||||||
|
}
|
||||||
|
const error = new Error(`HTTP Error: ${errorMessage}`);
|
||||||
|
error.httpStatus = response.status;
|
||||||
|
error.httpStatusText = response.statusText;
|
||||||
|
error.httpBody = bodyTextFmt || bodyText;
|
||||||
|
error.httpUrl = url;
|
||||||
|
error.httpQuery = typeof query === 'string' ? query : queryString.stringify(query);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_API_ROOT = prefixUrl('/api/');
|
||||||
|
export const ANALYTICS_ROOT = prefixUrl('/analytics/');
|
||||||
|
export const DEFAULT_DEPENDENCY_LOOKBACK = moment.duration(1, 'weeks').asMilliseconds();
|
||||||
|
|
||||||
|
const JaegerAPI = {
|
||||||
|
apiRoot: DEFAULT_API_ROOT,
|
||||||
|
archiveTrace(id) {
|
||||||
|
return getJSON(`${this.apiRoot}archive/${id}`, { method: 'POST' });
|
||||||
|
},
|
||||||
|
fetchQualityMetrics(service, lookback) {
|
||||||
|
return getJSON(`/qualitymetrics-v2`, { query: { service, lookback } });
|
||||||
|
},
|
||||||
|
fetchDecoration(url) {
|
||||||
|
return getJSON(url);
|
||||||
|
},
|
||||||
|
fetchDeepDependencyGraph(query) {
|
||||||
|
return getJSON(`${ANALYTICS_ROOT}v1/dependencies`, { query });
|
||||||
|
},
|
||||||
|
fetchDependencies(endTs = new Date().getTime(), lookback = DEFAULT_DEPENDENCY_LOOKBACK) {
|
||||||
|
return getJSON(`${this.apiRoot}dependencies`, { query: { endTs, lookback } });
|
||||||
|
},
|
||||||
|
fetchServiceOperations(serviceName) {
|
||||||
|
return getJSON(`${this.apiRoot}services/${encodeURIComponent(serviceName)}/operations`);
|
||||||
|
},
|
||||||
|
fetchServiceServerOps(service) {
|
||||||
|
return getJSON(`${this.apiRoot}operations`, {
|
||||||
|
query: {
|
||||||
|
service,
|
||||||
|
spanKind: 'server',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fetchTrace(id) {
|
||||||
|
return getJSON(`${this.apiRoot}traces/${id}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default JaegerAPI;
|
@ -0,0 +1,183 @@
|
|||||||
|
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
/* eslint-disable import/first */
|
||||||
|
jest.mock('isomorphic-fetch', () =>
|
||||||
|
jest.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
status: 200,
|
||||||
|
data: () => Promise.resolve({ data: null }),
|
||||||
|
json: () => Promise.resolve({ data: null }),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
import fetchMock from 'isomorphic-fetch';
|
||||||
|
import queryString from 'query-string';
|
||||||
|
|
||||||
|
import traceGenerator from '../demo/trace-generators';
|
||||||
|
import JaegerAPI, {
|
||||||
|
getMessageFromError,
|
||||||
|
DEFAULT_API_ROOT,
|
||||||
|
DEFAULT_DEPENDENCY_LOOKBACK,
|
||||||
|
ANALYTICS_ROOT,
|
||||||
|
} from './jaeger';
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
credentials: 'same-origin',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('archiveTrace', () => {
|
||||||
|
it('POSTs the specified id', () => {
|
||||||
|
JaegerAPI.archiveTrace('trace-id');
|
||||||
|
expect(fetchMock).toHaveBeenLastCalledWith(`${DEFAULT_API_ROOT}archive/trace-id`, {
|
||||||
|
...defaultOptions,
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchDeepDependencyGraph', () => {
|
||||||
|
it('GETs the specified query', () => {
|
||||||
|
const query = { service: 'serviceName', start: 400, end: 800 };
|
||||||
|
JaegerAPI.fetchDeepDependencyGraph(query);
|
||||||
|
expect(fetchMock).toHaveBeenLastCalledWith(
|
||||||
|
`${ANALYTICS_ROOT}v1/dependencies?${queryString.stringify(query)}`,
|
||||||
|
defaultOptions
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchDependencies', () => {
|
||||||
|
it('GETs the specified query', () => {
|
||||||
|
const endTs = 'end time stamp';
|
||||||
|
const lookback = 'test lookback';
|
||||||
|
JaegerAPI.fetchDependencies(endTs, lookback);
|
||||||
|
expect(fetchMock).toHaveBeenLastCalledWith(
|
||||||
|
`${DEFAULT_API_ROOT}dependencies?${queryString.stringify({ endTs, lookback })}`,
|
||||||
|
defaultOptions
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has default query values', () => {
|
||||||
|
JaegerAPI.fetchDependencies();
|
||||||
|
expect(fetchMock).toHaveBeenLastCalledWith(
|
||||||
|
expect.stringMatching(
|
||||||
|
new RegExp(`${DEFAULT_API_ROOT}dependencies\\?endTs=\\d+&lookback=${DEFAULT_DEPENDENCY_LOOKBACK}`)
|
||||||
|
),
|
||||||
|
defaultOptions
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchServiceServerOps', () => {
|
||||||
|
it('GETs the specified query', () => {
|
||||||
|
const service = 'serviceName';
|
||||||
|
const query = { service, spanKind: 'server' };
|
||||||
|
JaegerAPI.fetchServiceServerOps(service);
|
||||||
|
expect(fetchMock).toHaveBeenLastCalledWith(
|
||||||
|
`${DEFAULT_API_ROOT}operations?${queryString.stringify(query)}`,
|
||||||
|
defaultOptions
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchTrace', () => {
|
||||||
|
const generatedTraces = traceGenerator.traces({ numberOfTraces: 5 });
|
||||||
|
|
||||||
|
it('fetchTrace() should fetch with the id', () => {
|
||||||
|
JaegerAPI.fetchTrace('trace-id');
|
||||||
|
expect(fetchMock).toHaveBeenLastCalledWith(`${DEFAULT_API_ROOT}traces/trace-id`, defaultOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetchTrace() should resolve the whole response', async () => {
|
||||||
|
fetchMock.mockReturnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
status: 200,
|
||||||
|
json: () => Promise.resolve({ data: generatedTraces }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const resp = await JaegerAPI.fetchTrace('trace-id');
|
||||||
|
expect(resp.data).toBe(generatedTraces);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetchTrace() throws an error on a >= 400 status code', done => {
|
||||||
|
const status = 400;
|
||||||
|
const statusText = 'some-status';
|
||||||
|
const msg = 'some-message';
|
||||||
|
const errorData = { errors: [{ msg, code: status }] };
|
||||||
|
|
||||||
|
fetchMock.mockReturnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
status,
|
||||||
|
statusText,
|
||||||
|
text: () => Promise.resolve(JSON.stringify(errorData)),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
JaegerAPI.fetchTrace('trace-id').catch(err => {
|
||||||
|
expect(err.message).toMatch(msg);
|
||||||
|
expect(err.httpStatus).toBe(status);
|
||||||
|
expect(err.httpStatusText).toBe(statusText);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetchTrace() throws an useful error derived from a text payload', done => {
|
||||||
|
const status = 400;
|
||||||
|
const statusText = 'some-status';
|
||||||
|
const errorData = 'this is some error message';
|
||||||
|
|
||||||
|
fetchMock.mockReturnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
status,
|
||||||
|
statusText,
|
||||||
|
text: () => Promise.resolve(errorData),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
JaegerAPI.fetchTrace('trace-id').catch(err => {
|
||||||
|
expect(err.message).toMatch(errorData);
|
||||||
|
expect(err.httpStatus).toBe(status);
|
||||||
|
expect(err.httpStatusText).toBe(statusText);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMessageFromError()', () => {
|
||||||
|
describe('{ code, msg } error data', () => {
|
||||||
|
const data = { code: 1, msg: 'some-message' };
|
||||||
|
|
||||||
|
it('ignores code if it is the same as `status` arg', () => {
|
||||||
|
expect(getMessageFromError(data, 1)).toBe(data.msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns`$code - $msg` when code is novel', () => {
|
||||||
|
const rv = getMessageFromError(data, -1);
|
||||||
|
expect(rv).toBe(`${data.code} - ${data.msg}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('other data formats', () => {
|
||||||
|
it('stringifies the value, when possible', () => {
|
||||||
|
const data = ['abc'];
|
||||||
|
expect(getMessageFromError(data)).toBe(JSON.stringify(data));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the string, otherwise', () => {
|
||||||
|
const data = {};
|
||||||
|
data.data = data;
|
||||||
|
expect(getMessageFromError(data)).toBe(String(data));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,82 @@
|
|||||||
|
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { createActions, handleActions, ActionFunctionAny } from 'redux-actions';
|
||||||
|
|
||||||
|
import { archiveTrace } from '../../../actions/jaeger-api';
|
||||||
|
import { ApiError } from '../../../types/api-error';
|
||||||
|
import { TracesArchive } from '../../../types/archive';
|
||||||
|
import generateActionTypes from '../../../utils/generate-action-types';
|
||||||
|
|
||||||
|
type ArchiveAction = {
|
||||||
|
meta: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
payload?: ApiError | string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState: TracesArchive = {};
|
||||||
|
|
||||||
|
const actionTypes = generateActionTypes('@jaeger-ui/archive-trace', ['ACKNOWLEDGE']);
|
||||||
|
|
||||||
|
const fullActions = createActions({
|
||||||
|
[actionTypes.ACKNOWLEDGE]: traceID => traceID,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const actions: { [actionType: string]: ActionFunctionAny<any> } = {
|
||||||
|
...(fullActions as any).jaegerUi.archiveTrace,
|
||||||
|
archiveTrace,
|
||||||
|
};
|
||||||
|
|
||||||
|
function acknowledge(state: TracesArchive, { payload }: ArchiveAction) {
|
||||||
|
const traceID = typeof payload === 'string' ? payload : null;
|
||||||
|
if (!traceID) {
|
||||||
|
// make flow happy
|
||||||
|
throw new Error('Invalid state, missing traceID for archive acknowledge');
|
||||||
|
}
|
||||||
|
const traceArchive = state[traceID];
|
||||||
|
if (traceArchive && traceArchive.isLoading) {
|
||||||
|
// acknowledgement during loading is invalid (should not happen)
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
const next = { ...traceArchive, isAcknowledged: true };
|
||||||
|
return { ...state, [traceID]: next };
|
||||||
|
}
|
||||||
|
|
||||||
|
function archiveStarted(state: TracesArchive, { meta }: ArchiveAction) {
|
||||||
|
return { ...state, [meta.id]: { isLoading: true } };
|
||||||
|
}
|
||||||
|
|
||||||
|
function archiveDone(state: TracesArchive, { meta }: ArchiveAction) {
|
||||||
|
return { ...state, [meta.id]: { isArchived: true, isAcknowledged: false } };
|
||||||
|
}
|
||||||
|
|
||||||
|
function archiveErred(state: TracesArchive, { meta, payload }: ArchiveAction) {
|
||||||
|
if (!payload) {
|
||||||
|
// make flow happy
|
||||||
|
throw new Error('Invalid state, missing API error details');
|
||||||
|
}
|
||||||
|
const traceArchive = { error: payload, isArchived: false, isError: true, isAcknowledged: false };
|
||||||
|
return { ...state, [meta.id]: traceArchive };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handleActions(
|
||||||
|
{
|
||||||
|
[actionTypes.ACKNOWLEDGE]: acknowledge,
|
||||||
|
[`${archiveTrace}_PENDING`]: archiveStarted,
|
||||||
|
[`${archiveTrace}_FULFILLED`]: archiveDone,
|
||||||
|
[`${archiveTrace}_REJECTED`]: archiveErred,
|
||||||
|
},
|
||||||
|
initialState
|
||||||
|
);
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.ArchiveNotifier--errorNotification {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
width: 650px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ArchiveNotifier--errorIcon,
|
||||||
|
.ArchiveNotifier--doneIcon {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ArchiveNotifier--errorIcon {
|
||||||
|
color: #c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ArchiveNotifier--doneIcon {
|
||||||
|
color: teal;
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { notification } from 'antd';
|
||||||
|
import { LoadingOutlined,ClockCircleOutlined} from '@ant-design/icons';
|
||||||
|
|
||||||
|
import ErrorMessage from '../../common/ErrorMessage';
|
||||||
|
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const ENotifiedState = {
|
||||||
|
Progress : 'ENotifiedState.Progress',
|
||||||
|
Outcome : 'ENotifiedState.Outcome',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function getNextNotifiedState(props) {
|
||||||
|
const { archivedState } = props;
|
||||||
|
if (!archivedState) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (archivedState.isLoading) {
|
||||||
|
return ENotifiedState.Progress;
|
||||||
|
}
|
||||||
|
return archivedState.isAcknowledged ? null : ENotifiedState.Outcome;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNotification(oldState,nextState, props) {
|
||||||
|
if (oldState === nextState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (oldState) {
|
||||||
|
notification.close(oldState);
|
||||||
|
}
|
||||||
|
if (nextState === ENotifiedState.Progress) {
|
||||||
|
notification.info({
|
||||||
|
key: ENotifiedState.Progress,
|
||||||
|
description: null,
|
||||||
|
duration: 0,
|
||||||
|
icon: <LoadingOutlined />,
|
||||||
|
message: 'Archiving trace...',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { acknowledge, archivedState } = props;
|
||||||
|
if (nextState === ENotifiedState.Outcome) {
|
||||||
|
if (archivedState && archivedState.error) {
|
||||||
|
const error = typeof archivedState.error === 'string' ? archivedState.error : archivedState.error;
|
||||||
|
notification.warn({
|
||||||
|
key: ENotifiedState.Outcome,
|
||||||
|
className: 'ArchiveNotifier--errorNotification',
|
||||||
|
message: <ErrorMessage.Message error={error} wrap />,
|
||||||
|
description: <ErrorMessage.Details error={error} wrap />,
|
||||||
|
duration: null,
|
||||||
|
icon: <ClockCircleOutlined className="ArchiveNotifier--errorIcon" />,
|
||||||
|
onClose: acknowledge,
|
||||||
|
});
|
||||||
|
} else if (archivedState && archivedState.isArchived) {
|
||||||
|
notification.success({
|
||||||
|
key: ENotifiedState.Outcome,
|
||||||
|
description: null,
|
||||||
|
duration: null,
|
||||||
|
icon: <ClockCircleOutlined className="ArchiveNotifier--doneIcon" />,
|
||||||
|
message: 'This trace has been archived.',
|
||||||
|
onClose: acknowledge,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error('Unexpected condition');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processProps(notifiedState, props) {
|
||||||
|
const nxNotifiedState = getNextNotifiedState(props);
|
||||||
|
updateNotification(notifiedState, nxNotifiedState, props);
|
||||||
|
return nxNotifiedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ArchiveNotifier extends React.PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
const notifiedState = processProps(null, props);
|
||||||
|
this.state = { notifiedState };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
const notifiedState = processProps(this.state.notifiedState, nextProps);
|
||||||
|
if (this.state.notifiedState !== notifiedState) {
|
||||||
|
this.setState({ notifiedState });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
const { notifiedState } = this.state;
|
||||||
|
if (notifiedState) {
|
||||||
|
notification.close(notifiedState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,286 @@
|
|||||||
|
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
/* eslint-disable import/first */
|
||||||
|
jest.mock('./scroll-page');
|
||||||
|
|
||||||
|
import { scrollBy, scrollTo } from './scroll-page';
|
||||||
|
import ScrollManager from './ScrollManager';
|
||||||
|
|
||||||
|
const SPAN_HEIGHT = 2;
|
||||||
|
|
||||||
|
function getTrace() {
|
||||||
|
const spans = [];
|
||||||
|
const trace = {
|
||||||
|
spans,
|
||||||
|
duration: 2000,
|
||||||
|
startTime: 1000,
|
||||||
|
};
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
spans.push({ duration: 1, startTime: 1000, spanID: i + 1 });
|
||||||
|
}
|
||||||
|
return trace;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAccessors() {
|
||||||
|
return {
|
||||||
|
getViewRange: jest.fn(() => [0, 1]),
|
||||||
|
getSearchedSpanIDs: jest.fn(),
|
||||||
|
getCollapsedChildren: jest.fn(),
|
||||||
|
getViewHeight: jest.fn(() => SPAN_HEIGHT * 2),
|
||||||
|
getBottomRowIndexVisible: jest.fn(),
|
||||||
|
getTopRowIndexVisible: jest.fn(),
|
||||||
|
getRowPosition: jest.fn(),
|
||||||
|
mapRowIndexToSpanIndex: jest.fn(n => n),
|
||||||
|
mapSpanIndexToRowIndex: jest.fn(n => n),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ScrollManager', () => {
|
||||||
|
let trace;
|
||||||
|
let accessors;
|
||||||
|
let manager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scrollBy.mockReset();
|
||||||
|
scrollTo.mockReset();
|
||||||
|
trace = getTrace();
|
||||||
|
accessors = getAccessors();
|
||||||
|
manager = new ScrollManager(trace, { scrollBy, scrollTo });
|
||||||
|
manager.setAccessors(accessors);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saves the accessors', () => {
|
||||||
|
const n = Math.random();
|
||||||
|
manager.setAccessors(n);
|
||||||
|
expect(manager._accessors).toBe(n);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('_scrollPast()', () => {
|
||||||
|
it('throws if accessors is not set', () => {
|
||||||
|
manager.setAccessors(null);
|
||||||
|
expect(manager._scrollPast).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is a noop if an invalid rowPosition is returned by the accessors', () => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
const oldWarn = console.warn;
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn = () => {};
|
||||||
|
manager._scrollPast(null, null);
|
||||||
|
expect(accessors.getRowPosition.mock.calls.length).toBe(1);
|
||||||
|
expect(accessors.getViewHeight.mock.calls.length).toBe(0);
|
||||||
|
expect(scrollTo.mock.calls.length).toBe(0);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn = oldWarn;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scrolls up with direction is `-1`', () => {
|
||||||
|
const y = 10;
|
||||||
|
const expectTo = y - 0.5 * accessors.getViewHeight();
|
||||||
|
accessors.getRowPosition.mockReturnValue({ y, height: SPAN_HEIGHT });
|
||||||
|
manager._scrollPast(NaN, -1);
|
||||||
|
expect(scrollTo.mock.calls).toEqual([[expectTo]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scrolls down with direction `1`', () => {
|
||||||
|
const y = 10;
|
||||||
|
const vh = accessors.getViewHeight();
|
||||||
|
const expectTo = y + SPAN_HEIGHT - 0.5 * vh;
|
||||||
|
accessors.getRowPosition.mockReturnValue({ y, height: SPAN_HEIGHT });
|
||||||
|
manager._scrollPast(NaN, 1);
|
||||||
|
expect(scrollTo.mock.calls).toEqual([[expectTo]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('_scrollToVisibleSpan()', () => {
|
||||||
|
function getRefs(spanID) {
|
||||||
|
return [{ refType: 'CHILD_OF', spanID }];
|
||||||
|
}
|
||||||
|
let scrollPastMock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scrollPastMock = jest.fn();
|
||||||
|
manager._scrollPast = scrollPastMock;
|
||||||
|
});
|
||||||
|
it('throws if accessors is not set', () => {
|
||||||
|
manager.setAccessors(null);
|
||||||
|
expect(manager._scrollToVisibleSpan).toThrow();
|
||||||
|
});
|
||||||
|
it('exits if the trace is not set', () => {
|
||||||
|
manager.setTrace(null);
|
||||||
|
manager._scrollToVisibleSpan();
|
||||||
|
expect(scrollPastMock.mock.calls.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if already at the boundary', () => {
|
||||||
|
accessors.getTopRowIndexVisible.mockReturnValue(0);
|
||||||
|
accessors.getBottomRowIndexVisible.mockReturnValue(trace.spans.length - 1);
|
||||||
|
manager._scrollToVisibleSpan(-1);
|
||||||
|
expect(scrollPastMock.mock.calls.length).toBe(0);
|
||||||
|
manager._scrollToVisibleSpan(1);
|
||||||
|
expect(scrollPastMock.mock.calls.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('centers the current top or bottom span', () => {
|
||||||
|
accessors.getTopRowIndexVisible.mockReturnValue(5);
|
||||||
|
accessors.getBottomRowIndexVisible.mockReturnValue(5);
|
||||||
|
manager._scrollToVisibleSpan(-1);
|
||||||
|
expect(scrollPastMock).lastCalledWith(5, -1);
|
||||||
|
manager._scrollToVisibleSpan(1);
|
||||||
|
expect(scrollPastMock).lastCalledWith(5, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips spans that are out of view', () => {
|
||||||
|
trace.spans[4].startTime = trace.startTime + trace.duration * 0.5;
|
||||||
|
accessors.getViewRange = () => [0.4, 0.6];
|
||||||
|
accessors.getTopRowIndexVisible.mockReturnValue(trace.spans.length - 1);
|
||||||
|
accessors.getBottomRowIndexVisible.mockReturnValue(0);
|
||||||
|
manager._scrollToVisibleSpan(1);
|
||||||
|
expect(scrollPastMock).lastCalledWith(4, 1);
|
||||||
|
manager._scrollToVisibleSpan(-1);
|
||||||
|
expect(scrollPastMock).lastCalledWith(4, -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips spans that do not match the text search', () => {
|
||||||
|
accessors.getTopRowIndexVisible.mockReturnValue(trace.spans.length - 1);
|
||||||
|
accessors.getBottomRowIndexVisible.mockReturnValue(0);
|
||||||
|
accessors.getSearchedSpanIDs = () => new Set([trace.spans[4].spanID]);
|
||||||
|
manager._scrollToVisibleSpan(1);
|
||||||
|
expect(scrollPastMock).lastCalledWith(4, 1);
|
||||||
|
manager._scrollToVisibleSpan(-1);
|
||||||
|
expect(scrollPastMock).lastCalledWith(4, -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scrolls to boundary when scrolling away from closest spanID in findMatches', () => {
|
||||||
|
const closetFindMatchesSpanID = 4;
|
||||||
|
accessors.getTopRowIndexVisible.mockReturnValue(closetFindMatchesSpanID - 1);
|
||||||
|
accessors.getBottomRowIndexVisible.mockReturnValue(closetFindMatchesSpanID + 1);
|
||||||
|
accessors.getSearchedSpanIDs = () => new Set([trace.spans[closetFindMatchesSpanID].spanID]);
|
||||||
|
|
||||||
|
manager._scrollToVisibleSpan(1);
|
||||||
|
expect(scrollPastMock).lastCalledWith(trace.spans.length - 1, 1);
|
||||||
|
|
||||||
|
manager._scrollToVisibleSpan(-1);
|
||||||
|
expect(scrollPastMock).lastCalledWith(0, -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scrolls to last visible row when boundary is hidden', () => {
|
||||||
|
const parentOfLastRowWithHiddenChildrenIndex = trace.spans.length - 2;
|
||||||
|
accessors.getBottomRowIndexVisible.mockReturnValue(0);
|
||||||
|
accessors.getCollapsedChildren = () =>
|
||||||
|
new Set([trace.spans[parentOfLastRowWithHiddenChildrenIndex].spanID]);
|
||||||
|
accessors.getSearchedSpanIDs = () => new Set([trace.spans[0].spanID]);
|
||||||
|
trace.spans[trace.spans.length - 1].references = getRefs(
|
||||||
|
trace.spans[parentOfLastRowWithHiddenChildrenIndex].spanID
|
||||||
|
);
|
||||||
|
|
||||||
|
manager._scrollToVisibleSpan(1);
|
||||||
|
expect(scrollPastMock).lastCalledWith(parentOfLastRowWithHiddenChildrenIndex, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scrollToNextVisibleSpan() and scrollToPrevVisibleSpan()', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// change spans so 0 and 4 are top-level and their children are collapsed
|
||||||
|
const spans = trace.spans;
|
||||||
|
let parentID;
|
||||||
|
for (let i = 0; i < spans.length; i++) {
|
||||||
|
switch (i) {
|
||||||
|
case 0:
|
||||||
|
case 4:
|
||||||
|
parentID = spans[i].spanID;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
spans[i].references = getRefs(parentID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// set which spans are "in-view" and which have collapsed children
|
||||||
|
accessors.getTopRowIndexVisible.mockReturnValue(trace.spans.length - 1);
|
||||||
|
accessors.getBottomRowIndexVisible.mockReturnValue(0);
|
||||||
|
accessors.getCollapsedChildren.mockReturnValue(new Set([spans[0].spanID, spans[4].spanID]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skips spans that are hidden because their parent is collapsed', () => {
|
||||||
|
manager.scrollToNextVisibleSpan();
|
||||||
|
expect(scrollPastMock).lastCalledWith(4, 1);
|
||||||
|
manager.scrollToPrevVisibleSpan();
|
||||||
|
expect(scrollPastMock).lastCalledWith(4, -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores references with unknown types', () => {
|
||||||
|
// modify spans[2] so that it has an unknown refType
|
||||||
|
const spans = trace.spans;
|
||||||
|
spans[2].references = [{ refType: 'OTHER' }];
|
||||||
|
manager.scrollToNextVisibleSpan();
|
||||||
|
expect(scrollPastMock).lastCalledWith(2, 1);
|
||||||
|
manager.scrollToPrevVisibleSpan();
|
||||||
|
expect(scrollPastMock).lastCalledWith(4, -1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles more than one level of ancestry', () => {
|
||||||
|
// modify spans[2] so that it has an unknown refType
|
||||||
|
const spans = trace.spans;
|
||||||
|
spans[2].references = getRefs(spans[1].spanID);
|
||||||
|
manager.scrollToNextVisibleSpan();
|
||||||
|
expect(scrollPastMock).lastCalledWith(4, 1);
|
||||||
|
manager.scrollToPrevVisibleSpan();
|
||||||
|
expect(scrollPastMock).lastCalledWith(4, -1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scrollToFirstVisibleSpan', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(manager, '_scrollToVisibleSpan').mockImplementationOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls _scrollToVisibleSpan searching downwards from first span', () => {
|
||||||
|
manager.scrollToFirstVisibleSpan();
|
||||||
|
expect(manager._scrollToVisibleSpan).toHaveBeenCalledWith(1, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scrollPageDown() and scrollPageUp()', () => {
|
||||||
|
it('scrolls by +/~ viewHeight when invoked', () => {
|
||||||
|
manager.scrollPageDown();
|
||||||
|
expect(scrollBy).lastCalledWith(0.95 * accessors.getViewHeight(), true);
|
||||||
|
manager.scrollPageUp();
|
||||||
|
expect(scrollBy).lastCalledWith(-0.95 * accessors.getViewHeight(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is a no-op if _accessors or _scroller is not defined', () => {
|
||||||
|
manager._accessors = null;
|
||||||
|
manager.scrollPageDown();
|
||||||
|
manager.scrollPageUp();
|
||||||
|
expect(scrollBy.mock.calls.length).toBe(0);
|
||||||
|
manager._accessors = accessors;
|
||||||
|
manager._scroller = null;
|
||||||
|
manager.scrollPageDown();
|
||||||
|
manager.scrollPageUp();
|
||||||
|
expect(scrollBy.mock.calls.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('destroy()', () => {
|
||||||
|
it('disposes', () => {
|
||||||
|
expect(manager._trace).toBeDefined();
|
||||||
|
expect(manager._accessors).toBeDefined();
|
||||||
|
expect(manager._scroller).toBeDefined();
|
||||||
|
manager.destroy();
|
||||||
|
expect(manager._trace).not.toBeDefined();
|
||||||
|
expect(manager._accessors).not.toBeDefined();
|
||||||
|
expect(manager._scroller).not.toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,274 @@
|
|||||||
|
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { TNil } from '../../types';
|
||||||
|
import { Span, SpanReference, Trace } from '../../types/trace';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `Accessors` is necessary because `ScrollManager` needs to be created by
|
||||||
|
* `TracePage` so it can be passed into the keyboard shortcut manager. But,
|
||||||
|
* `ScrollManager` needs to know about the state of `ListView` and `Positions`,
|
||||||
|
* which are very low-level. And, storing their state info in redux or
|
||||||
|
* `TracePage#state` would be inefficient because the state info only rarely
|
||||||
|
* needs to be accessed (when a keyboard shortcut is triggered). `Accessors`
|
||||||
|
* allows that state info to be accessed in a loosely coupled fashion on an
|
||||||
|
* as-needed basis.
|
||||||
|
*/
|
||||||
|
export type Accessors = {
|
||||||
|
getViewRange: () => [number, number];
|
||||||
|
getSearchedSpanIDs: () => Set<string> | TNil;
|
||||||
|
getCollapsedChildren: () => Set<string> | TNil;
|
||||||
|
getViewHeight: () => number;
|
||||||
|
getBottomRowIndexVisible: () => number;
|
||||||
|
getTopRowIndexVisible: () => number;
|
||||||
|
getRowPosition: (rowIndex: number) => { height: number; y: number };
|
||||||
|
mapRowIndexToSpanIndex: (rowIndex: number) => number;
|
||||||
|
mapSpanIndexToRowIndex: (spanIndex: number) => number;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IScroller {
|
||||||
|
scrollTo: (rowIndex: number) => void;
|
||||||
|
// TODO arg names throughout
|
||||||
|
scrollBy: (rowIndex: number, opt?: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `{ isHidden: true, ... }` if one of the parents of `span` is
|
||||||
|
* collapsed, e.g. has children hidden.
|
||||||
|
*
|
||||||
|
* @param {Span} span The Span to check for.
|
||||||
|
* @param {Set<string>} childrenAreHidden The set of Spans known to have hidden
|
||||||
|
* children, either because it is
|
||||||
|
* collapsed or has a collapsed parent.
|
||||||
|
* @param {Map<string, Span | TNil} spansMap Mapping from spanID to Span.
|
||||||
|
* @returns {{ isHidden: boolean, parentIds: Set<string> }}
|
||||||
|
*/
|
||||||
|
function isSpanHidden(span: Span, childrenAreHidden: Set<string>, spansMap: Map<string, Span | TNil>) {
|
||||||
|
const parentIDs = new Set<string>();
|
||||||
|
let { references }: { references: SpanReference[] | TNil } = span;
|
||||||
|
let parentID: undefined | string;
|
||||||
|
const checkRef = (ref: SpanReference) => {
|
||||||
|
if (ref.refType === 'CHILD_OF' || ref.refType === 'FOLLOWS_FROM') {
|
||||||
|
parentID = ref.spanID;
|
||||||
|
parentIDs.add(parentID);
|
||||||
|
return childrenAreHidden.has(parentID);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
while (Array.isArray(references) && references.length) {
|
||||||
|
const isHidden = references.some(checkRef);
|
||||||
|
if (isHidden) {
|
||||||
|
return { isHidden, parentIDs };
|
||||||
|
}
|
||||||
|
if (!parentID) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const parent = spansMap.get(parentID);
|
||||||
|
parentID = undefined;
|
||||||
|
references = parent && parent.references;
|
||||||
|
}
|
||||||
|
return { parentIDs, isHidden: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ScrollManager is intended for scrolling the TracePage. Has two modes, paging
|
||||||
|
* and scrolling to the previous or next visible span.
|
||||||
|
*/
|
||||||
|
export default class ScrollManager {
|
||||||
|
_trace: Trace | TNil;
|
||||||
|
_scroller: IScroller;
|
||||||
|
_accessors: Accessors | TNil;
|
||||||
|
|
||||||
|
constructor(trace: Trace | TNil, scroller: IScroller) {
|
||||||
|
this._trace = trace;
|
||||||
|
this._scroller = scroller;
|
||||||
|
this._accessors = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
_scrollPast(rowIndex: number, direction: 1 | -1) {
|
||||||
|
const xrs = this._accessors;
|
||||||
|
/* istanbul ignore next */
|
||||||
|
if (!xrs) {
|
||||||
|
throw new Error('Accessors not set');
|
||||||
|
}
|
||||||
|
const isUp = direction < 0;
|
||||||
|
const position = xrs.getRowPosition(rowIndex);
|
||||||
|
if (!position) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('Invalid row index');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let { y } = position;
|
||||||
|
const vh = xrs.getViewHeight();
|
||||||
|
if (!isUp) {
|
||||||
|
y += position.height;
|
||||||
|
// scrollTop is based on the top of the window
|
||||||
|
y -= vh;
|
||||||
|
}
|
||||||
|
y += direction * 0.5 * vh;
|
||||||
|
this._scroller.scrollTo(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
_scrollToVisibleSpan(direction: 1 | -1, startRow?: number) {
|
||||||
|
const xrs = this._accessors;
|
||||||
|
/* istanbul ignore next */
|
||||||
|
if (!xrs) {
|
||||||
|
throw new Error('Accessors not set');
|
||||||
|
}
|
||||||
|
if (!this._trace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { duration, spans, startTime: traceStartTime } = this._trace;
|
||||||
|
const isUp = direction < 0;
|
||||||
|
let boundaryRow: number;
|
||||||
|
if (startRow != null) {
|
||||||
|
boundaryRow = startRow;
|
||||||
|
} else if (isUp) {
|
||||||
|
boundaryRow = xrs.getTopRowIndexVisible();
|
||||||
|
} else {
|
||||||
|
boundaryRow = xrs.getBottomRowIndexVisible();
|
||||||
|
}
|
||||||
|
const spanIndex = xrs.mapRowIndexToSpanIndex(boundaryRow);
|
||||||
|
if ((spanIndex === 0 && isUp) || (spanIndex === spans.length - 1 && !isUp)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// fullViewSpanIndex is one row inside the view window unless already at the top or bottom
|
||||||
|
let fullViewSpanIndex = spanIndex;
|
||||||
|
if (spanIndex !== 0 && spanIndex !== spans.length - 1) {
|
||||||
|
fullViewSpanIndex -= direction;
|
||||||
|
}
|
||||||
|
const [viewStart, viewEnd] = xrs.getViewRange();
|
||||||
|
const checkVisibility = viewStart !== 0 || viewEnd !== 1;
|
||||||
|
// use NaN as fallback to make flow happy
|
||||||
|
const startTime = checkVisibility ? traceStartTime + duration * viewStart : NaN;
|
||||||
|
const endTime = checkVisibility ? traceStartTime + duration * viewEnd : NaN;
|
||||||
|
const findMatches = xrs.getSearchedSpanIDs();
|
||||||
|
const _collapsed = xrs.getCollapsedChildren();
|
||||||
|
const childrenAreHidden = _collapsed ? new Set(_collapsed) : null;
|
||||||
|
// use empty Map as fallback to make flow happy
|
||||||
|
const spansMap: Map<string, Span> = childrenAreHidden
|
||||||
|
? new Map(spans.map(s => [s.spanID, s] as [string, Span]))
|
||||||
|
: new Map();
|
||||||
|
const boundary = direction < 0 ? -1 : spans.length;
|
||||||
|
let nextSpanIndex: number | undefined;
|
||||||
|
for (let i = fullViewSpanIndex + direction; i !== boundary; i += direction) {
|
||||||
|
const span = spans[i];
|
||||||
|
const { duration: spanDuration, spanID, startTime: spanStartTime } = span;
|
||||||
|
const spanEndTime = spanStartTime + spanDuration;
|
||||||
|
if (checkVisibility && (spanStartTime > endTime || spanEndTime < startTime)) {
|
||||||
|
// span is not visible within the view range
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (findMatches && !findMatches.has(spanID)) {
|
||||||
|
// skip to search matches (when searching)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (childrenAreHidden) {
|
||||||
|
// make sure the span is not collapsed
|
||||||
|
const { isHidden, parentIDs } = isSpanHidden(span, childrenAreHidden, spansMap);
|
||||||
|
if (isHidden) {
|
||||||
|
parentIDs.forEach(id => childrenAreHidden.add(id));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nextSpanIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!nextSpanIndex || nextSpanIndex === boundary) {
|
||||||
|
// might as well scroll to the top or bottom
|
||||||
|
nextSpanIndex = boundary - direction;
|
||||||
|
|
||||||
|
// If there are hidden children, scroll to the last visible span
|
||||||
|
if (childrenAreHidden) {
|
||||||
|
let isFallbackHidden: boolean;
|
||||||
|
do {
|
||||||
|
const { isHidden, parentIDs } = isSpanHidden(spans[nextSpanIndex], childrenAreHidden, spansMap);
|
||||||
|
if (isHidden) {
|
||||||
|
parentIDs.forEach(id => childrenAreHidden.add(id));
|
||||||
|
nextSpanIndex--;
|
||||||
|
}
|
||||||
|
isFallbackHidden = isHidden;
|
||||||
|
} while (isFallbackHidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const nextRow = xrs.mapSpanIndexToRowIndex(nextSpanIndex);
|
||||||
|
this._scrollPast(nextRow, direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sometimes the ScrollManager is created before the trace is loaded. This
|
||||||
|
* setter allows the trace to be set asynchronously.
|
||||||
|
*/
|
||||||
|
setTrace(trace: Trace | TNil) {
|
||||||
|
this._trace = trace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `setAccessors` is bound in the ctor, so it can be passed as a prop to
|
||||||
|
* children components.
|
||||||
|
*/
|
||||||
|
setAccessors = (accessors: Accessors) => {
|
||||||
|
this._accessors = accessors;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolls around one page down (0.95x). It is bounds in the ctor, so it can
|
||||||
|
* be used as a keyboard shortcut handler.
|
||||||
|
*/
|
||||||
|
scrollPageDown = () => {
|
||||||
|
if (!this._scroller || !this._accessors) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._scroller.scrollBy(0.95 * this._accessors.getViewHeight(), true);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolls around one page up (0.95x). It is bounds in the ctor, so it can
|
||||||
|
* be used as a keyboard shortcut handler.
|
||||||
|
*/
|
||||||
|
scrollPageUp = () => {
|
||||||
|
if (!this._scroller || !this._accessors) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._scroller.scrollBy(-0.95 * this._accessors.getViewHeight(), true);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolls to the next visible span, ignoring spans that do not match the
|
||||||
|
* text filter, if there is one. It is bounds in the ctor, so it can
|
||||||
|
* be used as a keyboard shortcut handler.
|
||||||
|
*/
|
||||||
|
scrollToNextVisibleSpan = () => {
|
||||||
|
this._scrollToVisibleSpan(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolls to the previous visible span, ignoring spans that do not match the
|
||||||
|
* text filter, if there is one. It is bounds in the ctor, so it can
|
||||||
|
* be used as a keyboard shortcut handler.
|
||||||
|
*/
|
||||||
|
scrollToPrevVisibleSpan = () => {
|
||||||
|
this._scrollToVisibleSpan(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
scrollToFirstVisibleSpan = () => {
|
||||||
|
this._scrollToVisibleSpan(1, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this._trace = undefined;
|
||||||
|
this._scroller = undefined as any;
|
||||||
|
this._accessors = undefined;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) 2018 The Jaeger Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.OpNode {
|
||||||
|
width: 100%;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-collapse: separate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OpNode--popoverContent {
|
||||||
|
border: 1px solid #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OpNode--vectorBorder {
|
||||||
|
box-sizing: content-box;
|
||||||
|
stroke: rgba(0, 0, 0, 0.4);
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OpNode td,
|
||||||
|
.OpNode th {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OpNode.is-ui-find-match {
|
||||||
|
outline: inherit;
|
||||||
|
outline-color: #fff3d7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OpNode--popover .OpNode.is-ui-find-match {
|
||||||
|
outline: #fff3d7 solid 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OpNode--legendNode {
|
||||||
|
background: #096dd9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OpNode--mode-time {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OpNode--metricCell {
|
||||||
|
text-align: right;
|
||||||
|
padding: 0.3rem 0.5rem;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.OpNode--labelCell {
|
||||||
|
padding: 0.3rem 0.5rem 0.3rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OpNode--popover .OpNode--copyIcon,
|
||||||
|
.OpNode:not(:hover) .OpNode--copyIcon {
|
||||||
|
color: transparent;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.OpNode--service {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tweak the popover aesthetics - unfortunate but necessary */
|
||||||
|
|
||||||
|
.OpNode--popover .ant-popover-inner-content {
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue