Skip to content

Commit 28bc1ce

Browse files
authoredSep 20, 2018
Merge pull request #1 from topcoderinc/challenge_2
challenge 2 submission
2 parents 6b87d6a + 88f13e3 commit 28bc1ce

34 files changed

+3910
-2774
lines changed
 

‎README.md

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ This project was generated with [Angular CLI](https://github.com/angular/angular
2424
### Local run
2525

2626
- goto submission folder, run `npm i` first
27-
- just run `npm run start`, and use browers open http://127.0.0.1:3003
27+
- just run `npm run start`, and use browsers open http://127.0.0.1:3003
2828

2929
### Configs
3030

@@ -43,26 +43,24 @@ This project was generated with [Angular CLI](https://github.com/angular/angular
4343

4444
- Angular 6
4545
- Angular Material https://material.angular.io/
46+
- Ngrx
4647
- Material Icons https://material.io/tools/icons/?style=baseline
4748

4849
- Backend
4950

50-
- express
51+
- hapi.js
5152
- ioredis https://github.com/luin/ioredis
5253

5354
- Commands
5455

5556
- what commands are included ?
5657

57-
INFO,GET,SET,RPUSH,SADD,ZADD,HMSET,LRANGE,ZRANGE,SMEMBERS,HGETALL,LLEN, SCARD, ZCARD,HLEN
58-
59-
- what commands should be added next?
60-
61-
- SREM remove one element from set
62-
- ZREM remove one element from ordered set
63-
- HSET, HMSET, HDEL, update/delete single/values from hash map
64-
- Other commands may need accroding to functions.
58+
INFO,GET,SET,RPUSH,SADD,ZADD,HMSET,LRANGE,ZRANGE,SMEMBERS,HGETALL,LLEN, SCARD, ZCARD,HLEN,SREM,ZREM,HSET,HMSET,HDEL
59+
60+
- what commands should be added next?
61+
62+
- Other commands may need according to functions.
6563

6664
- which commands are dependent on one another?
6765

68-
a web app function dependent a lot of command, so, in fact, there is no command dependent on one another command.
66+
a web app function dependent a lot of command, so, in fact, there is no command dependent on one another command.

‎app.js

Lines changed: 61 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,77 @@
1-
/**
2-
* Copyright (C) 2017 TopCoder Inc., All Rights Reserved.
3-
*/
4-
/**
5-
* The application entry point
6-
*
7-
* @author TCSCODER
8-
* @version 1.0
9-
*/
10-
11-
12-
13-
14-
const express = require('express');
15-
const cross = require('cors');
16-
const bodyParser = require('body-parser');
17-
const _ = require('lodash');
181
const config = require('config');
19-
const http = require('http');
20-
const path = require('path');
21-
const logger = require('./lib/common/logger');
22-
const errorMiddleware = require('./lib/common/error.middleware');
23-
const routes = require('./lib/route');
2+
const _ = require('lodash');
243

25-
const app = express();
26-
const httpServer = http.Server(app);
4+
const HApi = require('hapi');
5+
const routes = require('./lib/route');
6+
const logger = require('./lib/common/logger');
277

8+
// Create a server with a host and port
9+
const server = HApi.server({
10+
port: config.PORT,
11+
});
2812

29-
app.set('port', config.PORT);
30-
app.use(bodyParser.json());
31-
app.use(bodyParser.urlencoded({extended: true}));
32-
app.use(cross());
33-
const apiRouter = express.Router({});
3413

35-
// load all routes
36-
_.each(routes, (verbs, url) => {
37-
_.each(verbs, (def, verb) => {
38-
let actions = [];
14+
/**
15+
* inject cors headers
16+
*/
17+
const injectHeader = (h) => {
18+
h.header("Access-Control-Allow-Origin", "*");
19+
h.header("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS,POST,PUT");
20+
h.header("Access-Control-Allow-Headers", "If-Modified-Since, Origin, X-Requested-With, Content-Type, Accept, Authorization");
21+
return h;
22+
};
3923

40-
const {method} = def;
41-
if (!method) {
42-
throw new Error(`${verb.toUpperCase()} ${url} method is undefined`);
43-
}
44-
if (def.middleware && def.middleware.length > 0) {
45-
actions = actions.concat(def.middleware);
46-
}
24+
/**
25+
* inject routes
26+
*/
27+
_.each(routes, (route, path) => {
28+
const newPath = '/backend/' + config.API_VERSION + path;
29+
server.route({method: 'options', path: newPath, handler: (req, h) => injectHeader(h.response('ok'))});
30+
_.each(route, (handler, method) => {
4731

48-
actions.push(async (req, res, next) => {
49-
try {
50-
await method(req, res, next);
51-
} catch (e) {
52-
next(e);
32+
logger.info(`endpoint added, [${method.toUpperCase()}] ${newPath}`);
33+
server.route({
34+
method,
35+
path: newPath,
36+
handler: async (req, h) => {
37+
let result = {};
38+
let status = 200;
39+
try {
40+
result = await handler.method(req, h);
41+
} catch (e) {
42+
result = {code: e.status, message: e.message}
43+
status = e.status || 500;
44+
}
45+
return injectHeader(h.response(result).code(status));
5346
}
5447
});
55-
56-
const middlewares = [];
57-
for (let i = 0; i < actions.length - 1; i += 1) {
58-
if (actions[i].name.length !== 0) {
59-
middlewares.push(actions[i].name);
60-
}
61-
}
62-
63-
logger.info(`Endpoint discovered : [${middlewares.join(',')}] ${verb.toLocaleUpperCase()} /${config.API_VERSION}${url}`);
64-
apiRouter[verb](`/${config.API_VERSION}${url}`, actions);
6548
});
6649
});
67-
app.use('/backend/', apiRouter);
68-
app.use(errorMiddleware());
6950

70-
// Serve static assets
71-
app.use(express.static(path.resolve(__dirname, 'dist')));
72-
// Always return the main index.html
73-
app.get('/', (req, res) => {
74-
res.sendFile(path.resolve(__dirname, 'dist', 'index.html'));
75-
});
7651

52+
// Start the server
53+
async function start() {
54+
try {
55+
await server.register(require('inert'));
7756

78-
(async () => {
79-
if (!module.parent) { // this code will never run in unit test mode
80-
httpServer.listen(app.get('port'), () => {
81-
logger.info(`Express server listening on port ${app.get('port')}`);
57+
// add static folder
58+
server.route({
59+
method: 'GET',
60+
path: '/{param*}',
61+
handler: {
62+
directory: {
63+
path: 'dist',
64+
}
65+
}
8266
});
83-
} else {
84-
module.exports = app;
67+
await server.start();
8568
}
86-
})();
69+
catch (err) {
70+
logger.error(err);
71+
process.exit(1);
72+
}
73+
}
74+
75+
start().then(() => {
76+
logger.info('Server running at: ' + server.info.uri);
77+
});

‎lib/common/error.middleware.js

Lines changed: 0 additions & 38 deletions
This file was deleted.

‎lib/route.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,24 @@
1111

1212

1313
const redis = require('./redis');
14+
const logger = require('./common/logger');
15+
16+
logger.buildService("RedisService", redis);
1417

1518
module.exports = {
1619
'/redis/connect': {
1720
post: {
18-
method: async (req, res) => res.json(await redis.connect(req.body)),
21+
method: async req => await redis.connect(req.payload),
1922
},
2023
},
2124
'/redis/fetch': {
2225
get: {
23-
method: async (req, res) => res.json(await redis.fetchTree(req.query)),
26+
method: async req => await redis.fetchTree(req.query),
2427
}
2528
},
2629
'/redis/call': {
2730
post: {
28-
method: async (req, res) => res.json(await redis.call(req.query, req.body)),
31+
method: async req => await redis.call(req.query, req.payload),
2932
}
3033
}
3134
};

‎package-lock.json

Lines changed: 2901 additions & 2449 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@
2222
"@angular/platform-browser": "^6.1.0",
2323
"@angular/platform-browser-dynamic": "^6.1.0",
2424
"@angular/router": "^6.1.0",
25+
"@ngrx/effects": "^6.1.0",
26+
"@ngrx/store": "^6.1.0",
2527
"@ngx-loading-bar/http-client": "latest",
26-
"body-parser": "latest",
2728
"config": "^2.0.1",
2829
"core-js": "^2.5.4",
29-
"cors": "^2.8.4",
30-
"express": "^4.16.3",
3130
"get-parameter-names": "^0.3.0",
3231
"hammerjs": "^2.0.8",
32+
"hapi": "^17.5.4",
33+
"inert": "^5.1.0",
3334
"ioredis": "^4.0.0",
3435
"joi": "^13.6.0",
3536
"lodash": "latest",

‎src/app/app.component.html

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@
2626
</button>
2727
</div>
2828

29-
<div class="tree-list" *ngIf="instances && instances.length > 0">
30-
<app-instance-tree *ngFor="let instance of instances"
29+
30+
<div class="{{'tree-list ' + ((cli$ | async)?.expanded ? 'cli-expanded':'')}}">
31+
<app-instance-tree *ngFor="let instance of instances$ | async"
3132
(updatePage)="updatePage($event)"
3233
[instance]="instance"></app-instance-tree>
3334
</div>
@@ -40,23 +41,62 @@
4041
</div>
4142
<div class="right-part">
4243
<app-instance-root-panel
43-
[pageData]="currentPage"
44+
[pageData]="currentPage$ | async"
4445
(onDisconnect)="onDisconnect($event)"
4546
(onNewValue)="onNewValue($event)"
4647
[instance]=""
47-
*ngIf="currentPage.type === 'root-instance' && currentInstance.status === 'connected'"
48+
*ngIf=" (currentPage$| async)?.type === 'root-instance'"
4849
></app-instance-root-panel>
4950

5051
<app-data-viewer
5152
(onDeleteValue)="onDeleteValue()"
52-
*ngIf="currentPage.type === 'data-viewer'"
53-
[pageData]="currentPage"
53+
*ngIf="(currentPage$| async)?.type === 'data-viewer'"
54+
[pageData]="(currentPage$| async)"
5455
(onNewValue)="onNewValue($event)">
5556
</app-data-viewer>
5657
</div>
5758
</div>
5859

59-
<div class="cli">
6060

61+
<!--redis cli panel-->
62+
<div class="{{'cli mat-elevation-z12 ' + ((cli$ | async)?.expanded ? 'cli-expanded':'')}}">
63+
<div class="title">
64+
<span
65+
class="current-instance">{{currentInstance ? getShortName(currentInstance) : 'No redis instance select'}}</span>
66+
<span class="flex1"></span>
67+
68+
<button mat-icon-button (click)="clearHistory()"
69+
[disabled]="!currentInstance"
70+
matTooltip="Clear all history">
71+
<i class="material-icons">clear_all</i>
72+
</button>
73+
74+
<button mat-icon-button (click)="toggleCli()"
75+
[disabled]="!currentInstance"
76+
matTooltip="Toggle Cli panel">
77+
<i class="material-icons">{{(cli$ | async)?.expanded ? 'keyboard_arrow_down':'keyboard_arrow_up'}}</i>
78+
</button>
79+
80+
</div>
81+
82+
<div class="scroll-content" *ngIf="(cli$ | async)?.expanded" #cliScrollContent>
83+
<div class="item" *ngFor="let item of (cli$ | async)?.items">
84+
<div class="command">
85+
<span class="time">[{{item.time | date:'medium'}}] ></span>
86+
<span (click)="onRawContentClick(item.rawCommand)">{{item.rawCommand}}</span></div>
87+
<div class="{{'result ' + (item.error ? 'error':'')}}">{{item.result}}</div>
88+
</div>
89+
</div>
90+
91+
<div class="input">
92+
<mat-form-field class="example-full-width">
93+
<input matInput placeholder="{{!currentInstance ?'Please select a redis instance to Enter Redis command'
94+
: 'Enter Redis command'}}" value=""
95+
(keydown)="onCliInputKeyDown($event)"
96+
[(ngModel)]="cliInputValue"
97+
(focus)="onCliInputFocus()"
98+
[readonly]="!currentInstance">
99+
</mat-form-field>
100+
</div>
61101
</div>
62102
</div>

‎src/app/app.component.scss

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ $header-height: 60px;
55
height: 100%;
66
width: 100%;
77
flex-direction: column;
8+
position: relative;
89
.header {
910
z-index: 1;
1011
width: 100%;
@@ -21,10 +22,14 @@ $header-height: 60px;
2122
.left-part {
2223
position: relative;
2324
padding-right: 12px;
25+
2426
.tree-list {
2527
padding-left: 6px;
26-
height: calc(100vh - 60px - 48px);
27-
overflow: auto;
28+
overflow-y: auto;
29+
height: calc(100vh - 60px - 42px - 80px);
30+
&.cli-expanded {
31+
height: calc(100vh - 60px - 42px - 420px);
32+
}
2833
}
2934
.side-header {
3035
display: flex;
@@ -51,6 +56,72 @@ $header-height: 60px;
5156
}
5257
}
5358
.cli {
59+
height: 80px;
60+
background-color: white;
61+
position: fixed;
62+
left: 0;
63+
right: 0;
64+
bottom: 0;
65+
&.cli-expanded {
66+
height: 420px;
67+
}
68+
.title {
69+
display: flex;
70+
flex-direction: row;
71+
align-items: center;
72+
justify-content: center;
73+
height: 32px;
74+
75+
.current-instance {
76+
height: 22px;
77+
padding-left: 18px;
78+
padding-right: 18px;
79+
background-color: #4054b2;
80+
font-size: 14px;
81+
color: white;
82+
display: flex;
83+
align-items: center;
84+
justify-content: center;
85+
border-radius: 11px;
86+
}
87+
}
88+
89+
.scroll-content {
90+
margin-top: 5px;
91+
margin-bottom: 5px;
92+
height: 325px;
93+
overflow-y: auto;
94+
padding-left: 32px;
95+
96+
.item {
97+
.command {
98+
font-size: 14px;
99+
font-weight: bold;
100+
101+
.time {
102+
font-size: 12px;
103+
color: gray;
104+
font-weight: normal;
105+
margin-right: 6px;
106+
}
107+
}
108+
.result {
109+
margin-top: 2px;
110+
font-size: 12px;
111+
color: gray;
112+
113+
&.error {
114+
color: red;
115+
}
116+
}
117+
margin-bottom: 6px;
118+
}
119+
}
120+
121+
.input, .title {
122+
padding-left: 32px;
123+
padding-right: 32px;
124+
}
54125

55126
}
56127
}

‎src/app/app.component.ts

Lines changed: 162 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
1-
import {Component, OnInit} from '@angular/core';
1+
import {Component, ElementRef, OnInit, ViewChild} from '@angular/core';
22
import {MatDialog} from '@angular/material';
33
import {AddServerDialogComponent} from './components/add-server-dialog/add-server-dialog.component';
44
import {RedisInstance} from './models/redis-instance';
55
import uuid from 'uuid';
66
import {RedisService} from './services/redis.service';
7-
import _ from 'lodash';
87
import {UtilService} from './services/util.service';
8+
import {Store} from '@ngrx/store';
9+
import {
10+
ADD_REDIS_SERVER,
11+
DESELECT_ALL_REDIS,
12+
REDIS_DISCONNECT, REQ_FETCH_TREE,
13+
REQ_REDIS_CONNECT,
14+
SELECT_REDIS
15+
} from './ngrx/actions/redis-actions';
16+
import {Observable} from 'rxjs';
17+
import {REQ_LOAD_PAGE, REQ_LOAD_ROOT_PAGE} from './ngrx/actions/page-actions';
18+
import {PageModel} from './models/page-model';
19+
import {ADD_COMMAND, CLEAR_HISTORY, TOGGLE_CLI} from './ngrx/actions/cli-actions';
920

1021
/**
1122
* return a new right page component
@@ -28,10 +39,13 @@ const getNewPage = () => ({
2839
})
2940
export class AppComponent implements OnInit {
3041
title = 'REDIS MANAGER GUI TOOLS';
31-
instances = [];
42+
instances$: Observable<RedisInstance[]> = null;
43+
currentPage$: Observable<PageModel> = null;
44+
cli$: Observable<any> = null;
45+
currentInstance = null;
46+
cliInputValue = '';
3247

33-
public currentPage = getNewPage();
34-
public currentInstance = null;
48+
@ViewChild('cliScrollContent') private cliScrollContent: ElementRef;
3549

3650
public dragObject = {
3751
minWidth: 250,
@@ -45,47 +59,22 @@ export class AppComponent implements OnInit {
4559
constructor(public dialogService: MatDialog,
4660
private redisService: RedisService,
4761
private util: UtilService,
62+
private _store: Store<any>
4863
) {
49-
50-
const redisInstance = new RedisInstance();
51-
redisInstance.serverModel = {name: 'default-local', ip: 'localhost', port: 6379, db: 0, password: ''};
52-
redisInstance.id = uuid();
53-
this.instances.push(redisInstance);
54-
this.connectInstance(redisInstance);
55-
}
56-
57-
/**
58-
* connect a redis instance
59-
* @param redisInstance the redis instance data
60-
* @param {() => null} scb the success callback
61-
*/
62-
connectInstance(redisInstance, scb = () => null) {
63-
redisInstance.status = 'connecting';
64-
redisInstance.working = true;
65-
this.redisService.connect(redisInstance).subscribe(result => {
66-
this.getInstanceById(result.id).status = 'connected';
67-
redisInstance.working = false;
68-
scb();
69-
}, e => {
70-
this.getInstanceById(redisInstance.id).status = 'failed';
71-
redisInstance.working = false;
72-
this.util.showMessage(`redis ${redisInstance.id} connect failed`);
64+
this.instances$ = this._store.select('redis');
65+
this.currentPage$ = this._store.select('page');
66+
this.cli$ = this._store.select('cli');
67+
this.instances$.subscribe((instances) => {
68+
this._store.dispatch({type: REQ_REDIS_CONNECT, payload: {instance: instances[0]}});
7369
});
7470
}
7571

76-
/**
77-
* refresh and expand
78-
*/
79-
refreshAndExpand() {
80-
81-
}
82-
83-
/**
84-
* get redis instance by id
85-
* @param id the instance id
86-
*/
87-
getInstanceById(id) {
88-
return this.instances.find(ins => ins.id === id) || {};
72+
findInstance(id) {
73+
return new Promise(resolve => {
74+
this.instances$.subscribe(instances => {
75+
resolve(instances.find(ins => ins.id === id) || {});
76+
});
77+
});
8978
}
9079

9180
/**
@@ -98,13 +87,9 @@ export class AppComponent implements OnInit {
9887
});
9988
ref.afterClosed().subscribe(result => {
10089
if (result) {
101-
const instance = new RedisInstance();
102-
instance.id = uuid();
103-
instance.serverModel = result;
104-
this.instances.push(instance);
105-
this.connectInstance(instance, () => {
106-
this.util.showMessage('redis connect successful');
107-
});
90+
const instance = {id: uuid(), serverModel: result};
91+
this._store.dispatch({type: ADD_REDIS_SERVER, payload: instance}); // add new server
92+
this._store.dispatch({type: REQ_REDIS_CONNECT, payload: {instance}}); // connect
10893
}
10994
});
11095
}
@@ -113,30 +98,21 @@ export class AppComponent implements OnInit {
11398
* on refresh event
11499
*/
115100
onRefresh() {
116-
if (!this.currentInstance) {
117-
this.util.showMessage('you need select Redis instance first');
118-
return;
119-
}
120-
121-
const instance = this.currentInstance;
122-
instance.working = true;
123-
instance.status = 'connecting';
124-
this.redisService.connect(instance).subscribe(() => {
125-
instance.status = 'connected';
126-
if (instance.expanded) {
127-
instance.working = true;
128-
this.redisService.fetchTree({id: instance.id}).subscribe(ret => {
129-
instance.children = ret;
130-
instance.working = false;
131-
}, () => {
132-
instance.working = false;
133-
});
134-
} else {
135-
instance.working = false;
101+
this.instances$.subscribe(instances => {
102+
const ins = instances.find(i => i.selected === true);
103+
if (!ins) {
104+
this.util.showMessage('you need select Redis instance first');
105+
return;
136106
}
137-
}, e => {
138-
instance.status = 'failed';
139-
instance.working = false;
107+
this._store.dispatch({
108+
type: REQ_REDIS_CONNECT, payload: {
109+
instance: ins, scb: () => {
110+
if (ins.expanded) {
111+
this._store.dispatch({type: REQ_FETCH_TREE, payload: {id: ins.id}});
112+
}
113+
}
114+
}
115+
});
140116
});
141117
}
142118

@@ -163,13 +139,8 @@ export class AppComponent implements OnInit {
163139
* @param id the redis instance id
164140
*/
165141
onDisconnect(id) {
166-
this.currentPage = getNewPage();
167-
if (this.getInstanceById(id)) {
168-
this.getInstanceById(id).expanded = false;
169-
this.getInstanceById(id).status = null;
170-
this.getInstanceById(id).selected = false;
171-
}
172-
this.currentInstance = null;
142+
this._store.dispatch({type: REDIS_DISCONNECT, payload: {id}});
143+
this._store.dispatch({type: REQ_LOAD_PAGE, payload: getNewPage()});
173144
}
174145

175146
/**
@@ -193,7 +164,10 @@ export class AppComponent implements OnInit {
193164
* on delete value (succeed)
194165
*/
195166
onDeleteValue() {
196-
this.currentPage = getNewPage();
167+
this._store.dispatch({type: REQ_LOAD_PAGE, payload: getNewPage()});
168+
}
169+
170+
refreshAndExpand() {
197171
}
198172

199173
/**
@@ -204,49 +178,119 @@ export class AppComponent implements OnInit {
204178
const {id, type} = page;
205179
if (page.type === 'root-instance') { // show server information
206180
this.requireId = uuid();
207-
this.instances.forEach(i => i.selected = false);
208-
this.currentInstance = this.getInstanceById(id);
209-
if (!this.currentInstance) {
210-
this.util.showMessage('cannot found redis instance id = ' + id);
211-
return;
212-
}
213-
this.currentInstance.selected = true;
214-
const rId = _.clone(this.requireId);
215-
this.currentPage = {
216-
id, type,
217-
loading: true,
218-
item: [],
219-
};
220-
this.connectInstance(this.currentInstance, () => {
221-
this.redisService.call(id, [['info']]).subscribe(ret => {
222-
const rawInfo = ret[0];
223-
const result = [];
224-
rawInfo.split('\n').forEach(line => {
225-
if (line.indexOf('#') === 0) {
226-
return;
181+
this._store.dispatch({type: DESELECT_ALL_REDIS});
182+
this._store.dispatch({type: SELECT_REDIS, payload: {id}});
183+
this.findInstance(id).then(instance => {
184+
if (!instance['id']) {
185+
this.util.showMessage(`cannot found redis instance where id= ${id}`);
186+
return;
187+
}
188+
this.currentInstance = instance;
189+
this._store.dispatch({
190+
type: REQ_REDIS_CONNECT, payload: {
191+
instance: instance, scb: () => {
192+
this._store.dispatch({
193+
type: REQ_LOAD_ROOT_PAGE, payload: {
194+
id, type, loading: true, item: [],
195+
requestId: uuid(),
196+
}
197+
});
227198
}
228-
if (line.trim() === '') {
229-
return;
230-
}
231-
const parts = line.split(':');
232-
result.push({
233-
key: parts[0].split('_').join(' '),
234-
value: parts[1],
235-
});
236-
});
237-
if (rId === this.requireId) { // page didn't update
238-
this.currentPage.item = result;
239-
this.currentPage.loading = false;
240199
}
241-
}, () => {
242-
this.getInstanceById(id).status = 'failed';
243-
return this.util.showMessage('redis instance not exist');
244200
});
245201
});
246202
} else if (page.type === 'data-viewer') {
247-
this.currentPage = {
248-
id, type, loading: true, item: page.item
249-
};
203+
this._store.dispatch({type: REQ_LOAD_PAGE, payload: {id, type, loading: true, item: page.item}});
204+
}
205+
}
206+
207+
/**
208+
* get short name of a redis instance
209+
* @param instance the redis instance
210+
*/
211+
getShortName(instance) {
212+
return `${instance.serverModel.name}(${instance.serverModel.ip}:${instance.serverModel.port}:${instance.serverModel.db})`;
213+
}
214+
215+
/**
216+
* toggle cli panel
217+
*/
218+
toggleCli() {
219+
this._store.dispatch({type: TOGGLE_CLI});
220+
}
221+
222+
/**
223+
* clear cli history
224+
*/
225+
clearHistory() {
226+
this._store.dispatch({type: CLEAR_HISTORY});
227+
}
228+
229+
230+
/**
231+
* on Command added callback
232+
* @param err is command added failed
233+
*/
234+
onCommandAdded(err) {
235+
this.onRefresh();
236+
setTimeout(() => {
237+
try {
238+
this.cliScrollContent.nativeElement.scrollTop = this.cliScrollContent.nativeElement.scrollHeight;
239+
} catch (e) {
240+
241+
}
242+
}, 200);
243+
}
244+
245+
/**
246+
* in cli input focus
247+
*/
248+
onCliInputFocus() {
249+
this.cli$.subscribe(c => {
250+
if (!c.expanded && this.currentInstance) {
251+
this.toggleCli();
252+
}
253+
});
254+
}
255+
256+
/**
257+
* on raw content click, put this raw content into input box
258+
* @param content the raw input content
259+
*/
260+
onRawContentClick(content) {
261+
}
262+
263+
264+
/**
265+
* on cli input key down
266+
* @param evt the event
267+
*/
268+
onCliInputKeyDown(evt) {
269+
if (evt.key === 'Enter') {
270+
const v = this.cliInputValue.trim();
271+
this.cliInputValue = '';
272+
if (v === '') { // empty input
273+
return;
274+
}
275+
276+
const command = v.split(' ').filter(c => c.trim() !== ''); // combine into a command
277+
const id = uuid();
278+
this._store.dispatch({ // dispatch
279+
type: ADD_COMMAND, payload: {
280+
redisId: this.currentInstance.id,
281+
id,
282+
command,
283+
cb: (err) => this.onCommandAdded(err),
284+
item: {
285+
status: 'new',
286+
id,
287+
time: new Date(),
288+
rawCommand: v,
289+
command,
290+
result: ['running, please wait ...'],
291+
},
292+
}
293+
});
250294
}
251295
}
252296
}

‎src/app/app.module.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ import {AddValueFormComponent} from './components/add-value-form/add-value-form.
4848
import {DataViewerComponent} from './components/data-viewer/data-viewer.component';
4949
import {TreeNodeComponent} from './components/tree-node/tree-node.component';
5050
import {ImportDataDialogComponent} from './components/import-data-dialog/import-data-dialog.component';
51+
import {EffectsModule} from '@ngrx/effects';
52+
import {StoreModule} from '@ngrx/store';
53+
54+
import {RedisEffect} from './ngrx/effects/redis-effect';
55+
import {PageEffect} from './ngrx/effects/page-effect';
56+
import {CliEffect} from './ngrx/effects/cli-effect';
57+
import {reducer as redisReducer} from './ngrx/reducer/redis-reducer';
58+
import {reducer as pageReducer} from './ngrx/reducer/page-reducer';
59+
import {reducer as cliReducer} from './ngrx/reducer/cli-reducer';
60+
5161

5262
@NgModule({
5363
declarations: [
@@ -99,7 +109,13 @@ import {ImportDataDialogComponent} from './components/import-data-dialog/import-
99109
MatSnackBarModule,
100110
MatTableModule,
101111
MatSortModule,
102-
MatPaginatorModule
112+
MatPaginatorModule,
113+
StoreModule.forRoot({
114+
redis: redisReducer,
115+
page: pageReducer,
116+
cli: cliReducer,
117+
}),
118+
EffectsModule.forRoot([RedisEffect, PageEffect, CliEffect]),
103119
],
104120
providers: [
105121
HttpHelperService,

‎src/app/components/add-value-dialog/add-value-dialog.component.html

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<h1 mat-dialog-title>Add new value</h1>
1+
<h1 mat-dialog-title>{{isEditMode()?'Edit value':'Add new value'}}</h1>
22
<div mat-dialog-content>
33
<mat-form-field>
44
<input matInput placeholder="Key" [readonly]="data.hideType" [(ngModel)]="data.key">
@@ -14,11 +14,15 @@ <h1 mat-dialog-title>Add new value</h1>
1414
</mat-form-field>
1515

1616
<app-add-value-form [type]="data.type"
17+
[initValues]="data.values.values"
18+
[initHashMapValues]="data.values.hashMapValues"
19+
[initOrderedValues]="data.values.orderedValues"
20+
[isEditMode]="isEditMode()"
1721
(onValueUpdate)="data.values = $event">
1822

1923
</app-add-value-form>
2024
</div>
2125
<div mat-dialog-actions>
2226
<button mat-button [mat-dialog-close]="null">Cancel</button>
23-
<button mat-raised-button color="primary" (click)="onAdd()">Add</button>
27+
<button mat-raised-button color="primary" (click)="onAdd()">{{isEditMode()?'Update':'Add'}}</button>
2428
</div>

‎src/app/components/add-value-dialog/add-value-dialog.component.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export class ValueMode {
1111
key: string;
1212
type: string;
1313
hideType: boolean;
14+
isEditMode = false;
1415
values = {
1516
values: [{value: ''}],
1617
orderedValues: [{value: '', score: '0'}],
@@ -41,6 +42,9 @@ export class AddValueDialogComponent implements OnInit {
4142
ngOnInit() {
4243
}
4344

45+
isEditMode() {
46+
return this.data.isEditMode;
47+
}
4448
/**
4549
* show a error message
4650
* @param msg the error message text

‎src/app/components/add-value-form/add-value-form.component.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
<mat-form-field *ngIf="type === 'Hash Map'">
2525
<input matInput placeholder="Key"
26+
[readonly]="isEditMode"
2627
(change)="onValueChange()"
2728
[(ngModel)]="getItemArray()[i].key">
2829
</mat-form-field>
@@ -33,13 +34,13 @@
3334
[(ngModel)]="getItemArray()[i].value">
3435
</mat-form-field>
3536

36-
<button mat-icon-button (click)="onRemoveItem(getItemArray(),i)">
37+
<button mat-icon-button (click)="onRemoveItem(getItemArray(),i)" [disabled]="isEditMode">
3738
<i class="material-icons delete-icon">delete</i>
3839
</button>
3940
</div>
4041

4142
<div class="line buttons">
42-
<button mat-button color="primary" (click)="onAddItem(getItemArray())">
43+
<button mat-button color="primary" (click)="onAddItem(getItemArray())" [disabled]="isEditMode">
4344
<i class="material-icons">add</i>Add new value
4445
</button>
4546
</div>

‎src/app/components/add-value-form/add-value-form.component.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
2-
2+
import _ from 'lodash';
33

44
/**
55
* the new value form dialog component
@@ -11,13 +11,16 @@ import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
1111
})
1212
export class AddValueFormComponent implements OnInit {
1313
@Input() type = null;
14+
@Input() initValues = null;
15+
@Input() initOrderedValues = null;
16+
@Input() initHashMapValues = null;
17+
@Input() isEditMode = false;
1418
@Output() onValueUpdate = new EventEmitter();
1519

1620
values = [];
1721
orderedValues = [];
1822
hashMapValues = [];
1923

20-
2124
constructor() {
2225
}
2326

@@ -39,9 +42,9 @@ export class AddValueFormComponent implements OnInit {
3942
* the component init
4043
*/
4144
ngOnInit() {
42-
this.values = [{value: ''}];
43-
this.orderedValues = [{value: '', score: 0}];
44-
this.hashMapValues = [{value: '', key: ''}];
45+
this.values = this.initValues ? _.clone(this.initValues) : [{value: ''}];
46+
this.orderedValues = this.initOrderedValues ? _.clone(this.initOrderedValues) : [{value: '', score: 0}];
47+
this.hashMapValues = this.initHashMapValues ? _.clone(this.initHashMapValues) : [{value: '', key: ''}];
4548
}
4649

4750
/**

‎src/app/components/data-viewer/data-viewer.component.html

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
(click)="onDelete()">Delete {{pageData.item.type === 'folder'?
1010
'Branch':'Key'}}
1111
</button>
12+
13+
<button mat-raised-button *ngIf="pageData.item.type==='hash'"
14+
color="primary"
15+
(click)="onEdit()">Edit
16+
</button>
1217
</div>
1318

1419
<div class="string-editor" *ngIf="pageData.item.type === 'string'">
@@ -30,8 +35,20 @@
3035
</button>
3136
</div>
3237

33-
<div class="table-content mat-elevation-z8" *ngIf="isNeedShowTable()">
38+
<div class="{{'table-content mat-elevation-z8 ' + ((cli$ | async)?.expanded ? 'cli-expanded':'')}}"
39+
*ngIf="isNeedShowTable()">
3440
<table mat-table [dataSource]="data" class="example-table">
41+
42+
<ng-container matColumnDef="checkbox">
43+
<th mat-header-cell *matHeaderCellDef></th>
44+
<td mat-cell *matCellDef="let element">
45+
<mat-checkbox
46+
(change)="selectedMap[key(element)] = !selectedMap[key(element)]"
47+
[checked]="selectedMap[key(element)]"
48+
color="primary"></mat-checkbox>
49+
</td>
50+
</ng-container>
51+
3552
<ng-container matColumnDef="index">
3653
<th mat-header-cell *matHeaderCellDef>Index</th>
3754
<td mat-cell *matCellDef="let row">{{row.index}}</td>
@@ -53,6 +70,23 @@
5370
<td mat-cell *matCellDef="let row">{{row.score}}</td>
5471
</ng-container>
5572

73+
<ng-container matColumnDef="actions" *ngIf="pageData.item.type !== 'list'">
74+
<th mat-header-cell *matHeaderCellDef>Actions</th>
75+
<td mat-cell *matCellDef="let element">
76+
77+
<button mat-icon-button matTooltip="Edit this item"
78+
*ngIf="pageData.item.type === 'hash'"
79+
(click)="onEditMapElements([element])">
80+
<i class="material-icons">edit</i>
81+
</button>
82+
83+
<button mat-icon-button matTooltip="Delete Forever" (click)="removeElement(element)">
84+
<i class="material-icons">delete_forever</i>
85+
</button>
86+
87+
88+
</td>
89+
</ng-container>
5690

5791
<tr mat-header-row *matHeaderRowDef="getColumns()"></tr>
5892
<tr mat-row *matRowDef="let row; columns: getColumns();"></tr>

‎src/app/components/data-viewer/data-viewer.component.scss

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
.table-content {
1414
margin: 6px 16px 16px 16px;
1515
position: relative;
16-
height: calc(100vh - 60px - 78px - 56px - 36px);
16+
height: calc(100vh - 60px - 78px - 56px - 36px - 80px);
1717
overflow: auto;
1818
.example-table {
1919
position: relative;
@@ -22,6 +22,10 @@
2222
.string-value{
2323
word-break: break-all;
2424
}
25+
26+
&.cli-expanded {
27+
height: calc(100vh - 60px - 78px - 56px - 36px - 420px);
28+
}
2529
}
2630

2731
.string-editor {

‎src/app/components/data-viewer/data-viewer.component.ts

Lines changed: 122 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import {ConfirmDialogComponent} from '../confirm-dialog/confirm-dialog.component
55
import {RedisService} from '../../services/redis.service';
66
import _ from 'lodash';
77
import {UtilService} from '../../services/util.service';
8+
import {Observable} from 'rxjs';
9+
import {Store} from '@ngrx/store';
10+
import {REQ_FETCH_TREE} from '../../ngrx/actions/redis-actions';
811

912
/**
1013
* the backend type to frontend type map
@@ -34,16 +37,21 @@ export class DataViewerComponent implements OnInit, OnChanges {
3437
pageIndex: 0,
3538
pageSize: 20,
3639
};
40+
cli$: Observable<any> = null;
3741
public data = [];
3842
public setCachedData = null;
3943
public hashCachedData = null;
44+
public selectedMap = {};
4045

4146
constructor(
4247
public dialogService: MatDialog,
4348
private snackBar: MatSnackBar,
4449
private redisService: RedisService,
4550
private util: UtilService,
51+
private _store: Store<any>,
4652
) {
53+
54+
this.cli$ = this._store.select('cli');
4755
}
4856

4957
ngOnInit() {
@@ -59,19 +67,84 @@ export class DataViewerComponent implements OnInit, OnChanges {
5967
return ['list', 'set', 'zset', 'hash'].findIndex(v => v === this.pageData.item.type) >= 0;
6068
}
6169

70+
/**
71+
* remove element
72+
* @param element the element
73+
*/
74+
removeElement(element) {
75+
const t = this.pageData.item.type;
76+
const pk = this.pageData.item.key;
77+
let values = [];
78+
if (element) {
79+
values = [this.key(element)];
80+
} else {
81+
_.each(this.selectedMap, (v, k) => {
82+
if (v) {
83+
values.push(k);
84+
}
85+
});
86+
}
87+
this.dialogService.open(ConfirmDialogComponent, {
88+
width: '250px', data: {
89+
title: 'Delete Confirm',
90+
message: `Are you sure you want delete ${element ? 'this' : 'select'} value${values.length > 1 ? 's' : ''} ?`
91+
}
92+
}).afterClosed().subscribe(ret => {
93+
if (ret) {
94+
let command = [];
95+
if (t === 'set') {
96+
command = ['SREM', pk];
97+
} else if (t === 'zset') {
98+
command = ['ZREM', pk];
99+
} else if (t === 'hash') {
100+
command = ['HDEL', pk];
101+
}
102+
command = command.concat(values);
103+
this.redisService.call(this.pageData.id, [command]).subscribe(() => {
104+
105+
_.each(values, v => {
106+
delete this.selectedMap[v];
107+
});
108+
109+
this._store.dispatch({type: REQ_FETCH_TREE, payload: {id: this.pageData.id}});
110+
this.hashCachedData = null;
111+
this.setCachedData = null;
112+
this.fetchData();
113+
this.util.showMessage('Delete successful');
114+
}, () => this.util.showMessage('Delete failed'));
115+
}
116+
});
117+
}
62118

63119
/**
64120
* get columns by type
65121
*/
66122
getColumns() {
67123
const t = this.pageData.item.type;
68124
if (t === 'list' || t === 'set') {
69-
return ['index', 'value'];
125+
let c = ['index', 'value'];
126+
if (t === 'set') {
127+
c = ['checkbox'].concat(c);
128+
c.push('actions');
129+
}
130+
return c;
70131
} else if (t === 'zset') {
71-
return ['index', 'value', 'score'];
132+
return ['checkbox', 'index', 'value', 'score', 'actions'];
72133
} else {
73-
return ['key', 'value'];
134+
return ['checkbox', 'key', 'value', 'actions'];
135+
}
136+
}
137+
138+
/**
139+
* get unique key for item
140+
* @param item
141+
*/
142+
key(item) {
143+
const t = this.pageData.item.type;
144+
if (t === 'hash') {
145+
return item.key;
74146
}
147+
return item.value;
75148
}
76149

77150

@@ -141,7 +214,7 @@ export class DataViewerComponent implements OnInit, OnChanges {
141214
/**
142215
* on add new record
143216
*/
144-
onAddNewRecords() {
217+
onAddNewRecords(values) {
145218
const viewMode = new ValueMode();
146219
viewMode.type = TYPE_MAP[this.pageData.item.type];
147220
viewMode.hideType = true;
@@ -152,6 +225,10 @@ export class DataViewerComponent implements OnInit, OnChanges {
152225
}
153226
viewMode.id = this.pageData.id;
154227
viewMode.key = this.pageData.item['key'];
228+
if (values) {
229+
viewMode.values = values;
230+
viewMode.isEditMode = true;
231+
}
155232
this.dialogService.open(AddValueDialogComponent, {
156233
width: '480px',
157234
data: viewMode,
@@ -160,9 +237,7 @@ export class DataViewerComponent implements OnInit, OnChanges {
160237
ret.onSuccess = () => {
161238
if (this.pageData.item.type === 'folder') {
162239
} else {
163-
if (this.pageData.item.type !== 'string') {
164-
this.pageData.item.len += ret.len;
165-
}
240+
this._store.dispatch({type: REQ_FETCH_TREE, payload: {id: this.pageData.id}});
166241
this.hashCachedData = null;
167242
this.setCachedData = null;
168243
this.fetchData();
@@ -212,9 +287,14 @@ export class DataViewerComponent implements OnInit, OnChanges {
212287
* on delete redis value
213288
*/
214289
onDelete() {
290+
if (_.some(this.selectedMap, v => v)) {
291+
this.removeElement(null);
292+
return;
293+
}
215294
this.dialogService.open(ConfirmDialogComponent, {
216-
width: '250px', data: {
295+
width: '320px', data: {
217296
title: 'Delete Confirm',
297+
message: `Are you sure you want delete all values that belongs to "${this.pageData.item.key}" ?`,
218298
}
219299
}).afterClosed().subscribe(ret => {
220300
if (ret) {
@@ -228,6 +308,7 @@ export class DataViewerComponent implements OnInit, OnChanges {
228308
this.util.showMessage('delete successful');
229309
this.pageData.item.deleted = true;
230310
this.onDeleteValue.emit();
311+
this._store.dispatch({type: REQ_FETCH_TREE, payload: {id: this.pageData.id}});
231312
}, e => {
232313
this.util.showMessage('delete failed');
233314
console.error(e);
@@ -236,6 +317,38 @@ export class DataViewerComponent implements OnInit, OnChanges {
236317
});
237318
}
238319

320+
/**
321+
* on edit map elements
322+
* @param elements the element arr
323+
*/
324+
onEditMapElements(elements) {
325+
this.onAddNewRecords({hashMapValues: JSON.parse(JSON.stringify(elements))});
326+
}
327+
328+
/**
329+
* on edit select values
330+
*/
331+
onEdit() {
332+
const keys = [];
333+
_.each(this.selectedMap, (v, k) => {
334+
if (v) {
335+
keys.push(k);
336+
}
337+
});
338+
339+
if (keys.length <= 0) {
340+
return this.util.showMessage('you need select some row first');
341+
}
342+
343+
if (this.pageData.item.type === 'hash') {
344+
const items = [];
345+
_.each(keys, k => {
346+
items.push(this.hashCachedData.find(i => i.key === k));
347+
});
348+
this.onEditMapElements(items);
349+
}
350+
}
351+
239352
/**
240353
* when page changed
241354
* @param page the page item
@@ -251,6 +364,7 @@ export class DataViewerComponent implements OnInit, OnChanges {
251364
ngOnChanges(changes: SimpleChanges): void {
252365
this.page.pageIndex = 0;
253366
this.data = [];
367+
this.selectedMap = {};
254368
this.setCachedData = null;
255369
this.hashCachedData = null;
256370
this.fetchData();

‎src/app/components/instance-root-panel/instance-root-panel.component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
<button mat-raised-button (click)="onAddNewRecords()">Add new Records</button>
44
<button mat-raised-button color="warn" (click)="disconnect()">Disconnect</button>
55
</div>
6-
<div class="table-content mat-elevation-z8" *ngIf="pageData.item && pageData.item.length > 0">
6+
<div class="{{'table-content mat-elevation-z8 ' + ((cli$ | async)?.expanded ? 'cli-expanded':'')}}"
7+
*ngIf="pageData.item && pageData.item.length > 0">
78
<table mat-table [dataSource]="getData()" class="example-table">
89
<!-- Number Column -->
910
<ng-container matColumnDef="key">

‎src/app/components/instance-root-panel/instance-root-panel.component.scss

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@
1313
.table-content {
1414
margin: 6px 16px 16px 16px;
1515
position: relative;
16-
height: calc(100vh - 60px - 78px - 56px - 36px);
16+
height: calc(100vh - 60px - 78px - 56px - 36px - 80px);
1717
overflow: auto;
1818
.example-table {
1919
position: relative;
2020
width: 100%;
2121
}
22+
&.cli-expanded {
23+
height: calc(100vh - 60px - 78px - 56px - 36px - 420px);
24+
}
2225
}
2326
}

‎src/app/components/instance-root-panel/instance-root-panel.component.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
22
import {MatDialog} from '@angular/material';
33
import {ConfirmDialogComponent} from '../confirm-dialog/confirm-dialog.component';
44
import {AddValueDialogComponent, ValueMode} from '../add-value-dialog/add-value-dialog.component';
5+
import {Store} from '@ngrx/store';
6+
import {Observable} from 'rxjs';
57

68

79
/**
@@ -18,13 +20,15 @@ export class InstanceRootPanelComponent implements OnInit {
1820
@Output() onDisconnect = new EventEmitter();
1921
@Output() onNewValue = new EventEmitter();
2022
displayedColumns: string[] = ['key', 'value'];
23+
cli$: Observable<any> = null;
2124

2225
page = {
2326
pageIndex: 0,
2427
pageSize: 20,
2528
};
2629

27-
constructor(public dialogService: MatDialog) {
30+
constructor(public dialogService: MatDialog, private _store: Store<any>) {
31+
this.cli$ = this._store.select('cli');
2832
}
2933

3034
ngOnInit() {

‎src/app/components/instance-tree/instance-tree.component.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
22
import {RedisInstance} from '../../models/redis-instance';
33
import {RedisService} from '../../services/redis.service';
4+
import {Store} from '@ngrx/store';
5+
import {REQ_FETCH_TREE, TOGGLE_REDIS} from '../../ngrx/actions/redis-actions';
46

57
/**
68
* a redis instance tree component
@@ -16,26 +18,20 @@ export class InstanceTreeComponent implements OnInit {
1618
public selectedMap = {};
1719
public expandedMap = {};
1820

19-
constructor(private redisService: RedisService) {
21+
constructor(private redisService: RedisService, private _store: Store<any>) {
2022
}
2123

2224
/**
2325
* on redis expand
2426
*/
2527
onExpand() {
28+
const id = this.instance.id;
2629
if (this.instance.expanded) {
27-
this.instance.expanded = false;
30+
this._store.dispatch({type: TOGGLE_REDIS, payload: {id}});
2831
return;
2932
}
30-
this.instance.working = true;
31-
this.redisService.fetchTree({id: this.instance.id}).subscribe(ret => {
32-
this.instance.working = false;
33-
this.instance.expanded = true;
34-
this.instance.children = ret;
35-
}, e => {
36-
this.instance.status = 'failed';
37-
this.instance.working = false;
38-
});
33+
this._store.dispatch({type: REQ_FETCH_TREE, payload: {id}});
34+
this._store.dispatch({type: TOGGLE_REDIS, payload: {id}});
3935
}
4036

4137
/**
@@ -63,7 +59,6 @@ export class InstanceTreeComponent implements OnInit {
6359
});
6460
}
6561

66-
6762
ngOnInit() {
6863
}
6964
}

‎src/app/models/cli-model.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* the cli command model
3+
*/
4+
export class CliModel {
5+
id: string;
6+
rawCommand: string;
7+
command: [any];
8+
time: Date;
9+
result: [any];
10+
status = 'new';
11+
}

‎src/app/models/page-model.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* the right page model
3+
*/
4+
export class PageModel {
5+
id: string;
6+
type: string;
7+
loading = true;
8+
item = [];
9+
requestId: string;
10+
}

‎src/app/models/redis-instance.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export class RedisInstance {
88
working = false;
99
serverModel: AddServerModel;
1010
id = '';
11+
selected = false;
1112
expanded = false;
1213
children = [];
14+
rootInfo = {};
1315
}

‎src/app/ngrx/actions/cli-actions.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* the cli actions
3+
*/
4+
export const ADD_COMMAND = 'ADD_COMMAND'; // when user enter command and press enter key
5+
export const COMMAND_RUN_FINISHED = 'COMMAND_RUN_FINISHED'; // when command send to server and returned result
6+
export const CLEAR_HISTORY = 'CLEAR_HISTORY'; // clear all cli history
7+
export const TOGGLE_CLI = 'TOGGLE_CLI'; // toggle cli panel
8+
9+
export default {
10+
ADD_COMMAND,
11+
COMMAND_RUN_FINISHED,
12+
CLEAR_HISTORY,
13+
TOGGLE_CLI,
14+
};

‎src/app/ngrx/actions/page-actions.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* the right page actions
3+
*/
4+
export const REQ_LOAD_PAGE = 'REQ_LOAD_PAGE'; // request to load new date viewer page
5+
export const LOADED_PAGE = 'LOADED_PAGE'; // page loaded
6+
export const REQ_LOAD_ROOT_PAGE = 'REQ_LOAD_ROOT_PAGE'; // request to load root page
7+
8+
export default {
9+
REQ_LOAD_PAGE, REQ_LOAD_ROOT_PAGE, LOADED_PAGE
10+
};

‎src/app/ngrx/actions/redis-actions.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* the redis instance actions
3+
*/
4+
export const REQ_REDIS_CONNECT = 'REQ_REDIS_CONNECT'; // request to connect a redis
5+
export const REDIS_CONNECT = 'REDIS_CONNECT'; // when a redis instance connected
6+
export const REDIS_CONNECT_FAILED = 'REDIS_CONNECT_FAILED'; // when a redis instance connect failed
7+
export const REDIS_DISCONNECT = 'REDIS_DISCONNECT'; // disconnect a redis
8+
export const DESELECT_ALL_REDIS = 'DESELECT_ALL_REDIS'; // de selected all redis instance
9+
export const SELECT_REDIS = 'SELECT_REDIS'; // select a redis instance
10+
export const REQ_FETCH_TREE = 'REQ_FETCH_TREE'; // request to fetch a redis instance tree node
11+
export const FETCHED_TREE = 'FETCHED_TREE'; // when fetch tree finished
12+
export const TOGGLE_REDIS = 'TOGGLE_REDIS'; // toggle redis instance
13+
export const ADD_REDIS_SERVER = 'ADD_REDIS_SERVER'; // add redis server
14+
15+
export default {
16+
REQ_REDIS_CONNECT,
17+
REDIS_CONNECT,
18+
REDIS_CONNECT_FAILED,
19+
20+
REDIS_DISCONNECT,
21+
SELECT_REDIS,
22+
DESELECT_ALL_REDIS,
23+
REQ_FETCH_TREE,
24+
FETCHED_TREE,
25+
TOGGLE_REDIS,
26+
ADD_REDIS_SERVER,
27+
};

‎src/app/ngrx/effects/cli-effect.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* the cli async effect function
3+
*/
4+
import {Effect, Actions, ofType} from '@ngrx/effects';
5+
import {of} from 'rxjs';
6+
import {RedisService} from '../../services/redis.service';
7+
8+
import {catchError, map, mergeMap} from 'rxjs/operators';
9+
import {Injectable} from '@angular/core';
10+
import {Observable} from 'rxjs';
11+
import {Action} from '@ngrx/store';
12+
import {UtilService} from '../../services/util.service';
13+
import {ADD_COMMAND, COMMAND_RUN_FINISHED} from '../actions/cli-actions';
14+
15+
16+
@Injectable()
17+
export class CliEffect {
18+
constructor(private actions$: Actions,
19+
private util: UtilService,
20+
private redisService: RedisService) {
21+
}
22+
23+
/**
24+
* send command to backend when dispatch "ADD_COMMAND"
25+
* and when backend returned, dispatch data to "COMMAND_RUN_FINISHED"
26+
*/
27+
@Effect()
28+
addCommand: Observable<Action> = this.actions$.pipe(
29+
ofType(ADD_COMMAND),
30+
mergeMap(action => {
31+
return this.redisService.call(
32+
action['payload'].redisId,
33+
[action['payload'].command]).pipe(
34+
map(ret => {
35+
if (action['payload'].cb) {
36+
action['payload'].cb(false);
37+
}
38+
return {
39+
type: COMMAND_RUN_FINISHED,
40+
payload: {
41+
result: ret[0],
42+
id: action['payload'].id,
43+
error: false,
44+
}
45+
};
46+
}),
47+
catchError((e) => {
48+
if (action['payload'].cb) {
49+
action['payload'].cb(true);
50+
}
51+
return of({
52+
type: COMMAND_RUN_FINISHED, payload:
53+
{
54+
id: action['payload'].id,
55+
result: [e.error && e.error.message ? e.error.message : 'failed'],
56+
error: true,
57+
}
58+
});
59+
})
60+
);
61+
}
62+
)
63+
);
64+
}

‎src/app/ngrx/effects/page-effect.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* the page async effect functions
3+
*/
4+
import {Effect, Actions, ofType} from '@ngrx/effects';
5+
import {of} from 'rxjs';
6+
import {RedisService} from '../../services/redis.service';
7+
8+
9+
import {LOADED_PAGE, REQ_LOAD_ROOT_PAGE} from '../actions/page-actions';
10+
import {REDIS_CONNECT_FAILED} from '../actions/redis-actions';
11+
12+
import {catchError, map, mergeMap} from 'rxjs/operators';
13+
import {Injectable} from '@angular/core';
14+
import {Observable} from 'rxjs';
15+
import {Action} from '@ngrx/store';
16+
import {UtilService} from '../../services/util.service';
17+
18+
19+
@Injectable()
20+
export class PageEffect {
21+
constructor(private actions$: Actions,
22+
private util: UtilService,
23+
private redisService: RedisService) {
24+
}
25+
26+
/**
27+
* send command to backend when dispatch "REQ_LOAD_ROOT_PAGE"
28+
* and when backend returned, dispatch data to "LOADED_PAGE"
29+
*/
30+
@Effect()
31+
pageLoad: Observable<Action> = this.actions$.pipe(
32+
ofType(REQ_LOAD_ROOT_PAGE),
33+
mergeMap(action => {
34+
return this.redisService.call(
35+
action['payload'].id,
36+
[['info']]).pipe(
37+
map(ret => {
38+
const rawInfo = ret[0];
39+
const result = [];
40+
rawInfo.split('\n').forEach(line => {
41+
if (line.indexOf('#') === 0) {
42+
return;
43+
}
44+
if (line.trim() === '') {
45+
return;
46+
}
47+
const parts = line.split(':');
48+
result.push({
49+
key: parts[0].split('_').join(' '),
50+
value: parts[1],
51+
});
52+
});
53+
return {type: LOADED_PAGE, payload: {item: result, requestId: action['payload'].requestId, id: action['payload'].id}};
54+
}),
55+
catchError(() => {
56+
const id = action['payload'].id;
57+
return of({type: REDIS_CONNECT_FAILED, payload: {id}});
58+
})
59+
);
60+
}
61+
)
62+
);
63+
}

‎src/app/ngrx/effects/redis-effect.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* the redis instance async functions
3+
*/
4+
5+
import {Effect, Actions, ofType} from '@ngrx/effects';
6+
import {of} from 'rxjs';
7+
import {RedisService} from '../../services/redis.service';
8+
9+
10+
import {FETCHED_TREE, REDIS_CONNECT, REDIS_CONNECT_FAILED, REQ_FETCH_TREE, REQ_REDIS_CONNECT} from '../actions/redis-actions';
11+
import {catchError, map, mergeMap} from 'rxjs/operators';
12+
import {Injectable} from '@angular/core';
13+
import {Observable} from 'rxjs';
14+
import {Action} from '@ngrx/store';
15+
import {UtilService} from '../../services/util.service';
16+
17+
18+
@Injectable()
19+
export class RedisEffect {
20+
constructor(private actions$: Actions,
21+
private util: UtilService,
22+
private redisService: RedisService) {
23+
}
24+
25+
/**
26+
* send connect request to backend when dispatch "REQ_REDIS_CONNECT"
27+
* and when backend returned, dispatch data to "REDIS_CONNECT"
28+
* when backend return error, dispatch to "REDIS_CONNECT_FAILED"
29+
*/
30+
@Effect()
31+
connectRedis: Observable<Action> = this.actions$.pipe(
32+
ofType(REQ_REDIS_CONNECT),
33+
mergeMap(action => {
34+
return this.redisService.connect(action['payload'].instance).pipe(
35+
map(data => {
36+
if (action['payload'].scb) {
37+
action['payload'].scb(data);
38+
}
39+
return {type: REDIS_CONNECT, payload: data};
40+
}),
41+
catchError(() => {
42+
if (action['payload'].fcb) {
43+
action['payload'].fcb(action['payload'].instance);
44+
}
45+
const id = action['payload'].instance.id;
46+
this.util.showMessage(`redis ${id} connect failed`);
47+
return of({type: REDIS_CONNECT_FAILED, payload: {id}});
48+
})
49+
);
50+
}
51+
)
52+
);
53+
54+
/**
55+
* send fetch tree request to backend when dispatch "REQ_FETCH_TREE"
56+
* and when backend returned, dispatch data to "FETCHED_TREE"
57+
* when backend return error, dispatch to "REDIS_CONNECT_FAILED"
58+
*/
59+
@Effect()
60+
fetchTree: Observable<Action> = this.actions$.pipe(
61+
ofType(REQ_FETCH_TREE),
62+
mergeMap(action => {
63+
const id = action['payload'].id;
64+
return this.redisService.fetchTree({id}).pipe(
65+
map(data => {
66+
return {type: FETCHED_TREE, payload: {id, data}};
67+
}),
68+
catchError(() => {
69+
return of({type: REDIS_CONNECT_FAILED, payload: {id}});
70+
})
71+
);
72+
}
73+
)
74+
);
75+
}

‎src/app/ngrx/reducer/cli-reducer.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* the cli reducer
3+
*/
4+
import actions from '../actions/cli-actions';
5+
6+
7+
// @ts-ignore
8+
export const initialState = {
9+
expanded: false,
10+
items: [],
11+
};
12+
13+
const getItemById = (id, state) => state.items.find(i => i.id === id);
14+
15+
export function reducer(state = initialState, action) {
16+
switch (action.type) {
17+
case actions.ADD_COMMAND: {
18+
state.items.push(action.payload.item);
19+
return state;
20+
}
21+
case actions.COMMAND_RUN_FINISHED: {
22+
const id = action.payload.id;
23+
const i = getItemById(id, state);
24+
i.result = action.payload.result;
25+
i.status = 'end';
26+
i.error = action.payload.error;
27+
return state;
28+
}
29+
case actions.CLEAR_HISTORY: {
30+
// clear all, and only keep un completed command
31+
state.items = state.items.filter(i => i.status === 'new');
32+
return state;
33+
}
34+
case actions.TOGGLE_CLI: {
35+
state.expanded = !state.expanded;
36+
return state;
37+
}
38+
default: {
39+
return state;
40+
}
41+
}
42+
}

‎src/app/ngrx/reducer/page-reducer.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* the page reducer
3+
*/
4+
import actions from '../actions/page-actions';
5+
import {PageModel} from '../../models/page-model';
6+
7+
8+
// @ts-ignore
9+
export const initialState: PageModel = {};
10+
11+
12+
export function reducer(state = initialState, action) {
13+
switch (action.type) {
14+
case actions.REQ_LOAD_ROOT_PAGE: {
15+
return action.payload;
16+
}
17+
case actions.REQ_LOAD_PAGE: {
18+
return action.payload;
19+
}
20+
case actions.LOADED_PAGE: {
21+
if (state.id === action.payload.id && state.requestId === action.payload.requestId) { // same page
22+
state.item = action.payload.item;
23+
state.loading = false;
24+
}
25+
return state;
26+
}
27+
default: {
28+
return state;
29+
}
30+
}
31+
}

‎src/app/ngrx/reducer/redis-reducer.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* the redis reducer
3+
*/
4+
import uuid from 'uuid';
5+
import actions from '../actions/redis-actions';
6+
import {RedisInstance} from '../../models/redis-instance';
7+
8+
9+
/**
10+
* default instance
11+
*/
12+
// @ts-ignore
13+
export const initialState: [RedisInstance]
14+
= [{serverModel: {name: 'default-local', ip: 'localhost', port: 6379, db: 0, password: ''}, id: uuid()}];
15+
16+
const getInstanceById = (id, state) => state.find(ins => ins.id === id) || {};
17+
18+
export function reducer(state = initialState, action) {
19+
switch (action.type) {
20+
case actions.REQ_REDIS_CONNECT: {
21+
const i = getInstanceById(action.payload.instance.id, state);
22+
i.status = 'connecting';
23+
i.working = true;
24+
return state;
25+
}
26+
case actions.REDIS_CONNECT_FAILED: {
27+
const i = getInstanceById(action.payload.id, state);
28+
i.status = 'failed';
29+
i.working = false;
30+
return state;
31+
}
32+
case actions.REDIS_CONNECT: {
33+
const i = getInstanceById(action.payload.id, state);
34+
i.status = 'connected';
35+
i.working = false;
36+
return state;
37+
}
38+
case actions.DESELECT_ALL_REDIS: {
39+
state.forEach(r => r.selected = false);
40+
return state;
41+
}
42+
case actions.SELECT_REDIS: {
43+
const i = getInstanceById(action.payload.id, state);
44+
i.selected = true;
45+
return state;
46+
}
47+
case actions.REDIS_DISCONNECT: {
48+
const i = getInstanceById(action.payload.id, state);
49+
i.expanded = false;
50+
i.status = null;
51+
i.selected = false;
52+
return state;
53+
}
54+
55+
case actions.REQ_FETCH_TREE: {
56+
const i = getInstanceById(action.payload.id, state);
57+
i.working = true;
58+
return state;
59+
}
60+
case actions.FETCHED_TREE: {
61+
const i = getInstanceById(action.payload.id, state);
62+
i.children = action.payload.data;
63+
i.working = false;
64+
return state;
65+
}
66+
case actions.TOGGLE_REDIS: {
67+
const i = getInstanceById(action.payload.id, state);
68+
i.expanded = !i.expanded;
69+
return state;
70+
}
71+
72+
case actions.ADD_REDIS_SERVER: {
73+
state.push(action.payload);
74+
return state;
75+
}
76+
default: {
77+
return state;
78+
}
79+
}
80+
}

‎src/app/services/http-helper.service.ts

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -41,44 +41,6 @@ export class HttpHelperService {
4141
.pipe(catchError(err => this.catchError(err)));
4242
}
4343

44-
/**
45-
* Performs a request with `put` http method.
46-
* @param url the url
47-
* @param body the body
48-
* @param options the request options
49-
* @returns {Observable<any>}
50-
*/
51-
put(url: string, body?: any, options?: any, isUpload?: boolean): Observable<any> {
52-
return this.http
53-
.put(API_BASE_URL + url, body, this.requestOptions(options, isUpload))
54-
.pipe(catchError(err => this.catchError(err)));
55-
}
56-
57-
/**
58-
* Performs a request with `put` http method.
59-
* @param url the url
60-
* @param body the body
61-
* @param options the request options
62-
* @returns {Observable<any>}
63-
*/
64-
patch(url: string, body?: any, options?: any, isUpload?: boolean): Observable<any> {
65-
return this.http
66-
.patch(API_BASE_URL + url, body, this.requestOptions(options, isUpload))
67-
.pipe(catchError(err => this.catchError(err)));
68-
}
69-
70-
/**
71-
* Performs a request with `delete` http method.
72-
* @param url the url
73-
* @param options the request options
74-
* @returns {Observable<any>}
75-
*/
76-
delete(url: string, options?: any): Observable<any> {
77-
return this.http.delete(API_BASE_URL + url, this.requestOptions(options))
78-
.pipe(catchError(err => this.catchError(err)));
79-
}
80-
81-
8244
/**
8345
* catches the auth error
8446
* @param error the error response

0 commit comments

Comments
 (0)
Please sign in to comment.