├── src └── app │ ├── components │ ├── .gitkeep │ ├── Footer │ │ ├── style.css │ │ └── index.tsx │ ├── Title │ │ ├── style.css │ │ └── index.tsx │ ├── ContentHeader │ │ ├── style.css │ │ └── index.tsx │ ├── Detail │ │ ├── style.css │ │ └── index.tsx │ └── Echart │ │ ├── SmoothedLine │ │ └── index.tsx │ │ ├── PvUvLine │ │ └── index.tsx │ │ ├── Bar │ │ └── index.tsx │ │ ├── DoughnutPie │ │ └── index.tsx │ │ └── ChinaMap │ │ └── index.tsx │ ├── constants │ └── .gitkeep │ ├── containers │ ├── Modules │ │ ├── Error │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── PageMsg │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── User │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── Referrer │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── System │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── AssetsMsg │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── RealTime │ │ │ ├── index.css │ │ │ └── overView.tsx │ │ ├── WebPerformance │ │ │ ├── index.css │ │ │ └── index.tsx │ │ ├── Home │ │ │ ├── index.css │ │ │ └── index.tsx │ │ └── BaseMsg │ │ │ ├── index.css │ │ │ └── index.tsx │ ├── Website │ │ ├── index.css │ │ └── index.tsx │ ├── Root │ │ └── index.tsx │ ├── Auth │ │ ├── style.css │ │ ├── signIn.tsx │ │ └── signUp.tsx │ ├── Container │ │ └── index.tsx │ ├── View2.tsx │ ├── View.tsx │ └── App │ │ └── index.tsx │ ├── models │ └── index.ts │ ├── index.css │ ├── api │ ├── realtime.ts │ ├── pages.ts │ ├── assets.ts │ ├── commonData.ts │ ├── auth.ts │ ├── axios.ts │ └── host.ts │ ├── layout │ ├── Header │ │ ├── style.css │ │ └── index.tsx │ └── Sider │ │ └── index.tsx │ ├── stores │ ├── TokenStore.ts │ ├── TimerStore.ts │ ├── RouterStore.ts │ ├── PageStore.ts │ ├── index.ts │ ├── RealTimeStore.ts │ ├── OverViewStore.ts │ ├── AuthStore.ts │ ├── HostStore.ts │ ├── AssetsStore.ts │ └── CommonDataStore.ts │ ├── config │ └── path.ts │ ├── index.tsx │ ├── reset.css │ ├── interfaces │ └── index.ts │ ├── registerServiceWorker.ts │ └── macarons.js ├── public ├── favicon.ico ├── images │ └── tinylog.png ├── manifest.json └── index.html ├── docs └── 20180505210245.png ├── tsconfig.test.json ├── config ├── jest │ ├── typescriptTransform.js │ ├── fileTransform.js │ └── cssTransform.js ├── polyfills.js ├── paths.js ├── env.js ├── webpackDevServer.config.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── .gitignore ├── .circleci └── config.yml ├── nginx-site.conf ├── scripts ├── test.js ├── start.js └── build.js ├── tsconfig.json ├── Dockerfile ├── tslint.json ├── package.json └── README.md /src/app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/constants/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/containers/Modules/Error/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/containers/Modules/PageMsg/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/containers/Modules/PageMsg/index.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinylog/tinylog-ui/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /docs/20180505210245.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinylog/tinylog-ui/HEAD/docs/20180505210245.png -------------------------------------------------------------------------------- /public/images/tinylog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinylog/tinylog-ui/HEAD/public/images/tinylog.png -------------------------------------------------------------------------------- /src/app/models/index.ts: -------------------------------------------------------------------------------- 1 | export interface IUser { 2 | email: string, 3 | token: string, 4 | id: string 5 | } -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /src/app/index.css: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | @import './reset.css'; 3 | 4 | #root, #root > div { 5 | height: 100%; 6 | } -------------------------------------------------------------------------------- /src/app/components/Footer/style.css: -------------------------------------------------------------------------------- 1 | .tl-footer { 2 | text-align: center; 3 | font-size: 12px; 4 | color: rgba(0, 0, 0, 0.65); 5 | } -------------------------------------------------------------------------------- /src/app/components/Title/style.css: -------------------------------------------------------------------------------- 1 | h3 { 2 | color: #1890ff; 3 | font-size: 15px; 4 | padding: 0 4px; 5 | padding-bottom: 8px; 6 | } -------------------------------------------------------------------------------- /src/app/containers/Website/index.css: -------------------------------------------------------------------------------- 1 | .website-page { 2 | padding: 40px; 3 | } 4 | 5 | .action-panel { 6 | margin-bottom: 16px; 7 | } -------------------------------------------------------------------------------- /src/app/containers/Modules/User/index.css: -------------------------------------------------------------------------------- 1 | .user-cont { 2 | display: flex; 3 | justify-content: space-around; 4 | padding: 20px 0; 5 | } -------------------------------------------------------------------------------- /src/app/containers/Modules/Referrer/index.css: -------------------------------------------------------------------------------- 1 | .referrer-cont { 2 | display: flex; 3 | justify-content: space-around; 4 | padding: 20px 0; 5 | } -------------------------------------------------------------------------------- /src/app/containers/Modules/System/index.css: -------------------------------------------------------------------------------- 1 | .system-cont { 2 | display: flex; 3 | justify-content: space-around; 4 | padding: 20px 0; 5 | } -------------------------------------------------------------------------------- /src/app/containers/Modules/AssetsMsg/index.css: -------------------------------------------------------------------------------- 1 | .assets-msg-data-cont { 2 | display: flex; 3 | justify-content: space-around; 4 | padding: 20px 0; 5 | } -------------------------------------------------------------------------------- /src/app/containers/Modules/RealTime/index.css: -------------------------------------------------------------------------------- 1 | .realtime-msg-data-cont { 2 | display: flex; 3 | justify-content: space-around; 4 | padding: 20px 0; 5 | } -------------------------------------------------------------------------------- /src/app/containers/Modules/WebPerformance/index.css: -------------------------------------------------------------------------------- 1 | .web-performance-data-cont { 2 | display: flex; 3 | justify-content: space-around; 4 | padding: 20px 0; 5 | } -------------------------------------------------------------------------------- /src/app/containers/Modules/Home/index.css: -------------------------------------------------------------------------------- 1 | .home-logo { 2 | width: 600px; 3 | height: 200px; 4 | background: url(/images/tinylog.png) no-repeat; 5 | background-size: 100% 100%; 6 | margin: 150px auto; 7 | } -------------------------------------------------------------------------------- /config/jest/typescriptTransform.js: -------------------------------------------------------------------------------- 1 | // Copyright 2004-present Facebook. All Rights Reserved. 2 | 3 | 'use strict'; 4 | 5 | const tsJestPreprocessor = require('ts-jest/preprocessor'); 6 | 7 | module.exports = tsJestPreprocessor; 8 | -------------------------------------------------------------------------------- /src/app/containers/Modules/BaseMsg/index.css: -------------------------------------------------------------------------------- 1 | .base-msg-data-cont { 2 | display: flex; 3 | justify-content: space-around; 4 | padding-bottom: 10px; 5 | border-bottom: 1px solid #e8e8e8; 6 | margin-bottom: 10px; 7 | border-bottom: 1px solid #e8e8e8; 8 | } -------------------------------------------------------------------------------- /src/app/containers/Modules/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './index.css'; 3 | 4 | class Home extends React.Component<{}, {}> { 5 | render () { 6 | return ( 7 |
8 | ) 9 | } 10 | } 11 | 12 | export default Home; -------------------------------------------------------------------------------- /src/app/components/Title/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './style.css'; 3 | 4 | class Title extends React.Component<{title: string}, {}> { 5 | render () { 6 | return ( 7 |

{this.props.title}

8 | ); 9 | } 10 | } 11 | 12 | export default Title; -------------------------------------------------------------------------------- /src/app/components/ContentHeader/style.css: -------------------------------------------------------------------------------- 1 | .content-header { 2 | margin: 0 16px; 3 | margin-top: 16px; 4 | padding: 0 16px; 5 | background: rgb(255, 255, 255); 6 | } 7 | 8 | .content-header h3 { 9 | height: 52px; 10 | font-size: 18px; 11 | font-weight: bold; 12 | line-height: 50px; 13 | } -------------------------------------------------------------------------------- /src/app/api/realtime.ts: -------------------------------------------------------------------------------- 1 | import rest from './axios'; 2 | import { IRealTimeQuery } from '../interfaces'; 3 | 4 | const getOverViewRealTime = (query: IRealTimeQuery) => { 5 | return rest.request({ 6 | method: 'get', 7 | url: `/realtime/${query.id}/overview` 8 | }) 9 | } 10 | 11 | export default { 12 | getOverViewRealTime 13 | } -------------------------------------------------------------------------------- /src/app/components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './style.css'; 3 | 4 | class Footer extends React.Component<{}, {}> { 5 | render () { 6 | return ( 7 | 10 | ); 11 | } 12 | } 13 | 14 | export default Footer; -------------------------------------------------------------------------------- /src/app/layout/Header/style.css: -------------------------------------------------------------------------------- 1 | .header ul li:last-child { 2 | position: absolute; 3 | right: 0; 4 | } 5 | 6 | .header .logo { 7 | width: 120px; 8 | height: 31px; 9 | margin: 16px 28px 16px 0; 10 | color: #fff; 11 | text-align: center; 12 | font-size: 15px; 13 | line-height: 31px; 14 | background: #e6f7ff34; 15 | float: left; 16 | } -------------------------------------------------------------------------------- /src/app/stores/TokenStore.ts: -------------------------------------------------------------------------------- 1 | import { observable, action } from 'mobx'; 2 | 3 | class TokenStore { 4 | @observable token; 5 | constructor () { 6 | this.token = ''; 7 | } 8 | @action resetToken () { 9 | this.token = ''; 10 | } 11 | @action setToken (token: string) { 12 | this.token = token; 13 | } 14 | } 15 | 16 | export default TokenStore; -------------------------------------------------------------------------------- /src/app/components/ContentHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './style.css'; 3 | 4 | class ContentHeader extends React.Component<{ title: string }, {}> { 5 | render () { 6 | return ( 7 |
8 |

{this.props.title}

9 |
10 | ); 11 | } 12 | } 13 | 14 | export default ContentHeader; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/app/api/pages.ts: -------------------------------------------------------------------------------- 1 | import rest from './axios'; 2 | import { IPageQuery } from '../interfaces'; 3 | 4 | const getPages = (query: IPageQuery) => { 5 | return rest.request({ 6 | method: 'get', 7 | url: `/host/${query.id}/pages/slow`, 8 | params: { 9 | from: query.from, 10 | to: query.to 11 | } 12 | }) 13 | } 14 | 15 | export default { 16 | getPages 17 | } -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/app/api/assets.ts: -------------------------------------------------------------------------------- 1 | import rest from './axios'; 2 | import { IAssetsQuery } from '../interfaces'; 3 | 4 | const getAssetsSlow = (query: IAssetsQuery) => { 5 | return rest.request({ 6 | method: 'get', 7 | url: `/host/${query.id}/assets/slow`, 8 | params: { 9 | from: query.from, 10 | to: query.to 11 | } 12 | }) 13 | } 14 | 15 | export default { 16 | getAssetsSlow 17 | } -------------------------------------------------------------------------------- /src/app/stores/TimerStore.ts: -------------------------------------------------------------------------------- 1 | import { observable, action } from 'mobx'; 2 | 3 | class TimerStore { 4 | @observable timer; 5 | constructor () { 6 | this.timer = 0; 7 | } 8 | @action resetTimer () { 9 | this.timer = 0; 10 | } 11 | 12 | @action addTimer () { 13 | this.timer++; 14 | } 15 | 16 | @action subTimer () { 17 | this.timer--; 18 | } 19 | } 20 | 21 | export default TimerStore; -------------------------------------------------------------------------------- /src/app/api/commonData.ts: -------------------------------------------------------------------------------- 1 | import rest from './axios'; 2 | import { ICommonDataQuery } from './../interfaces/index'; 3 | 4 | const getCommonDataQuery = (query: ICommonDataQuery) => { 5 | return rest.request({ 6 | method: 'get', 7 | url: `/host/${query.id}/distribution/${query.type}`, 8 | params: { 9 | from: query.from, 10 | to: query.to 11 | } 12 | }) 13 | } 14 | 15 | export default { 16 | getCommonDataQuery 17 | } -------------------------------------------------------------------------------- /src/app/stores/RouterStore.ts: -------------------------------------------------------------------------------- 1 | import { History } from 'history'; 2 | import { RouterStore as BaseRouterStore, syncHistoryWithStore } from 'mobx-react-router'; 3 | 4 | // 路由状态同步 5 | class RouterStore extends BaseRouterStore { 6 | public history; 7 | constructor(history?: History) { 8 | super(); 9 | if (history) { 10 | this.history = syncHistoryWithStore(history, this); 11 | } 12 | } 13 | } 14 | 15 | export default RouterStore; -------------------------------------------------------------------------------- /src/app/api/auth.ts: -------------------------------------------------------------------------------- 1 | import { ISignIn, ISignUp } from './../interfaces/index'; 2 | import rest from './axios'; 3 | 4 | const signIn = (data: ISignIn) => { 5 | return rest.request({ 6 | method: 'post', 7 | url: '/user/login', 8 | data 9 | }); 10 | }; 11 | 12 | const signUp = (data: ISignUp) => { 13 | return rest.request({ 14 | method: 'post', 15 | url: '/user/register', 16 | data 17 | }); 18 | }; 19 | 20 | export default { 21 | signIn, 22 | signUp 23 | } -------------------------------------------------------------------------------- /src/app/containers/Modules/Error/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './index.css'; 3 | import Detail from '../../../components/Detail'; 4 | 5 | class Error extends React.Component<{}, {}> { 6 | render () { 7 | const title = '错误信息' 8 | const messages = [ 9 | { name: '错误次数', value: 0 } 10 | ] 11 | return ( 12 |
13 | 14 |
15 | ) 16 | } 17 | } 18 | 19 | export default Error; -------------------------------------------------------------------------------- /src/app/containers/Root/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export class Root extends React.Component<{}, {}> { 4 | 5 | renderDevTool() { 6 | if (process.env.NODE_ENV !== 'production') { 7 | const DevTools = require('mobx-react-devtools').default; 8 | return (); 9 | } 10 | return null; 11 | } 12 | 13 | render() { 14 | return ( 15 |
16 | {this.props.children} 17 | {this.renderDevTool()} 18 |
19 | ); 20 | } 21 | } -------------------------------------------------------------------------------- /src/app/components/Detail/style.css: -------------------------------------------------------------------------------- 1 | .tl-detail-container { 2 | overflow: hidden; 3 | padding-bottom: 10px; 4 | border-bottom: 1px solid #e8e8e8; 5 | margin-bottom: 10px; 6 | } 7 | 8 | .tl-detail-form { 9 | overflow: hidden; 10 | } 11 | 12 | .tl-detail-container .tl-detail-form p { 13 | float: left; 14 | width: 33.3%; 15 | box-sizing: border-box; 16 | padding: 4px; 17 | } 18 | 19 | .tl-detail-container .tl-detail-form span { 20 | display: inline-block; 21 | min-width: 150px; 22 | padding-right: 4px; 23 | } -------------------------------------------------------------------------------- /src/app/stores/PageStore.ts: -------------------------------------------------------------------------------- 1 | import { observable, action, runInAction } from 'mobx'; 2 | import { IPageQuery, IPage } from '../interfaces'; 3 | import api from '../api/pages'; 4 | 5 | class PageStore { 6 | @observable pages: (IPage)[]; 7 | constructor () { 8 | this.pages = []; 9 | } 10 | @action async getPages (query: IPageQuery) { 11 | const { data: res } = await api.getPages(query); 12 | runInAction(() => { 13 | this.pages = res.data; 14 | }) 15 | return res; 16 | } 17 | } 18 | 19 | export default PageStore; -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | image_config: &image_config 2 | 3 | IMAGE_NAME: tinylog-ui 4 | 5 | IMAGE_TAG: latest 6 | 7 | version: 2 8 | jobs: 9 | 10 | build: 11 | machine: true 12 | 13 | environment: 14 | <<: *image_config 15 | 16 | steps: 17 | - checkout 18 | 19 | - run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD 20 | 21 | - run: docker build -t $DOCKER_USERNAME/$IMAGE_NAME:$IMAGE_TAG . 22 | 23 | - run: docker push $DOCKER_USERNAME/$IMAGE_NAME:$IMAGE_TAG && sleep 10 24 | 25 | - store_artifacts: 26 | path: Dockerfile 27 | -------------------------------------------------------------------------------- /src/app/containers/Auth/style.css: -------------------------------------------------------------------------------- 1 | .auth { 2 | position: relative; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | width: 100%; 7 | height: 100%; 8 | } 9 | 10 | .auth .login-form { 11 | width: 300px; 12 | } 13 | 14 | .auth .login-form-forgot { 15 | float: right; 16 | } 17 | 18 | .auth .login-form-button { 19 | width: 100%; 20 | } 21 | 22 | .auth-logo { 23 | position: absolute; 24 | width: 400px; 25 | height: 140px; 26 | background: url(/images/tinylog.png) no-repeat; 27 | background-size: 100% 100%; 28 | margin: auto; 29 | top: 100px; 30 | left: 0; 31 | right: 0; 32 | } -------------------------------------------------------------------------------- /src/app/stores/index.ts: -------------------------------------------------------------------------------- 1 | import TimerStore from './TimerStore'; 2 | import RouterStore from './RouterStore'; 3 | import TokenStore from './TokenStore'; 4 | import AuthStore from './AuthStore'; 5 | import HostStore from './HostStore'; 6 | import OverViewStore from './OverViewStore'; 7 | import AssetsStore from './AssetsStore'; 8 | import CommonDataStore from './CommonDataStore' 9 | import PageStore from './PageStore'; 10 | import RealTimeStore from './RealTimeStore'; 11 | 12 | export { 13 | TimerStore, 14 | RouterStore, 15 | TokenStore, 16 | AuthStore, 17 | HostStore, 18 | OverViewStore, 19 | AssetsStore, 20 | PageStore, 21 | CommonDataStore, 22 | RealTimeStore 23 | }; -------------------------------------------------------------------------------- /src/app/stores/RealTimeStore.ts: -------------------------------------------------------------------------------- 1 | import { observable, action, runInAction } from 'mobx'; 2 | import api from '../api/realtime'; 3 | import { IRealTimeQuery, IRealTime } from '../interfaces/index'; 4 | 5 | class RealTimeStore { 6 | @observable realtime: IRealTime; 7 | constructor () { 8 | this.realtime = { 9 | count: 0, 10 | referrer: [], 11 | browserName: [], 12 | deviceType: [], 13 | country: [] 14 | } 15 | } 16 | @action async getRealTime (query: IRealTimeQuery) { 17 | const { data: res} = await api.getOverViewRealTime(query); 18 | runInAction (() => { 19 | this.realtime = res.data; 20 | }) 21 | return res; 22 | } 23 | } 24 | 25 | export default RealTimeStore; -------------------------------------------------------------------------------- /nginx-site.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | server_name _; 4 | root /usr/share/nginx/html; 5 | index index.html; 6 | 7 | gzip on; 8 | gzip_disable "msie6"; 9 | 10 | gzip_vary on; 11 | gzip_proxied any; 12 | gzip_comp_level 6; 13 | gzip_buffers 16 8k; 14 | gzip_http_version 1.1; 15 | gzip_min_length 256; 16 | gzip_types text/plain text/css application/json application/x-javascript application/javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon; 17 | 18 | location / { 19 | try_files $uri $uri/ /index.html =404; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/containers/Container/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Header from '../../layout/Header'; 3 | import { IAuth } from '../../interfaces'; 4 | import { Route, Switch } from 'react-router'; 5 | import { inject, observer } from 'mobx-react'; 6 | import App from '../App'; 7 | import Website from '../Website'; 8 | 9 | interface Container extends IAuth { 10 | } 11 | 12 | @inject('router', 'auth') 13 | @observer 14 | class Container extends React.Component { 15 | render () { 16 | return ( 17 |
18 |
19 | 20 | 21 | 22 | 23 |
24 | ) 25 | } 26 | } 27 | 28 | export default Container; -------------------------------------------------------------------------------- /src/app/stores/OverViewStore.ts: -------------------------------------------------------------------------------- 1 | import { observable, action, runInAction } from 'mobx'; 2 | import api from '../api/host'; 3 | import { IOverview, IOverviewQuery } from '../interfaces'; 4 | import moment from 'moment'; 5 | 6 | // 网站的基本数据信息 7 | class OverViewStore { 8 | @observable overviews: (IOverview)[]; 9 | constructor () { 10 | this.overviews = [] 11 | } 12 | @action async getOverview (query: IOverviewQuery) { 13 | const { data: res } = await api.getOverview(query) 14 | runInAction (() => { 15 | // mobx 的数据被封装过,使用需要slice一下: console.log(this.overviews.slice()) 16 | this.overviews = res.data.map(item => { 17 | item.date = moment(item.date).format('YY-MM-DD') 18 | return item 19 | }) 20 | }) 21 | return res; 22 | } 23 | } 24 | 25 | export default OverViewStore -------------------------------------------------------------------------------- /src/app/components/Detail/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Title from '../Title'; 3 | import './style.css'; 4 | 5 | interface IMessages { 6 | name: string; 7 | value: string|number; 8 | } 9 | 10 | interface DetailProps { 11 | title: string; 12 | messages: (IMessages)[]; 13 | } 14 | 15 | class Detail extends React.Component { 16 | render () { 17 | const messages = this.props.messages.map((item, index) => { 18 | return ( 19 |

