├── .babelrc ├── .circleci └── config.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── __mocks__ ├── electron.js ├── randomcolor.js ├── rethinkdb-mock.js └── rethinkdb-ts.js ├── app ├── main │ ├── db │ │ ├── __tests__ │ │ │ └── driver.test.js │ │ ├── driver.js │ │ ├── evalQuery.js │ │ ├── index.js │ │ ├── models │ │ │ ├── cluster.js │ │ │ ├── database.js │ │ │ ├── index.js │ │ │ ├── logs.js │ │ │ ├── server.js │ │ │ ├── stats.js │ │ │ └── table.js │ │ ├── queries │ │ │ ├── cluster.js │ │ │ ├── database.js │ │ │ ├── logs.js │ │ │ ├── server.js │ │ │ ├── stats.js │ │ │ └── table.js │ │ └── resolvers │ │ │ ├── actionResolver.js │ │ │ └── queryResolver.js │ ├── helpers │ │ ├── __tests__ │ │ │ └── url.test.js │ │ ├── constants.js │ │ └── url.js │ └── index.js ├── renderer │ ├── App.js │ ├── components │ │ ├── ActionsBar │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── Alert │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── AppHeader │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── Button │ │ │ └── index.js │ │ ├── Dropdown │ │ │ ├── dropdown.css │ │ │ └── index.js │ │ ├── Error │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── Icon │ │ │ ├── icons.js │ │ │ └── index.js │ │ ├── LogsPanel │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── Modal │ │ │ └── index.js │ │ ├── Page │ │ │ ├── Footer.js │ │ │ ├── Header.js │ │ │ ├── Info.js │ │ │ └── index.js │ │ ├── SideBar │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── Switch │ │ │ └── index.js │ │ ├── TimeChart │ │ │ └── index.js │ │ └── Toast │ │ │ ├── index.js │ │ │ └── toast.css │ ├── contexts │ │ ├── LogsContext.js │ │ └── StatsContext.js │ ├── helpers │ │ ├── __tests__ │ │ │ └── connectionStore.test.js │ │ ├── connectionStore.js │ │ ├── constants.js │ │ ├── format.js │ │ ├── storage.js │ │ └── stringHash.js │ ├── index.html │ ├── index.js │ ├── routes │ │ └── index.js │ ├── service │ │ ├── __mocks__ │ │ │ └── ipc.js │ │ ├── __tests__ │ │ │ └── connection.test.js │ │ ├── connection.js │ │ └── ipc.js │ ├── static │ │ ├── png │ │ │ └── rebirth_logo.png │ │ └── svg │ │ │ └── icons │ │ │ ├── cloud.svg │ │ │ ├── connections.svg │ │ │ ├── copy.svg │ │ │ ├── danger.svg │ │ │ ├── database.svg │ │ │ ├── error.svg │ │ │ ├── plus.svg │ │ │ ├── servers.svg │ │ │ └── table.svg │ ├── style │ │ ├── app.js │ │ └── common.js │ └── views │ │ ├── Connection │ │ ├── Connections │ │ │ ├── ConnectionItem.js │ │ │ ├── ConnectionItemActions.js │ │ │ ├── ConnectionList.js │ │ │ ├── ConnectionListHeader.js │ │ │ ├── __tests__ │ │ │ │ ├── ConnectionItem.test.js │ │ │ │ └── __snapshots__ │ │ │ │ │ └── ConnectionItem.test.js.snap │ │ │ └── styles.js │ │ ├── EditConnection │ │ │ ├── EditConnectionForm.js │ │ │ ├── index.js │ │ │ └── styles.js │ │ └── NewConnection │ │ │ ├── NewConnectionForm.js │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── Dashboard │ │ ├── Chart │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── Dashboard.js │ │ ├── DashboardPanels │ │ │ ├── Indexes.js │ │ │ ├── PanelContent.js │ │ │ ├── Panels.js │ │ │ ├── Resources.js │ │ │ ├── Servers.js │ │ │ ├── Tables.js │ │ │ └── panelStyles.js │ │ └── index.js │ │ ├── Explorer │ │ ├── index.js │ │ └── types.js │ │ ├── Home │ │ ├── index.js │ │ └── styles.js │ │ ├── Logs │ │ └── index.js │ │ ├── Servers │ │ └── index.js │ │ └── Tables │ │ ├── Database │ │ ├── DatabaseItem.js │ │ ├── DatabaseList.js │ │ └── styles.js │ │ ├── Modals │ │ ├── AddDatabase.js │ │ ├── AddTable.js │ │ ├── DeleteDatabase.js │ │ ├── DeleteTables.js │ │ └── styles.js │ │ ├── Table │ │ ├── TableItem.js │ │ └── styles.js │ │ ├── index.js │ │ └── tableHelpers.js └── shared │ └── channels.js ├── docs └── SystemTables.md ├── jest.config.js ├── main.js ├── package-lock.json ├── package.json ├── webpack.config.dev.js └── webpack.config.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": [], 3 | "presets": [["env", {"useBuiltIns": true, "targets": { "node": 10 }}], "stage-0", "react"], 4 | "plugins": [ 5 | "transform-class-properties", 6 | "transform-es2015-classes", 7 | "emotion" 8 | ], 9 | "env": { 10 | "test": { 11 | "presets": [], 12 | "plugins": [] 13 | }, 14 | "production": { 15 | "presets": [], 16 | "plugins": [] 17 | }, 18 | "development": { 19 | "presets": [], 20 | "plugins": ["react-hot-loader/babel"] 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | 2 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 3 | # 4 | version: 2 5 | jobs: 6 | build: 7 | docker: 8 | # specify the version you desire here 9 | - image: circleci/node:10-browsers 10 | 11 | # Specify service dependencies here if necessary 12 | # CircleCI maintains a library of pre-built images 13 | # documented at https://circleci.com/docs/2.0/circleci-images/ 14 | # - image: circleci/mongo:3.4.4 15 | 16 | working_directory: ~/repo 17 | 18 | steps: 19 | - checkout 20 | 21 | # Download and cache dependencies 22 | - restore_cache: 23 | keys: 24 | - v1-dependencies-{{ checksum "package.json" }} 25 | # fallback to using the latest cache if no exact match is found 26 | - v1-dependencies- 27 | 28 | - run: yarn install 29 | 30 | - save_cache: 31 | paths: 32 | - node_modules 33 | key: v1-dependencies-{{ checksum "package.json" }} 34 | 35 | # run tests! 36 | - run: yarn test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | *.pid.lock 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # Bower dependency directory (https://bower.io/) 26 | bower_components 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (http://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules/ 36 | jspm_packages/ 37 | 38 | # Typescript v1 declaration files 39 | typings/ 40 | 41 | # Optional npm cache directory 42 | .npm 43 | 44 | # Optional eslint cache 45 | .eslintcache 46 | 47 | # Optional REPL history 48 | .node_repl_history 49 | 50 | # Output of 'npm pack' 51 | *.tgz 52 | 53 | # dotenv environment variables file 54 | .env 55 | .idea 56 | .cache/ 57 | build/ 58 | dist/ 59 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Ofer Itzhaki 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RethinkDB Desktop 2 | 3 | [![CircleCI](https://circleci.com/gh/rethinkdb/rethinkdb-desktop.svg?style=svg)](https://circleci.com/gh/rethinkdb/rethinkdb-desktop) 4 | 5 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 6 | 7 | ## Overview 8 | 9 | RethinkDB Admin Desktop App - this project goal is to seperate the current admin from the core database and provide it as a desktop app. 10 | 11 | ## Setup 12 | ### Installation 13 | 14 | `npm install` 15 | 16 | `npm run dev` 17 | -------------------------------------------------------------------------------- /__mocks__/electron.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | app: { 3 | getPath: jest.fn() 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /__mocks__/randomcolor.js: -------------------------------------------------------------------------------- 1 | module.exports = jest.fn(() => '#fff') -------------------------------------------------------------------------------- /__mocks__/rethinkdb-mock.js: -------------------------------------------------------------------------------- 1 | const runnable = { 2 | run: Promise.resolve({}) 3 | } 4 | const query = Object.assign( 5 | jest.fn(), 6 | { 7 | merge: jest.fn(() => query), 8 | filter: jest.fn(() => query), 9 | map: jest.fn(() => query), 10 | reduce: jest.fn(() => query), 11 | slice: jest.fn(() => query), 12 | limit: jest.fn(() => query), 13 | pluck: jest.fn(() => query), 14 | sum: jest.fn(() => query), 15 | count: jest.fn(() => query), 16 | coerceTo: jest.fn(() => query), 17 | each: jest.fn(() => query), 18 | eq: jest.fn(() => query), 19 | ne: jest.fn(() => query), 20 | gt: jest.fn(() => query), 21 | lt: jest.fn(() => query), 22 | and: jest.fn(() => query), 23 | or: jest.fn(() => query), 24 | changes: jest.fn(() => query), 25 | replace: jest.fn(() => query), 26 | without: jest.fn(() => query), 27 | withFields: jest.fn(() => query) 28 | }, 29 | runnable 30 | ) 31 | 32 | const connection = { 33 | close: jest.fn(() => Promise.resolve({})), 34 | reconnect: jest.fn(() =>Promise.resolve({})) 35 | } 36 | 37 | const table = Object.assign( 38 | jest.fn(), 39 | { 40 | insert: jest.fn(() => query), 41 | get: jest.fn(() => query), 42 | getAll: jest.fn(() => query) 43 | }, 44 | query, 45 | runnable 46 | ) 47 | 48 | const db = { 49 | table, 50 | tableCreate: jest.fn(() => query), 51 | tableDrop: jest.fn(() => query), 52 | tableList: jest.fn(() => query), 53 | do: jest.fn(() => query) 54 | } 55 | 56 | const r = Object.assign( 57 | { 58 | connection, 59 | connect: jest.fn(() => Promise.resolve(connection)), 60 | expr: jest.fn(() => query), 61 | row: jest.fn(() => query), 62 | mock: { 63 | run: query.run, 64 | connection: connection 65 | } 66 | }, 67 | db 68 | ) 69 | 70 | r.db = jest.fn(() => db) 71 | 72 | module.exports = { r } 73 | -------------------------------------------------------------------------------- /__mocks__/rethinkdb-ts.js: -------------------------------------------------------------------------------- 1 | const mock = require('./rethinkdb-mock') 2 | module.exports = mock -------------------------------------------------------------------------------- /app/main/db/__tests__/driver.test.js: -------------------------------------------------------------------------------- 1 | import driver from '../driver' 2 | import { r as mockedR } from 'rethinkdb-ts' 3 | 4 | const conn = { host: 'test', port: 3000 } 5 | const conn2 = { host: 'test2', port: 3000 } 6 | const disconnect = driver.disconnect 7 | 8 | beforeEach(() => { 9 | // need to figure out why driver.disconnect.mockClear not working 10 | driver.disconnect = disconnect 11 | }) 12 | 13 | test('driver - does not disconnect if no active connection', async () => { 14 | driver.disconnect = jest.fn() 15 | await driver.connect(conn) 16 | expect(driver.disconnect).not.toBeCalled() 17 | }) 18 | 19 | test('driver - disconnect if has active connection', async () => { 20 | driver.disconnect = jest.fn() 21 | await driver.connect(conn) 22 | await driver.connect(conn2) 23 | expect(driver.disconnect).toBeCalled() 24 | }) 25 | 26 | test('driver - disconnect', async () => { 27 | const connection = await driver.connect(conn) 28 | await driver.disconnect() 29 | expect(connection.close).toBeCalled() 30 | }) 31 | 32 | test('driver - return connection', async () => { 33 | await driver.connect(conn) 34 | const c = driver.getConnection() 35 | expect(c).toBeDefined() 36 | }) 37 | -------------------------------------------------------------------------------- /app/main/db/driver.js: -------------------------------------------------------------------------------- 1 | const { r } = require('rethinkdb-ts') 2 | // const { getServers, getTables } = require('./queries/stats') 3 | // const r = require('rethinkdb') 4 | 5 | let connection 6 | 7 | const driver = { 8 | getConnection: () => connection, 9 | 10 | connect: async function (config = {}) { 11 | if (connection) { 12 | console.info('there is an active connection - closing current connection') 13 | await driver.disconnect() 14 | console.info('closed') 15 | } 16 | console.info('new connection request') 17 | connection = await r.connect(config) 18 | console.info('connected') 19 | return connection 20 | }, 21 | async disconnect () { 22 | if (connection && connection.close) { 23 | return connection.close() 24 | } 25 | } 26 | } 27 | 28 | module.exports = driver 29 | -------------------------------------------------------------------------------- /app/main/db/evalQuery.js: -------------------------------------------------------------------------------- 1 | const driver = require('./driver') 2 | const { r } = require('rethinkdb-ts') 3 | const vm = require('vm') 4 | 5 | // Custom eval function for the repl module 6 | async function replEval (code, context) { 7 | if (code === '(\n)') { 8 | return null 9 | } 10 | 11 | let err, re 12 | 13 | // we do not need force expression, just because we carefully check syntax later 14 | // code = code.replace(/^\(/, '').replace(/\)$/, '') 15 | 16 | // first, create the Script object to check the syntax 17 | try { 18 | var script = vm.createScript(code, { 19 | displayErrors: false 20 | }) 21 | } catch (e) { 22 | console.log('Script compile error:', e) 23 | console.log(e.stack) 24 | throw e 25 | } 26 | 27 | // then, run in context 28 | if (!err) { 29 | try { 30 | re = script.runInContext(context, { displayErrors: false }) 31 | } catch (e) { 32 | console.log('Runtime error:', e) 33 | console.log(e.stack) 34 | throw e 35 | } 36 | return evalResult(context.conn, re) 37 | } 38 | } 39 | 40 | async function evalResult (conn, result) { 41 | if (typeof result.run !== 'function') { 42 | throw new Error(result) 43 | } 44 | 45 | return result.run(conn).then(resultOrCursor => { 46 | if (!resultOrCursor) { 47 | return null 48 | } 49 | 50 | if (typeof resultOrCursor.toArray !== 'function') { 51 | return JSON.stringify(resultOrCursor) 52 | } 53 | return resultOrCursor.toArray() 54 | }) 55 | } 56 | 57 | function evalQuery (code) { 58 | const context = vm.createContext({ 59 | r, 60 | conn: driver.getConnection() 61 | }) 62 | return replEval(code, context) 63 | } 64 | 65 | module.exports = evalQuery 66 | -------------------------------------------------------------------------------- /app/main/db/index.js: -------------------------------------------------------------------------------- 1 | const ipc = require('electron-better-ipc') 2 | const { connect } = require('./driver') 3 | const evalQuery = require('./evalQuery') 4 | const { startLiveStats } = require('./models/stats') 5 | const { startClusterReadWriteChanges } = require('./models/cluster') 6 | const queryResolver = require('./resolvers/queryResolver') 7 | const actionResolver = require('./resolvers/actionResolver') 8 | const url = require('../helpers/url') 9 | const { 10 | CONNECT_CHANNEL_NAME, 11 | QUERIES_CHANNEL_NAME, 12 | ACTIONS_CHANNEL_NAME, 13 | EVAL_QUERY_CHANNEL_NAME 14 | } = require('../../shared/channels') 15 | 16 | ipc.answerRenderer(CONNECT_CHANNEL_NAME, async ({ name, address, username, password }) => { 17 | const { host, port } = url.extract(address) 18 | const connectResult = await connect({ host, port, username, password }) 19 | // connection created - we can start pushing updates 20 | startLiveStats() 21 | startClusterReadWriteChanges() 22 | return connectResult 23 | }) 24 | 25 | ipc.answerRenderer(QUERIES_CHANNEL_NAME, async query => { 26 | return queryResolver(query) 27 | }) 28 | 29 | ipc.answerRenderer(ACTIONS_CHANNEL_NAME, async action => { 30 | return actionResolver(action) 31 | }) 32 | 33 | ipc.answerRenderer(EVAL_QUERY_CHANNEL_NAME, async code => { 34 | return evalQuery(code) 35 | }) 36 | -------------------------------------------------------------------------------- /app/main/db/models/cluster.js: -------------------------------------------------------------------------------- 1 | const { BrowserWindow } = require('electron') 2 | const ipc = require('electron-better-ipc') 3 | 4 | const { getReadWriteChanges } = require('../queries/cluster') 5 | 6 | const { CLUSTER_CHANNEL_NAME } = require('../../../shared/channels') 7 | 8 | const stats = { 9 | async startClusterReadWriteChanges () { 10 | console.info('starting live cluster read write changes...') 11 | try { 12 | const readWriteChanges = await getReadWriteChanges() 13 | readWriteChanges.each((err, data) => { 14 | if (err) { 15 | console.error(err) 16 | return 17 | } 18 | const win = BrowserWindow.getAllWindows() 19 | if (win.length) { 20 | ipc.callRenderer(win[0], CLUSTER_CHANNEL_NAME, data.new_val.query_engine) 21 | } 22 | }) 23 | } catch (e) { 24 | console.error(e) 25 | } 26 | } 27 | } 28 | 29 | module.exports = stats 30 | -------------------------------------------------------------------------------- /app/main/db/models/database.js: -------------------------------------------------------------------------------- 1 | const { addDatabase, deleteDatabase } = require('../queries/database') 2 | 3 | const database = { 4 | add (name) { 5 | return addDatabase(name) 6 | }, 7 | del (name) { 8 | return deleteDatabase(name) 9 | } 10 | } 11 | 12 | module.exports = database 13 | -------------------------------------------------------------------------------- /app/main/db/models/index.js: -------------------------------------------------------------------------------- 1 | const server = require('./server') 2 | const table = require('./table') 3 | const stats = require('./stats') 4 | const database = require('./database') 5 | const logs = require('./logs') 6 | 7 | module.exports = { 8 | server, 9 | table, 10 | stats, 11 | database, 12 | logs 13 | } 14 | -------------------------------------------------------------------------------- /app/main/db/models/logs.js: -------------------------------------------------------------------------------- 1 | const { getLogs } = require('../queries/logs') 2 | 3 | const logs = { 4 | async getLogs () { 5 | return getLogs() 6 | } 7 | } 8 | 9 | module.exports = logs 10 | -------------------------------------------------------------------------------- /app/main/db/models/server.js: -------------------------------------------------------------------------------- 1 | const { serverList } = require('../queries/server') 2 | 3 | const server = { 4 | getServers () { 5 | return serverList() 6 | }, 7 | getServer (matchers = {}) {} 8 | } 9 | 10 | module.exports = server 11 | -------------------------------------------------------------------------------- /app/main/db/models/stats.js: -------------------------------------------------------------------------------- 1 | const { BrowserWindow } = require('electron') 2 | 3 | const ipc = require('electron-better-ipc') 4 | const { 5 | getServerStats, 6 | getTableStats, 7 | getIndexStats, 8 | getResourceStats, 9 | getIssuesStats 10 | } = require('../queries/stats') 11 | 12 | const { STATS_CHANNEL_NAME } = require('../../../shared/channels') 13 | 14 | let statsInterval = null 15 | 16 | const stats = { 17 | async onTick () { 18 | try { 19 | const servers = await getServerStats() 20 | const tables = await getTableStats() 21 | const indexes = await getIndexStats() 22 | const resources = await getResourceStats() 23 | const issues = await getIssuesStats() 24 | 25 | const win = BrowserWindow.getAllWindows() 26 | if (win.length) { 27 | ipc.callRenderer(win[0], STATS_CHANNEL_NAME, { 28 | servers, 29 | tables, 30 | indexes, 31 | resources, 32 | issues 33 | }) 34 | } 35 | } catch (e) { 36 | console.warn('startLiveStats failed: ', e) 37 | } 38 | }, 39 | async startLiveStats () { 40 | if (statsInterval) { 41 | console.info('clearing stats interval...') 42 | clearInterval(statsInterval) 43 | } 44 | console.info('starting live stats...') 45 | await stats.onTick() 46 | statsInterval = setInterval(stats.onTick, 2000) 47 | } 48 | } 49 | 50 | module.exports = stats 51 | -------------------------------------------------------------------------------- /app/main/db/models/table.js: -------------------------------------------------------------------------------- 1 | const { tablesByDb, addTable, deleteTables } = require('../queries/table') 2 | 3 | const table = { 4 | tablesByDb () { 5 | return tablesByDb() 6 | }, 7 | add (table) { 8 | try { 9 | return addTable(table) 10 | } catch (e) { 11 | console.log('e', e) 12 | } 13 | }, 14 | del (tables) { 15 | return deleteTables(tables) 16 | }, 17 | 18 | getTable (matchers = {}) {} 19 | } 20 | 21 | module.exports = table 22 | -------------------------------------------------------------------------------- /app/main/db/queries/cluster.js: -------------------------------------------------------------------------------- 1 | const { r } = require('rethinkdb-ts') 2 | const driver = require('../driver') 3 | const { SYSTEM_DB } = require('../../helpers/constants') 4 | 5 | const connection = () => driver.getConnection() 6 | 7 | module.exports = { 8 | getReadWriteChanges 9 | } 10 | 11 | function getReadWriteChanges () { 12 | return r 13 | .db(SYSTEM_DB) 14 | .table('stats') 15 | .get(['cluster']) 16 | .changes() 17 | .run(connection()) 18 | } 19 | -------------------------------------------------------------------------------- /app/main/db/queries/database.js: -------------------------------------------------------------------------------- 1 | const { r } = require('rethinkdb-ts') 2 | const driver = require('../driver') 3 | const { SYSTEM_DB } = require('../../helpers/constants') 4 | 5 | const connection = () => driver.getConnection() 6 | const addDatabase = ({ name }) => { 7 | return r 8 | .db(SYSTEM_DB) 9 | .table('db_config') 10 | .insert({ name }) 11 | .run(connection()) 12 | } 13 | 14 | const deleteDatabase = ({ name }) => { 15 | return r.dbDrop(name).run(connection()) 16 | } 17 | 18 | module.exports = { 19 | addDatabase, 20 | deleteDatabase 21 | } 22 | -------------------------------------------------------------------------------- /app/main/db/queries/logs.js: -------------------------------------------------------------------------------- 1 | const { r } = require('rethinkdb-ts') 2 | const driver = require('../driver') 3 | const { SYSTEM_DB } = require('../../helpers/constants') 4 | 5 | const connection = () => driver.getConnection() 6 | 7 | module.exports = { 8 | getLogs 9 | } 10 | 11 | function getLogs () { 12 | return r 13 | .db(SYSTEM_DB) 14 | .table('logs') 15 | .limit(10) 16 | .coerceTo('array') 17 | .run(connection()) 18 | } 19 | -------------------------------------------------------------------------------- /app/main/db/queries/server.js: -------------------------------------------------------------------------------- 1 | const { r } = require('rethinkdb-ts') 2 | 3 | const serverList = () => { 4 | return r 5 | .db('rethinkdb') 6 | .table('stats') 7 | .filter(row => 8 | row('id') 9 | .nth(0) 10 | .eq('server') 11 | ) 12 | .coerceTo('array') 13 | } 14 | 15 | module.exports = { 16 | serverList 17 | } 18 | -------------------------------------------------------------------------------- /app/main/db/queries/stats.js: -------------------------------------------------------------------------------- 1 | const { r } = require('rethinkdb-ts') 2 | const driver = require('../driver') 3 | const { SYSTEM_DB } = require('../../helpers/constants') 4 | 5 | const connection = () => driver.getConnection() 6 | const sysTable = name => r.db(SYSTEM_DB).table(name) 7 | 8 | const getServerStats = () => { 9 | const serverConfig = sysTable('server_config') 10 | const serverStatus = sysTable('server_status') 11 | const tableConfig = sysTable('table_config') 12 | 13 | return r 14 | .do( 15 | serverConfig.nth(0), 16 | // All connected servers 17 | serverStatus('name').coerceTo('array'), 18 | // All servers assigned to tables 19 | tableConfig 20 | .concatMap(row => row('shards').default([])) 21 | .concatMap(row => row('replicas')) 22 | .distinct(), 23 | (server, connectedServers, assignedServers) => ({ 24 | server, 25 | serversConnected: connectedServers.count(), 26 | serversMissing: assignedServers.setDifference(connectedServers), 27 | unknownMissing: tableConfig 28 | .filter(row => row.hasFields('shards').not())('name') 29 | .isEmpty() 30 | .not() 31 | }) 32 | ) 33 | .run(connection()) 34 | } 35 | 36 | const getTableStats = async () => { 37 | const tableStatus = sysTable('table_status') 38 | 39 | const tablesReady = await tableStatus 40 | .count(row => row('status')('all_replicas_ready')) 41 | .run(connection()) 42 | 43 | const tablesNotReady = await tableStatus 44 | .count(row => row('status')('all_replicas_ready').not()) 45 | .run(connection()) 46 | 47 | return { 48 | tablesReady, 49 | tablesNotReady 50 | } 51 | } 52 | 53 | const getIndexStats = async () => { 54 | const tableConfig = sysTable('table_config') 55 | const jobs = sysTable('jobs') 56 | 57 | const secondaryIndexes = await tableConfig.sum(row => row('indexes').count()).run(connection()) 58 | const secondaryIndexesConstructing = await jobs 59 | .filter({ type: 'index_construction' })('info') 60 | .map(row => ({ 61 | db: row('db'), 62 | table: row('table'), 63 | index: row('index'), 64 | progress: row('progress') 65 | })) 66 | .coerceTo('array') 67 | .run(connection()) 68 | 69 | return { 70 | secondaryIndexes, 71 | secondaryIndexesConstructing 72 | } 73 | } 74 | 75 | const getResourceStats = async () => { 76 | const stats = sysTable('stats') 77 | const serverStatus = sysTable('server_status') 78 | 79 | const cacheUsed = await stats 80 | .filter(stat => stat('id').contains('table_server'))('storage_engine')('cache')('in_use_bytes') 81 | .sum() 82 | .run(connection()) 83 | 84 | const cacheTotal = await serverStatus('process')('cache_size_mb') 85 | .map(row => row.mul(1024 * 1024)) 86 | .sum() 87 | .run(connection()) 88 | 89 | const diskUsed = await stats 90 | .filter(row => row('id').contains('table_server'))('storage_engine')('disk')('space_usage') 91 | .map(data => 92 | data('data_bytes') 93 | .add(data('garbage_bytes')) 94 | .add(data('metadata_bytes')) 95 | .add(data('preallocated_bytes')) 96 | ) 97 | .sum() 98 | .run(connection()) 99 | 100 | return { 101 | cacheUsed, 102 | cacheTotal, 103 | diskUsed 104 | } 105 | } 106 | 107 | const getIssuesStats = () => { 108 | return sysTable('current_issues') 109 | .coerceTo('array') 110 | .run(connection()) 111 | } 112 | 113 | module.exports = { 114 | getIssuesStats, 115 | getServerStats, 116 | getTableStats, 117 | getIndexStats, 118 | getResourceStats 119 | } 120 | -------------------------------------------------------------------------------- /app/main/db/queries/table.js: -------------------------------------------------------------------------------- 1 | const { r } = require('rethinkdb-ts') 2 | const driver = require('../driver') 3 | const { SYSTEM_DB } = require('../../helpers/constants') 4 | 5 | const connection = () => driver.getConnection() 6 | 7 | const tablesByDb = () => { 8 | return r 9 | .db(SYSTEM_DB) 10 | .table('db_config') 11 | .filter(db => db('name').ne(SYSTEM_DB)) 12 | .map(db => ({ 13 | name: db('name'), 14 | id: db('id'), 15 | tables: r 16 | .db(SYSTEM_DB) 17 | .table('table_status') 18 | .orderBy(table => table('name')) 19 | .filter({ db: db('name') }) 20 | .merge(table => ({ 21 | shards: table('shards') 22 | .count() 23 | .default(0), 24 | replicas: table('shards') 25 | .default([]) 26 | .map(shard => shard('replicas').count()) 27 | .sum(), 28 | replicasReady: table('shards') 29 | .default([]) 30 | .map(shard => 31 | shard('replicas') 32 | .filter(replica => replica('state').eq('ready')) 33 | .count() 34 | ) 35 | .sum(), 36 | status: table('status'), 37 | id: table('id') 38 | })) 39 | })) 40 | .run(connection()) 41 | } 42 | 43 | const deleteTables = tablesToDelete => { 44 | return r 45 | .db(SYSTEM_DB) 46 | .table('table_config') 47 | .filter(table => { 48 | return r.expr(tablesToDelete).contains({ 49 | db: table('db'), 50 | name: table('name') 51 | }) 52 | }) 53 | .delete({ returnChanges: true }) 54 | .run(connection()) 55 | } 56 | 57 | const addTable = ({ db, name, primaryKey, durability }) => { 58 | return r 59 | .db(SYSTEM_DB) 60 | .table('server_status') 61 | .coerceTo('ARRAY') 62 | .do(servers => { 63 | return r.branch( 64 | servers.isEmpty(), 65 | r.error('No server is connected'), 66 | servers 67 | .sample(1) 68 | .nth(0)('name') 69 | .do(server => { 70 | return r 71 | .db(SYSTEM_DB) 72 | .table('table_config') 73 | .insert( 74 | { 75 | db, 76 | name, 77 | primary_key: primaryKey, 78 | durability, 79 | shards: [ 80 | { 81 | primary_replica: server, 82 | replicas: [server] 83 | } 84 | ] 85 | }, 86 | { returnChanges: true } 87 | ) 88 | }) 89 | ) 90 | }) 91 | .run(connection()) 92 | } 93 | 94 | module.exports = { 95 | tablesByDb, 96 | addTable, 97 | deleteTables 98 | } 99 | -------------------------------------------------------------------------------- /app/main/db/resolvers/actionResolver.js: -------------------------------------------------------------------------------- 1 | const table = require('../models/table') 2 | const database = require('../models/database') 3 | 4 | const actions = { 5 | addDatabase: database.add, 6 | deleteDatabase: database.del, 7 | addTable: table.add, 8 | deleteTables: table.del 9 | } 10 | const actionResolver = ({ name = 'action', payload = {} }) => { 11 | if (!actions[name]) { 12 | throw new Error(`Could not resolve action "${name}"`) 13 | } 14 | return actions[name](payload) 15 | } 16 | 17 | module.exports = actionResolver 18 | -------------------------------------------------------------------------------- /app/main/db/resolvers/queryResolver.js: -------------------------------------------------------------------------------- 1 | const table = require('../models/table') 2 | const logs = require('../models/logs') 3 | 4 | const queries = { 5 | tablesByDb: table.tablesByDb, 6 | getLogs: logs.getLogs 7 | } 8 | const queryResolver = ({ name = 'query', payload = {} }) => { 9 | if (!queries[name]) { 10 | throw new Error(`Could not resolve query "${name}"`) 11 | } 12 | return queries[name](payload) 13 | } 14 | 15 | module.exports = queryResolver 16 | -------------------------------------------------------------------------------- /app/main/helpers/__tests__/url.test.js: -------------------------------------------------------------------------------- 1 | import url from '../url' 2 | 3 | test('extract - with protocol - http', () => { 4 | const http = 'http://localhost:1111' 5 | const actual = url.extract(http) 6 | expect(actual.host).toBe('http://localhost') 7 | expect(actual.port).toBe('1111') 8 | }) 9 | 10 | test('extract - with protocol - https', () => { 11 | const https = 'https://localhost:2222' 12 | const actual = url.extract(https) 13 | expect(actual.host).toBe('https://localhost') 14 | expect(actual.port).toBe('2222') 15 | }) 16 | 17 | test('extract - without protocol', () => { 18 | const addr = 'localhost:3333' 19 | const actual = url.extract(addr) 20 | expect(actual.host).toBe('localhost') 21 | expect(actual.port).toBe('3333') 22 | }) 23 | 24 | test('extract - without protocol - ip', () => { 25 | const addr = '34.193.202.51:4444' 26 | const actual = url.extract(addr) 27 | expect(actual.host).toBe('34.193.202.51') 28 | expect(actual.port).toBe('4444') 29 | }) 30 | 31 | test('extract - without protocol - domain', () => { 32 | const addr = 'foo.bar.baz.maz:5555' 33 | const actual = url.extract(addr) 34 | expect(actual.host).toBe('foo.bar.baz.maz') 35 | expect(actual.port).toBe('5555') 36 | }) 37 | -------------------------------------------------------------------------------- /app/main/helpers/constants.js: -------------------------------------------------------------------------------- 1 | const SYSTEM_DB = 'rethinkdb' 2 | 3 | module.exports = { 4 | SYSTEM_DB 5 | } 6 | -------------------------------------------------------------------------------- /app/main/helpers/url.js: -------------------------------------------------------------------------------- 1 | module.exports.extract = url => { 2 | let host 3 | let port 4 | const DELIMITER = '://' 5 | const withProtocol = url.includes('http://') || url.includes('https://') 6 | if (withProtocol) { 7 | let parts = url.split(DELIMITER) 8 | let protocol = parts[0] 9 | let [h, p] = [...parts[1].split(':')] 10 | host = protocol + DELIMITER + h 11 | port = p 12 | } else { 13 | const s = url.split(':') 14 | host = s[0] 15 | port = s[1] 16 | } 17 | 18 | return { host, port } 19 | } 20 | -------------------------------------------------------------------------------- /app/main/index.js: -------------------------------------------------------------------------------- 1 | require('./db') 2 | 3 | function init () { 4 | console.log('registering IPC handlers...') 5 | } 6 | 7 | module.exports = init 8 | -------------------------------------------------------------------------------- /app/renderer/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import { hot } from 'react-hot-loader' 3 | import { Router } from 'react-router' 4 | import { createHashHistory } from 'history' 5 | import Routes from './routes' 6 | import StatsProvider from './contexts/StatsContext' 7 | import { ToastContainer } from './components/Toast' 8 | import './components/Icon/icons' 9 | import './style/app.js' 10 | 11 | const history = createHashHistory() 12 | 13 | class App extends Component { 14 | render () { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | } 26 | } 27 | 28 | export default hot(module)(App) 29 | -------------------------------------------------------------------------------- /app/renderer/components/ActionsBar/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StyledActionsBar } from './styles.js' 3 | 4 | const ActionsBar = ({ title, children }) => { 5 | return ( 6 | 7 |