{item.name}{item.value}

20 | ) 21 | }) 22 | return ( 23 |
24 | 25 | <div className={'tl-detail-form'}> 26 | {messages} 27 | </div> 28 | </div> 29 | ); 30 | } 31 | } 32 | 33 | export default Detail; -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | const argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI or in coverage mode 22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 23 | argv.push('--watch'); 24 | } 25 | 26 | 27 | jest.run(argv); 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "esnext", 5 | "target": "es5", 6 | "lib": ["es6", "dom"], 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "jsx": "react", 10 | "moduleResolution": "node", 11 | "rootDir": "src", 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "noImplicitThis": true, 15 | "noImplicitAny": false, 16 | "strictNullChecks": true, 17 | "experimentalDecorators": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noUnusedLocals": true, 20 | "allowSyntheticDefaultImports": true 21 | }, 22 | "exclude": [ 23 | "node_modules", 24 | "build", 25 | "scripts", 26 | "acceptance-tests", 27 | "webpack", 28 | "jest", 29 | "src/setupTests.ts" 30 | ], 31 | "types": [ 32 | "typePatches" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /src/app/api/axios.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { message } from 'antd'; 3 | 4 | axios.interceptors.request.use(function (config: any) { 5 | config.baseURL = 'https://tinylog.ruiming.me/api'; 6 | config.headers = { 7 | authorization: `Bearer ${localStorage.getItem('token')}` 8 | } 9 | return config; 10 | }, function (error: any) { 11 | return Promise.reject(error); 12 | }); 13 | 14 | axios.interceptors.response.use(function (response: any) { 15 | const data = response.data; 16 | response.data = {} 17 | response.data.code = 200; 18 | response.data.data = data; 19 | return response; 20 | }, function (error: any) { 21 | console.dir(error) 22 | const msg = (error.response && error.response.data && error.response.data.message) || '服务器连接异常'; 23 | message.info(msg); 24 | return Promise.reject({ 25 | code: (error.response && error.response.status) || 500, 26 | msg 27 | }); 28 | }); 29 | 30 | export default axios; -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | 18 | // In tests, polyfill requestAnimationFrame since jsdom doesn't provide it yet. 19 | // We don't polyfill it in the browser--this is user's responsibility. 20 | if (process.env.NODE_ENV === 'test') { 21 | require('raf').polyfill(global); 22 | } 23 | -------------------------------------------------------------------------------- /src/app/api/host.ts: -------------------------------------------------------------------------------- 1 | import rest from './axios'; 2 | import { IOverviewQuery } from '../interfaces' 3 | import { IHostBody } from '../interfaces'; 4 | 5 | const getHost = () => { 6 | return rest.request({ 7 | method: 'get', 8 | url: '/host' 9 | }); 10 | }; 11 | 12 | const postHost = (data: IHostBody) => { 13 | return rest.request({ 14 | method: 'post', 15 | url: '/host/create', 16 | data 17 | }) 18 | } 19 | 20 | const patchHost = (data: IHostBody) => { 21 | return rest.request({ 22 | method: 'patch', 23 | url: `/host/${data.id}` 24 | }) 25 | } 26 | 27 | const deleteHost = (data: IHostBody) => { 28 | return rest.request({ 29 | method: 'delete', 30 | url: '/host', 31 | data: { 32 | list: [data.id] 33 | } 34 | }) 35 | } 36 | 37 | const getOverview = (query: IOverviewQuery) => { 38 | return rest.request({ 39 | method: 'get', 40 | url: `/host/${query.id}/overview`, 41 | params: { 42 | from: query.from, 43 | to: query.to 44 | } 45 | }) 46 | } 47 | 48 | export default { 49 | getHost, 50 | getOverview, 51 | postHost, 52 | deleteHost, 53 | patchHost 54 | } -------------------------------------------------------------------------------- /src/app/containers/View2.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | import { TimerStore } from '../stores'; 4 | import { RouterStore } from 'mobx-react-router'; 5 | import { RouteComponentProps } from 'react-router'; 6 | import { Button } from 'antd'; 7 | export interface ViewProps extends RouteComponentProps<{}> { 8 | timer: TimerStore; 9 | router: RouterStore; 10 | } 11 | 12 | @inject('timer', 'router') 13 | @observer 14 | class View extends React.Component<ViewProps, {}> { 15 | render () { 16 | const { goBack } = this.props.router; 17 | return ( 18 | <div> 19 | <h1>Test2</h1> 20 | <Button onClick={this.onReset}> 21 | Seconds passed: {this.props.timer.timer} 22 | </Button> 23 | <Button onClick={this.addTimer}>Add</Button> 24 | <Button onClick={this.subTimer}>Sub</Button> 25 | <Button onClick={() => goBack()}>Go Back</Button> 26 | </div> 27 | ); 28 | } 29 | onReset = () => { 30 | this.props.timer.resetTimer(); 31 | } 32 | addTimer = () => { 33 | this.props.timer.addTimer(); 34 | } 35 | subTimer = () => { 36 | this.props.timer.subTimer(); 37 | } 38 | 39 | } 40 | 41 | export default View; -------------------------------------------------------------------------------- /src/app/config/path.ts: -------------------------------------------------------------------------------- 1 | export const headerPath = [ 2 | { 3 | title: '我的站点', 4 | path: '/website', 5 | }, 6 | { 7 | title: '退出', 8 | path: '/signIn' 9 | } 10 | ]; 11 | 12 | export const siderPath = [ 13 | { 14 | title: '基本信息', 15 | icon: 'global', 16 | menuItems: [{ 17 | title: '网站信息', 18 | path: '/base/webMsg' 19 | }] 20 | }, 21 | { 22 | title: '实时数据', 23 | icon: 'dashboard', 24 | menuItems: [{ 25 | title: '实时预览', 26 | path: '/realtime/webMsg' 27 | }] 28 | }, 29 | { 30 | title: '性能分析', 31 | icon: 'laptop', 32 | menuItems: [{ 33 | title: '网站性能', 34 | path: '/performance/webMsg' 35 | }, { 36 | title: '资源信息', 37 | path: '/performance/assetsMsg' 38 | }] 39 | }, 40 | { 41 | title: '数据分析', 42 | icon: 'bar-chart', 43 | menuItems: [{ 44 | title: '来源地址', 45 | path: '/common/referrer' 46 | }, { 47 | title: '环境系统', 48 | path: '/common/system' 49 | }, { 50 | title: '用户分析', 51 | path: '/common/user' 52 | }] 53 | }, 54 | { 55 | title: '错误上报', 56 | icon: 'notification', 57 | menuItems: [{ 58 | title: '错误概览', 59 | path: '/error' 60 | }] 61 | } 62 | ]; -------------------------------------------------------------------------------- /src/app/containers/Modules/User/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | import { autobind } from 'core-decorators'; 4 | import Detail from '../../../components/Detail'; 5 | import Title from '../../../components/Title'; 6 | import ChinaMap from '../../../components/Echart/ChinaMap'; 7 | import { ICommonDataPage } from '../../../interfaces'; 8 | 9 | import './index.css'; 10 | 11 | interface UserProps extends ICommonDataPage { 12 | } 13 | 14 | @inject('host', 'router', 'commmon') 15 | @autobind 16 | @observer 17 | class User extends React.Component<UserProps, {}> { 18 | async componentWillMount () { 19 | await this.props.host.getHost(); 20 | await this.props.commmon.getCountry({ 21 | id: this.props.host.id, 22 | to: new Date().toISOString(), 23 | from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString() 24 | }); 25 | } 26 | render () { 27 | const title = '用户数据展示'; 28 | const messages = [ 29 | { name: '用户量:', value: 200 }, 30 | { name: '', value: '' } 31 | ] 32 | return ( 33 | <div> 34 | <Detail title={title} messages={messages}/> 35 | <Title title="用户地区分布情况"/> 36 | <div className="user-cont"> 37 | <ChinaMap vaules={this.props.commmon.country.slice().map(item => ({ value: item.count, name: item.value}))}/> 38 | </div> 39 | </div> 40 | ) 41 | } 42 | } 43 | 44 | export default User; 45 | -------------------------------------------------------------------------------- /src/app/layout/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Layout, Menu } from 'antd'; 3 | import { headerPath } from '../../config/path'; 4 | import { autobind } from 'core-decorators'; 5 | import { IAuth } from '../../interfaces/index'; 6 | import { Link } from 'react-router-dom'; 7 | import './style.css'; 8 | 9 | const { Header } = Layout; 10 | 11 | interface HeaderLayoutProps extends IAuth { 12 | } 13 | 14 | @autobind 15 | class HeaderLayout extends React.Component<HeaderLayoutProps, {}> { 16 | handleMenuItemClick ({ item, key, keyPath }: any) { 17 | if (headerPath[+key].path === '/signIn') { 18 | this.props.auth.clearStorage(); 19 | } 20 | this.props.history.push(headerPath[+key].path); 21 | } 22 | render () { 23 | return ( 24 | <Header className="header"> 25 | <div className="logo"><Link to="/" style={{color: '#fff', textDecoration: 'none'}}>Tiny Log</Link></div> 26 | <Menu 27 | theme="dark" 28 | mode="horizontal" 29 | defaultSelectedKeys={['2']} 30 | onClick={this.handleMenuItemClick} 31 | style={{ lineHeight: '64px' }} 32 | > 33 | { 34 | headerPath.map((item, index) => { 35 | return ( 36 | <Menu.Item key={index}> 37 | {item.title} 38 | </Menu.Item> 39 | ); 40 | }) 41 | } 42 | </Menu> 43 | </Header> 44 | ); 45 | } 46 | } 47 | 48 | export default HeaderLayout; -------------------------------------------------------------------------------- /src/app/components/Echart/SmoothedLine/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import echarts from 'echarts'; 3 | 4 | export interface ISommthedLine { 5 | width: number; 6 | height: number; 7 | id: string; 8 | title: string; 9 | xValues?: (string|number)[]; 10 | yValues?: (string|number)[]; 11 | } 12 | 13 | class SommthedLine extends React.Component<ISommthedLine, ISommthedLine> { 14 | constructor (props: ISommthedLine, state: ISommthedLine) { 15 | super(props, state) 16 | this.state = Object.assign({}, this.props) 17 | } 18 | componentWillReceiveProps (nextProps: ISommthedLine) { 19 | this.setState(nextProps, () => { 20 | this.initChart() 21 | }) 22 | } 23 | componentDidMount () { 24 | this.initChart() 25 | } 26 | initChart () { 27 | let chart = echarts.init(document.getElementById(this.state.id), 'macarons'); 28 | chart.setOption({ 29 | title: { 30 | left: 'center', 31 | top: 'top', 32 | text: this.state.title, 33 | textStyle: { 34 | fontSize: 14 35 | } 36 | }, 37 | xAxis: { 38 | type: 'category', 39 | data: this.state.xValues 40 | }, 41 | yAxis: { 42 | type: 'value' 43 | }, 44 | series: [{ 45 | data: this.state.yValues, 46 | type: 'line', 47 | smooth: true 48 | }] 49 | }); 50 | } 51 | render () { 52 | return ( 53 | <div id={this.state.id} style={{display: 'inline-block', width: this.state.width, height: this.state.height}}/> 54 | ); 55 | } 56 | } 57 | 58 | export default SommthedLine; -------------------------------------------------------------------------------- /src/app/containers/Modules/Referrer/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | import { autobind } from 'core-decorators'; 4 | import Detail from '../../../components/Detail'; 5 | import Title from '../../../components/Title'; 6 | import { ICommonDataPage } from '../../../interfaces'; 7 | import Bar from '../../../components/Echart/Bar'; 8 | import './index.css'; 9 | 10 | interface ReferrerProps extends ICommonDataPage { 11 | } 12 | 13 | @inject('host', 'router', 'commmon') 14 | @autobind 15 | @observer 16 | class Referrer extends React.Component<ReferrerProps, {}> { 17 | async componentWillMount () { 18 | await this.props.host.getHost(); 19 | await this.props.commmon.getReffer({ 20 | id: this.props.host.id, 21 | to: new Date().toISOString(), 22 | from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString() 23 | }) 24 | } 25 | render () { 26 | const title = '网站来源地址'; 27 | const messages = [ 28 | { name: '来源地址总数:', value: this.props.commmon.referrer.slice().length + '个' }, 29 | { name: '来源次数最多地址:', value: 'www.baidu.com' } 30 | ] 31 | return ( 32 | <div> 33 | <Detail title={title} messages={messages}/> 34 | <Title title="来源地址数据分布"/> 35 | <div className="referrer-cont"> 36 | <Bar 37 | title="来源地址统计" 38 | id="referrerBar" 39 | width={1500} 40 | height={350} 41 | xValues={this.props.commmon.referrer.map(item => item.value)} 42 | yValues={this.props.commmon.referrer.map(item => item.count)} 43 | /> 44 | </div> 45 | </div> 46 | ) 47 | } 48 | } 49 | 50 | export default Referrer; 51 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8"> 5 | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 6 | <meta name="theme-color" content="#000000"> 7 | <!-- 8 | manifest.json provides metadata used when your web app is added to the 9 | homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ 10 | --> 11 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json"> 12 | <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> 13 | <!-- 14 | Notice the use of %PUBLIC_URL% in the tags above. 15 | It will be replaced with the URL of the `public` folder during the build. 16 | Only files inside the `public` folder can be referenced from the HTML. 17 | 18 | Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will 19 | work correctly both with client-side routing and a non-root public URL. 20 | Learn how to configure a non-root public URL by running `npm run build`. 21 | --> 22 | <title>TinyLog Ui 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/app/stores/AuthStore.ts: -------------------------------------------------------------------------------- 1 | import { ISignIn, ISignUp } from './../interfaces/index'; 2 | import { observable, action } from 'mobx'; 3 | import api from '../api/auth'; 4 | import { IUser } from '../models'; 5 | 6 | // 登录注册状态 7 | class AuthStore { 8 | @observable token; 9 | @observable id; 10 | @observable email; 11 | constructor () { 12 | this.id = ''; 13 | this.token = ''; 14 | this.email = ''; 15 | } 16 | setLocalStorage ({ id, token, email }: IUser) { 17 | localStorage.setItem('id', id); 18 | localStorage.setItem('token', token); 19 | localStorage.setItem('email', email); 20 | } 21 | clearStorage () { 22 | localStorage.clear(); 23 | } 24 | @action async signIn (data: ISignIn) { 25 | try { 26 | const { data: res } = await api.signIn(data); 27 | this.id = res.data.id; 28 | this.token = res.data.token; 29 | this.email = res.data.email; 30 | this.setLocalStorage({ 31 | id: this.id, 32 | token: this.token, 33 | email: this.email 34 | }); 35 | return res; 36 | } catch (error) { 37 | return error; 38 | } 39 | } 40 | 41 | @action async signUp (data: ISignUp) { 42 | try { 43 | const { data: res } = await api.signUp(data); 44 | this.id = res.data.id; 45 | this.token = res.data.token; 46 | this.email = res.data.email; 47 | this.setLocalStorage({ 48 | id: this.id, 49 | token: this.token, 50 | email: this.email 51 | }); 52 | return res; 53 | } catch (error) { 54 | return error; 55 | } 56 | } 57 | 58 | @action signOut () { 59 | this.id = ''; 60 | this.token = ''; 61 | this.email = ''; 62 | this.clearStorage() 63 | } 64 | } 65 | 66 | export default AuthStore; -------------------------------------------------------------------------------- /src/app/containers/View.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | import { TimerStore } from '../stores'; 4 | import { RouterStore } from 'mobx-react-router'; 5 | import { RouteComponentProps } from 'react-router'; 6 | import { Button } from 'antd'; 7 | import echarts from 'echarts'; 8 | 9 | export interface ViewProps extends RouteComponentProps<{}> { 10 | timer: TimerStore; 11 | router: RouterStore; 12 | } 13 | 14 | @inject('timer', 'router') 15 | @observer 16 | class View extends React.Component { 17 | componentDidMount () { 18 | let chart = echarts.init(document.getElementById('main')); 19 | chart.setOption({ 20 | title: { text: 'ECharts 入门示例' }, 21 | tooltip: {}, 22 | xAxis: { 23 | data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'] 24 | }, 25 | yAxis: {}, 26 | series: [{ 27 | name: '销量', 28 | type: 'bar', 29 | data: [5, 20, 36, 10, 10, 20] 30 | }] 31 | }); 32 | } 33 | render () { 34 | const { push } = this.props.router; 35 | return ( 36 |
37 |
38 |

Test1

39 | 42 | 43 | 44 | 45 |
46 | ); 47 | } 48 | onReset = () => { 49 | this.props.timer.resetTimer(); 50 | } 51 | addTimer = () => { 52 | this.props.timer.addTimer(); 53 | } 54 | subTimer = () => { 55 | this.props.timer.subTimer(); 56 | } 57 | 58 | } 59 | 60 | export default View; -------------------------------------------------------------------------------- /src/app/layout/Sider/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Layout, Menu, Icon } from 'antd'; 3 | import { siderPath } from '../../config/path'; 4 | import { Link } from 'react-router-dom'; 5 | import { autobind } from 'core-decorators'; 6 | 7 | const { SubMenu } = Menu; 8 | const { Sider } = Layout; 9 | 10 | interface TitleState { 11 | item: object; 12 | key: string; 13 | } 14 | 15 | interface ISiderLayout { 16 | onMenuItemClick: any; 17 | } 18 | 19 | @autobind 20 | class SiderLayout extends React.Component { 21 | handleMenuItemClick (state: TitleState) { 22 | let arr = state.key.split('-').map((item) => { 23 | return parseInt(item, 10); 24 | }); 25 | this.props.onMenuItemClick(siderPath[arr[0]].menuItems[arr[1]].title); 26 | } 27 | render () { 28 | return ( 29 | 30 | 37 | { 38 | siderPath.map((subMenu, index) => { 39 | return ( 40 | {subMenu.title}}> 41 | { 42 | subMenu.menuItems.map((menuItem, _index) => { 43 | return ( 44 | 45 | {menuItem.title} 46 | 47 | ); 48 | }) 49 | } 50 | 51 | ); 52 | }) 53 | } 54 | 55 | 56 | ); 57 | } 58 | } 59 | 60 | export default SiderLayout; -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:stretch 2 | MAINTAINER Ruiming Zhuang 3 | 4 | ENV NGINX_VERSION 1.12.1-1~stretch 5 | ENV NJS_VERSION 1.12.1.0.1.10-1~stretch 6 | 7 | RUN apt-get update \ 8 | && apt-get install --no-install-recommends --no-install-suggests -y gnupg1 \ 9 | && \ 10 | NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; \ 11 | found=''; \ 12 | for server in \ 13 | ha.pool.sks-keyservers.net \ 14 | hkp://keyserver.ubuntu.com:80 \ 15 | hkp://p80.pool.sks-keyservers.net:80 \ 16 | pgp.mit.edu \ 17 | ; do \ 18 | echo "Fetching GPG key $NGINX_GPGKEY from $server"; \ 19 | apt-key adv --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$NGINX_GPGKEY" && found=yes && break; \ 20 | done; \ 21 | test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ 22 | apt-get remove --purge -y gnupg1 && apt-get -y --purge autoremove && rm -rf /var/lib/apt/lists/* \ 23 | && echo "deb http://nginx.org/packages/debian/ stretch nginx" >> /etc/apt/sources.list \ 24 | && apt-get update \ 25 | && apt-get install --no-install-recommends --no-install-suggests -y \ 26 | nginx=${NGINX_VERSION} \ 27 | nginx-module-xslt=${NGINX_VERSION} \ 28 | nginx-module-geoip=${NGINX_VERSION} \ 29 | nginx-module-image-filter=${NGINX_VERSION} \ 30 | nginx-module-njs=${NJS_VERSION} \ 31 | gettext-base \ 32 | && rm -rf /var/lib/apt/lists/* 33 | 34 | # forward request and error logs to docker log collector 35 | RUN ln -sf /dev/stdout /var/log/nginx/access.log \ 36 | && ln -sf /dev/stderr /var/log/nginx/error.log 37 | 38 | COPY package.json /tmp/package.json 39 | COPY package-lock.json /tmp/package-lock.json 40 | RUN cd /tmp && NPM_CONFIG_LOGLEVEL=warn yarn install 41 | COPY . /tmp 42 | RUN cd /tmp && yarn run build 43 | RUN rm -rf /usr/share/nginx/html && mv /tmp/build /usr/share/nginx/html 44 | COPY nginx-site.conf /etc/nginx/conf.d/default.conf 45 | 46 | EXPOSE 80 47 | CMD nginx -g 'daemon off;' 48 | -------------------------------------------------------------------------------- /src/app/components/Echart/PvUvLine/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import echarts from 'echarts'; 3 | 4 | export interface IPvUvLine { 5 | width: number; 6 | height: number; 7 | id: string; 8 | title: string; 9 | xValues?: (string|number)[]; 10 | pvValues?: (string|number)[]; 11 | uvValues?: (string|number)[]; 12 | } 13 | 14 | class PvUvLine extends React.Component { 15 | constructor (props: IPvUvLine, state: IPvUvLine) { 16 | super(props, state) 17 | this.state = Object.assign({}, this.props) 18 | } 19 | componentWillReceiveProps (nextProps: IPvUvLine) { 20 | this.setState(nextProps, () => { 21 | this.initChart() 22 | }) 23 | } 24 | componentDidMount () { 25 | this.initChart() 26 | } 27 | initChart () { 28 | let chart = echarts.init(document.getElementById(this.state.id), 'macarons'); 29 | chart.setOption({ 30 | title: { 31 | left: 'center', 32 | top: 'bottom', 33 | text: this.state.title, 34 | textStyle: { 35 | fontSize: 14 36 | } 37 | }, 38 | tooltip: { 39 | trigger: 'none', 40 | axisPointer: { 41 | type: 'cross' 42 | } 43 | }, 44 | legend: { 45 | data: ['pv 访问数据', 'uv 访问数据'] 46 | }, 47 | xAxis: { 48 | type: 'category', 49 | data: this.state.xValues 50 | }, 51 | yAxis: { 52 | type: 'value' 53 | }, 54 | series: [{ 55 | name: 'pv 访问数据', 56 | data: this.state.pvValues, 57 | type: 'line', 58 | smooth: true 59 | }, { 60 | name: 'uv 访问数据', 61 | data: this.state.uvValues, 62 | type: 'line', 63 | smooth: true 64 | }] 65 | }); 66 | } 67 | render () { 68 | return ( 69 |
70 | ); 71 | } 72 | } 73 | 74 | export default PvUvLine; -------------------------------------------------------------------------------- /src/app/components/Echart/Bar/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import echarts from 'echarts'; 3 | 4 | interface IBar { 5 | width: number; 6 | height: number; 7 | title: string; 8 | id: string; 9 | xValues: (string|number)[]; 10 | yValues: (string|number)[]; 11 | } 12 | 13 | class Bar extends React.Component { 14 | constructor (props: IBar, state: IBar) { 15 | super(props, state) 16 | this.state = Object.assign({}, this.props) 17 | } 18 | componentWillReceiveProps (nextProps: IBar) { 19 | this.setState(nextProps, () => { 20 | this.initChart(); 21 | }) 22 | } 23 | componentDidMount() { 24 | this.initChart(); 25 | } 26 | initChart () { 27 | let chart = echarts.init(document.getElementById(this.state.id), 'macarons'); 28 | chart.setOption({ 29 | title: { 30 | left: 'center', 31 | top: 'top', 32 | text: this.props.title, 33 | textStyle: { 34 | fontSize: 14 35 | } 36 | }, 37 | tooltip: { 38 | trigger: 'axis', 39 | axisPointer: { 40 | type: 'shadow' 41 | } 42 | }, 43 | grid: { 44 | left: '3%', 45 | right: '4%', 46 | bottom: '3%', 47 | containLabel: true 48 | }, 49 | xAxis: [ 50 | { 51 | type: 'category', 52 | data: this.state.xValues, 53 | axisTick: { 54 | alignWithLabel: true 55 | } 56 | } 57 | ], 58 | yAxis: [ 59 | { 60 | type: 'value' 61 | } 62 | ], 63 | series: [ 64 | { 65 | name: this.state.title, 66 | type: 'bar', 67 | barWidth: '60%', 68 | data: this.state.yValues 69 | } 70 | ] 71 | }); 72 | } 73 | render() { 74 | return ( 75 |
76 | ); 77 | } 78 | } 79 | 80 | export default Bar; -------------------------------------------------------------------------------- /src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { Router, Route, Switch } from 'react-router'; 4 | import { createBrowserHistory } from 'history'; 5 | import { useStrict } from 'mobx'; 6 | import { Provider } from 'mobx-react'; 7 | import { RouterStore, syncHistoryWithStore } from 'mobx-react-router'; 8 | import { 9 | TokenStore, 10 | AuthStore, 11 | HostStore, 12 | OverViewStore, 13 | AssetsStore, 14 | CommonDataStore, 15 | PageStore, 16 | RealTimeStore 17 | } from './stores'; 18 | import registerServiceWorker from './registerServiceWorker'; 19 | import { Root } from './containers/Root'; 20 | import './index.css'; 21 | import Container from './containers/Container'; 22 | import SignIn from './containers/Auth/signIn'; 23 | import SignUp from './containers/Auth/signUp'; 24 | import './macarons'; 25 | import 'echarts/map/js/world'; 26 | 27 | useStrict(true); 28 | 29 | const browserHistory = createBrowserHistory(); 30 | const routerStore = new RouterStore(); 31 | const history = syncHistoryWithStore(browserHistory, routerStore); 32 | const rootStore = { 33 | token: new TokenStore(), 34 | auth: new AuthStore(), 35 | host: new HostStore(), 36 | overview: new OverViewStore(), 37 | assets: new AssetsStore(), 38 | commmon: new CommonDataStore(), 39 | page: new PageStore(), 40 | realtime: new RealTimeStore(), 41 | router: routerStore 42 | }; 43 | 44 | ReactDOM.render( 45 | 46 | 47 | 48 | 49 | 53 | 57 | 61 | 62 | 63 | 64 | , 65 | document.getElementById('root') as HTMLElement 66 | ); 67 | registerServiceWorker(); 68 | -------------------------------------------------------------------------------- /src/app/containers/Auth/signIn.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | import { IAuth } from '../../interfaces/index'; 4 | import { Form, Icon, Input, Button } from 'antd'; 5 | import { Link } from 'react-router-dom'; 6 | import { autobind } from 'core-decorators'; 7 | import { ISignIn } from '../../interfaces'; 8 | const FormItem = Form.Item; 9 | import './style.css'; 10 | 11 | interface SignInState extends ISignIn { 12 | } 13 | 14 | @inject('auth', 'router') 15 | @autobind 16 | @observer 17 | class SignIn extends React.Component { 18 | componentWillMount () { 19 | if (localStorage.getItem('token')) { 20 | this.props.history.push('/'); 21 | return; 22 | } 23 | } 24 | async handleSubmit (event: any) { 25 | event.preventDefault(); 26 | const res = await this.props.auth.signIn({ 27 | email: this.state.email, 28 | password: this.state.password 29 | }) 30 | if (res.code === 200) { 31 | this.props.history.push('/'); 32 | } 33 | } 34 | handleInputEmail (event: any) { 35 | this.setState({ 36 | email: event.target.value 37 | }); 38 | } 39 | handleInputPass (event: any) { 40 | this.setState({ 41 | password: event.target.value 42 | }); 43 | } 44 | render () { 45 | return ( 46 |
47 |
48 |
49 | 50 | } type="email" placeholder="邮箱" onChange={this.handleInputEmail} /> 51 | 52 | 53 | } type="password" placeholder="密码" onChange={this.handleInputPass}/> 54 | 55 | 56 | 59 | 或 开始注册! 60 | 61 |
62 |
63 | ); 64 | } 65 | } 66 | 67 | export default SignIn; -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right