{title}

8 | {children} 9 |
10 | ) 11 | } 12 | 13 | export default ActionsBar 14 | -------------------------------------------------------------------------------- /app/renderer/components/ActionsBar/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'react-emotion' 2 | import theme from '@/style/common' 3 | 4 | export const StyledActionsBar = styled('header')(props => ({ 5 | display: 'flex', 6 | width: '100%', 7 | backgroundColor: theme.contrastColor, 8 | borderBottom: `1px solid ${theme.mainBorderColor}`, 9 | height: '50px', 10 | padding: '15px', 11 | h3: { 12 | fontWeight: 500 13 | } 14 | })) 15 | -------------------------------------------------------------------------------- /app/renderer/components/Alert/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StyledAlert } from './styles.js' 3 | 4 | const Alert = ({ type = 'basic', children }) => { 5 | return {children} 6 | } 7 | 8 | export default Alert 9 | -------------------------------------------------------------------------------- /app/renderer/components/Alert/styles.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'react-emotion' 2 | 3 | const success = css` 4 | color: #52c41a; 5 | background: #f6ffed; 6 | border-color: #b7eb8f; 7 | ` 8 | const error = css` 9 | color: #f5222d; 10 | background: #fff1f0; 11 | border-color: #ffa39e; 12 | ` 13 | const info = css` 14 | color: #1890ff; 15 | background: #e6f7ff; 16 | border-color: #91d5ff; 17 | ` 18 | const warning = css` 19 | color: #fa8c16; 20 | background: #fff7e6; 21 | border-color: #ffd591; 22 | ` 23 | const basic = css` 24 | border: 1px solid #d9d9d9; 25 | background: #fafafa; 26 | padding: 8px 12px; 27 | border-radius: 3px; 28 | margin: 5px 0; 29 | ` 30 | 31 | const alertTypes = { 32 | basic: basic, 33 | success: success, 34 | error: error, 35 | info: info, 36 | warning: warning 37 | } 38 | const alertStyle = props => 39 | css` 40 | ${basic} ${alertTypes[props.type]}; 41 | ` 42 | 43 | export const StyledAlert = styled('p')` 44 | ${alertStyle}; 45 | ` 46 | -------------------------------------------------------------------------------- /app/renderer/components/AppHeader/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StyledAppHeader } from './styles.js' 3 | 4 | const AppHeader = ({ children }) => { 5 | return {children} 6 | } 7 | 8 | export default AppHeader 9 | -------------------------------------------------------------------------------- /app/renderer/components/AppHeader/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'react-emotion' 2 | import theme from '@/style/common' 3 | 4 | export const StyledAppHeader = styled('header')(props => ({ 5 | position: 'fixed', 6 | width: '100%', 7 | top: 0, 8 | backgroundColor: theme.secColor, 9 | borderBottom: `1px solid ${theme.mainBorderColor}`, 10 | zIndex: 2, 11 | height: theme.appHeaderHeight, 12 | padding: '10px 15px 10px 0' 13 | })) 14 | -------------------------------------------------------------------------------- /app/renderer/components/Button/index.js: -------------------------------------------------------------------------------- 1 | import styled from 'react-emotion' 2 | 3 | export const Button = styled('button')(props => ({ 4 | padding: '5px 12px', 5 | background: 'transparent', 6 | border: '1px solid #603e85', 7 | color: '#603e85', 8 | transition: 'all ease-in 250ms', 9 | cursor: 'pointer', 10 | '&:hover': { 11 | borderColor: '#eb48ca', 12 | color: '#eb48ca' 13 | }, 14 | '&:focus': { 15 | outline: 'none' 16 | }, 17 | '&:disabled': { 18 | border: '1px solid #aaa', 19 | color: '#aaa', 20 | cursor: 'auto' 21 | } 22 | })) 23 | -------------------------------------------------------------------------------- /app/renderer/components/Dropdown/dropdown.css: -------------------------------------------------------------------------------- 1 | .rc-dropdown { 2 | position: absolute; 3 | left: -9999px; 4 | top: -9999px; 5 | z-index: 1070; 6 | display: block; 7 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 8 | font-size: 12px; 9 | font-weight: normal; 10 | line-height: 1.5; 11 | } 12 | .rc-dropdown-hidden { 13 | display: none; 14 | } 15 | .rc-dropdown-menu { 16 | outline: none; 17 | position: relative; 18 | list-style-type: none; 19 | padding: 0; 20 | margin: 2px 0 0 0; 21 | text-align: left; 22 | background-color: #fff; 23 | border-radius: 2px; 24 | box-shadow: rgba(68, 66, 88, 0.15) 0px 0.5rem 2rem, rgba(68, 66, 88, 0.15) 0px 0px 0.0625rem; 25 | background-clip: padding-box; 26 | } 27 | .rc-dropdown-menu > li { 28 | margin: 0; 29 | padding: 0; 30 | } 31 | .rc-dropdown-menu:before { 32 | content: ""; 33 | position: absolute; 34 | top: -4px; 35 | left: 0; 36 | width: 100%; 37 | height: 4px; 38 | background: #ffffff; 39 | background: rgba(255, 255, 255, 0.01); 40 | } 41 | .rc-dropdown-menu > .rc-dropdown-menu-item { 42 | position: relative; 43 | display: block; 44 | padding: 7px 10px; 45 | clear: both; 46 | font-size: 12px; 47 | font-weight: normal; 48 | color: #666666; 49 | white-space: nowrap; 50 | cursor: pointer; 51 | } 52 | .rc-dropdown-menu > .rc-dropdown-menu-item:hover, 53 | .rc-dropdown-menu > .rc-dropdown-menu-item-active, 54 | .rc-dropdown-menu > .rc-dropdown-menu-item-selected { 55 | background-color: rgba(241, 240, 250, 0.5); 56 | color: rgb(34, 33, 44); 57 | outline: 0px; 58 | } 59 | .rc-dropdown-menu > .rc-dropdown-menu-item-selected { 60 | position: relative; 61 | } 62 | .rc-dropdown-menu > .rc-dropdown-menu-item-disabled { 63 | color: #ccc; 64 | cursor: not-allowed; 65 | pointer-events: none; 66 | } 67 | .rc-dropdown-menu > .rc-dropdown-menu-item-disabled:hover { 68 | color: #ccc; 69 | background-color: #fff; 70 | cursor: not-allowed; 71 | } 72 | .rc-dropdown-menu > .rc-dropdown-menu-item:last-child { 73 | border-bottom-left-radius: 3px; 74 | border-bottom-right-radius: 3px; 75 | } 76 | .rc-dropdown-menu > .rc-dropdown-menu-item:first-child { 77 | border-top-left-radius: 3px; 78 | border-top-right-radius: 3px; 79 | } 80 | .rc-dropdown-menu > .rc-dropdown-menu-item-divider { 81 | height: 1px; 82 | margin: 1px 0; 83 | overflow: hidden; 84 | background-color: #e5e5e5; 85 | line-height: 0; 86 | } 87 | .rc-dropdown-slide-up-enter, 88 | .rc-dropdown-slide-up-appear { 89 | animation-duration: 0.3s; 90 | animation-fill-mode: both; 91 | transform-origin: 0 0; 92 | display: block !important; 93 | opacity: 0; 94 | animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); 95 | animation-play-state: paused; 96 | } 97 | .rc-dropdown-slide-up-leave { 98 | animation-duration: 0.3s; 99 | animation-fill-mode: both; 100 | transform-origin: 0 0; 101 | display: block !important; 102 | opacity: 1; 103 | animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34); 104 | animation-play-state: paused; 105 | } 106 | .rc-dropdown-slide-up-enter.rc-dropdown-slide-up-enter-active.rc-dropdown-placement-bottomLeft, 107 | .rc-dropdown-slide-up-appear.rc-dropdown-slide-up-appear-active.rc-dropdown-placement-bottomLeft, 108 | .rc-dropdown-slide-up-enter.rc-dropdown-slide-up-enter-active.rc-dropdown-placement-bottomCenter, 109 | .rc-dropdown-slide-up-appear.rc-dropdown-slide-up-appear-active.rc-dropdown-placement-bottomCenter, 110 | .rc-dropdown-slide-up-enter.rc-dropdown-slide-up-enter-active.rc-dropdown-placement-bottomRight, 111 | .rc-dropdown-slide-up-appear.rc-dropdown-slide-up-appear-active.rc-dropdown-placement-bottomRight { 112 | animation-name: rcDropdownSlideUpIn; 113 | animation-play-state: running; 114 | } 115 | .rc-dropdown-slide-up-enter.rc-dropdown-slide-up-enter-active.rc-dropdown-placement-topLeft, 116 | .rc-dropdown-slide-up-appear.rc-dropdown-slide-up-appear-active.rc-dropdown-placement-topLeft, 117 | .rc-dropdown-slide-up-enter.rc-dropdown-slide-up-enter-active.rc-dropdown-placement-topCenter, 118 | .rc-dropdown-slide-up-appear.rc-dropdown-slide-up-appear-active.rc-dropdown-placement-topCenter, 119 | .rc-dropdown-slide-up-enter.rc-dropdown-slide-up-enter-active.rc-dropdown-placement-topRight, 120 | .rc-dropdown-slide-up-appear.rc-dropdown-slide-up-appear-active.rc-dropdown-placement-topRight { 121 | animation-name: rcDropdownSlideDownIn; 122 | animation-play-state: running; 123 | } 124 | .rc-dropdown-slide-up-leave.rc-dropdown-slide-up-leave-active.rc-dropdown-placement-bottomLeft, 125 | .rc-dropdown-slide-up-leave.rc-dropdown-slide-up-leave-active.rc-dropdown-placement-bottomCenter, 126 | .rc-dropdown-slide-up-leave.rc-dropdown-slide-up-leave-active.rc-dropdown-placement-bottomRight { 127 | animation-name: rcDropdownSlideUpOut; 128 | animation-play-state: running; 129 | } 130 | .rc-dropdown-slide-up-leave.rc-dropdown-slide-up-leave-active.rc-dropdown-placement-topLeft, 131 | .rc-dropdown-slide-up-leave.rc-dropdown-slide-up-leave-active.rc-dropdown-placement-topCenter, 132 | .rc-dropdown-slide-up-leave.rc-dropdown-slide-up-leave-active.rc-dropdown-placement-topRight { 133 | animation-name: rcDropdownSlideDownOut; 134 | animation-play-state: running; 135 | } 136 | @keyframes rcDropdownSlideUpIn { 137 | 0% { 138 | opacity: 0; 139 | transform-origin: 0% 0%; 140 | transform: scaleY(0); 141 | } 142 | 100% { 143 | opacity: 1; 144 | transform-origin: 0% 0%; 145 | transform: scaleY(1); 146 | } 147 | } 148 | @keyframes rcDropdownSlideUpOut { 149 | 0% { 150 | opacity: 1; 151 | transform-origin: 0% 0%; 152 | transform: scaleY(1); 153 | } 154 | 100% { 155 | opacity: 0; 156 | transform-origin: 0% 0%; 157 | transform: scaleY(0); 158 | } 159 | } 160 | @keyframes rcDropdownSlideDownIn { 161 | 0% { 162 | opacity: 0; 163 | transform-origin: 0% 100%; 164 | transform: scaleY(0); 165 | } 166 | 100% { 167 | opacity: 1; 168 | transform-origin: 0% 100%; 169 | transform: scaleY(1); 170 | } 171 | } 172 | @keyframes rcDropdownSlideDownOut { 173 | 0% { 174 | opacity: 1; 175 | transform-origin: 0% 100%; 176 | transform: scaleY(1); 177 | } 178 | 100% { 179 | opacity: 0; 180 | transform-origin: 0% 100%; 181 | transform: scaleY(0); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /app/renderer/components/Dropdown/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import RCDropdown from 'rc-dropdown' 3 | import './dropdown.css' 4 | 5 | const Dropdown = props => { 6 | return {props.children} 7 | } 8 | 9 | export default Dropdown 10 | -------------------------------------------------------------------------------- /app/renderer/components/Error/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Icon from '../../components/Icon' 3 | 4 | import { ErrorBoundryMessage } from './styles' 5 | 6 | const toTitle = (error, componentStack) => { 7 | return `${error.toString()}\n\nThis is located at:${componentStack}` 8 | } 9 | 10 | const ErrorBoundaryFallbackComponent = ({ componentStack, error }) => ( 11 | 12 | 13 |

Something went wrong...

14 |
{toTitle(error, componentStack)}
15 |
16 | ) 17 | 18 | export default ErrorBoundaryFallbackComponent 19 | -------------------------------------------------------------------------------- /app/renderer/components/Error/styles.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'react-emotion' 2 | import theme from '@/style/common' 3 | 4 | const baseError = css` 5 | width: 100%; 6 | height: 100%; 7 | top: ${theme.appHeaderHeight}; 8 | left: 0; 9 | position: fixed; 10 | text-align: center; 11 | background: #000; 12 | color: #29d829; 13 | margin-top: 10px; 14 | font-size: 10px; 15 | padding: 5px; 16 | text-align: left; 17 | padding: 10px; 18 | h2 { 19 | font-weight: 700; 20 | margin: 20px 0; 21 | font-size: 22px; 22 | } 23 | .error-icon { 24 | position: absolute; 25 | top: 10px; 26 | right: 10px; 27 | } 28 | ` 29 | 30 | export const ErrorBoundryMessage = styled.div` 31 | ${baseError}; 32 | ` 33 | -------------------------------------------------------------------------------- /app/renderer/components/Icon/icons.js: -------------------------------------------------------------------------------- 1 | import cloud from '../../static/svg/icons/cloud.svg' 2 | import connections from '../../static/svg/icons/connections.svg' 3 | import copy from '../../static/svg/icons/copy.svg' 4 | import danger from '../../static/svg/icons/danger.svg' 5 | import database from '../../static/svg/icons/database.svg' 6 | import error from '../../static/svg/icons/error.svg' 7 | import plus from '../../static/svg/icons/plus.svg' 8 | import servers from '../../static/svg/icons/servers.svg' 9 | import table from '../../static/svg/icons/table.svg' 10 | 11 | export default { 12 | cloud, 13 | connections, 14 | copy, 15 | danger, 16 | database, 17 | error, 18 | plus, 19 | servers, 20 | table 21 | } 22 | -------------------------------------------------------------------------------- /app/renderer/components/Icon/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Icon = ({ type = '', size = 24, color = '#000', className = 'rebirth-icon' }) => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | Icon.propTypes = { 15 | type: PropTypes.string.isRequired, 16 | size: PropTypes.number, 17 | color: PropTypes.string, 18 | className: PropTypes.string 19 | } 20 | 21 | export default Icon 22 | -------------------------------------------------------------------------------- /app/renderer/components/LogsPanel/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import moment from 'moment' 3 | import { Box, Message, Server, Time, Level } from './styles' 4 | 5 | const LogsPanel = ({ records }) => ( 6 | 7 |

Recent log entries

8 |
9 | 23 |
24 | ) 25 | 26 | export default LogsPanel 27 | -------------------------------------------------------------------------------- /app/renderer/components/LogsPanel/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'react-emotion' 2 | import theme from '@/style/common' 3 | 4 | export const Message = styled('div')({ 5 | flex: 1, 6 | div: { 7 | div: { 8 | margin: '0.5rem 0' 9 | } 10 | } 11 | }) 12 | 13 | export const Server = styled('span')({ 14 | color: `${theme.info}`, 15 | fontStyle: 'italic' 16 | }) 17 | 18 | export const Box = styled('div')({ 19 | border: `1px solid ${theme.mainBorderColor}`, 20 | background: `${theme.contrastColor}`, 21 | boxShadow: '0 2px 1px rgba(0, 0, 0, 0.04), inset 0 -1px 2px rgba(0, 0, 0, 0.05)', 22 | margin: '2rem', 23 | h1: { 24 | padding: '1rem' 25 | }, 26 | hr: { 27 | margin: '0 1rem 0.5rem 1rem', 28 | display: 'block', 29 | height: '1px', 30 | border: 0, 31 | borderTop: `1px solid ${theme.mainBorderColor}`, 32 | padding: 0 33 | }, 34 | ul: { 35 | listStyle: 'none' 36 | }, 37 | li: { 38 | display: 'flex', 39 | alignItems: 'center' 40 | } 41 | }) 42 | 43 | export const Level = styled('div')(({ value }) => ({ 44 | margin: '1rem', 45 | width: '1rem', 46 | height: '1rem', 47 | borderRadius: '50%', 48 | background: `${value === 'info' ? theme.info : theme.grayTextColor}` 49 | })) 50 | 51 | export const Time = styled('div')({ 52 | width: '6rem', 53 | marginLeft: '2rem' 54 | }) 55 | -------------------------------------------------------------------------------- /app/renderer/components/Modal/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import RCModal from 'rc-dialog' 3 | import 'rc-dialog/assets/index.css' 4 | 5 | const Modal = props => { 6 | return {props.children} 7 | } 8 | 9 | export default Modal 10 | -------------------------------------------------------------------------------- /app/renderer/components/Page/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'react-emotion' 3 | import theme from '@/style/common' 4 | 5 | const Container = styled.footer` 6 | background: ${theme.infoBarsColor}; 7 | display: flex; 8 | padding: 1rem 10%; 9 | color: black; 10 | 11 | > * { 12 | flex: 1; 13 | } 14 | 15 | ul { 16 | display: flex; 17 | list-style: none; 18 | 19 | li { 20 | flex: 1; 21 | } 22 | } 23 | ` 24 | 25 | const Footer = () => ( 26 | 27 | 32 | 33 | ) 34 | 35 | export default Footer 36 | -------------------------------------------------------------------------------- /app/renderer/components/Page/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'react-emotion' 3 | import { NavLink, Link } from 'react-router-dom' 4 | import theme from '@/style/common' 5 | 6 | const Container = styled.header` 7 | -webkit-app-region: drag; 8 | background: ${theme.mainColor}; 9 | display: flex; 10 | padding: 10px 10%; 11 | align-items: center; 12 | nav { 13 | flex: 1; 14 | margin-left: 5rem; 15 | } 16 | 17 | ul { 18 | display: flex; 19 | list-style: none; 20 | 21 | li { 22 | flex: 1; 23 | text-align: center; 24 | a { 25 | -webkit-app-region: no-drag; 26 | color: ${theme.mainColorLight}; 27 | font-size: 1rem; 28 | text-decoration: none; 29 | font-weight: bolder; 30 | &.active { 31 | color: white; 32 | } 33 | } 34 | } 35 | } 36 | ` 37 | 38 | const Logo = styled(Link)` 39 | font-family: 'Quicksand'; 40 | color: ${theme.mainColorLight}; 41 | text-decoration: none; 42 | font-size: 18px; 43 | font-weight: bolder; 44 | span { 45 | color: ${theme.secColor}; 46 | } 47 | ` 48 | 49 | const Header = () => ( 50 | 51 | 52 | Rebirth 53 | DB 54 | 55 | 84 | 85 | ) 86 | 87 | export default Header 88 | -------------------------------------------------------------------------------- /app/renderer/components/Page/Info.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'react-emotion' 3 | import Icon from '../../components/Icon' 4 | import theme from '@/style/common' 5 | import { StatsContext } from '../../contexts/StatsContext' 6 | 7 | const List = styled.ul` 8 | background: ${theme.infoBarsColor}; 9 | width: 100%; 10 | display: flex; 11 | padding: 1rem 10%; 12 | list-style: none; 13 | color: black; 14 | border-bottom: thin solid ${theme.mainBorderColor}; 15 | ` 16 | 17 | const Item = styled.li` 18 | flex: 1; 19 | width: 100%; 20 | text-align: center; 21 | ` 22 | const Header = styled.div` 23 | display: inline-block; 24 | padding: 0.5rem; 25 | margin-right: 1rem; 26 | ` 27 | 28 | const Body = styled.div` 29 | display: inline-block; 30 | vertical-align: top; 31 | text-align: left; 32 | padding-top: 0.25rem; 33 | ` 34 | 35 | const Title = styled.div` 36 | color: #909090; 37 | ` 38 | 39 | const Value = styled.div` 40 | color: #4d535c; 41 | font-size: 1rem; 42 | font-weight: bold; 43 | ` 44 | 45 | const Info = () => ( 46 | 47 | {({ stats }) => { 48 | const { servers, tables, issues = [] } = stats || {} 49 | const { server: { name = '' } = {}, serversConnected = 0 } = servers || {} 50 | const { tablesReady = 0, tablesNotReady = 0 } = tables || {} 51 | return ( 52 | 53 | 54 |
55 | 56 |
57 | 58 | Connected to 59 | {name} 60 | 61 |
62 | 63 |
64 | 65 |
66 | 67 | Issues 68 | {issues.length || 'No'} Issues 69 | 70 |
71 | 72 |
73 | 74 |
75 | 76 | Servers 77 | {serversConnected} connected 78 | 79 |
80 | 81 |
82 | 83 |
84 | 85 | Tables 86 | 87 | {tablesReady}/{tablesNotReady + tablesReady} ready 88 | 89 | 90 |
91 |
92 | ) 93 | }} 94 |
95 | ) 96 | 97 | export default Info 98 | -------------------------------------------------------------------------------- /app/renderer/components/Page/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Header from './Header' 3 | import Footer from './Footer' 4 | import Info from './Info' 5 | import styled from 'react-emotion' 6 | 7 | const Container = styled.div` 8 | display: flex; 9 | flex-direction: column; 10 | min-height: 100vh; 11 | ` 12 | 13 | const Body = styled.div` 14 | flex: 1; 15 | ` 16 | 17 | const Page = ({ children }) => ( 18 | 19 |
20 | 21 | {children} 22 |