├── .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 | [](https://circleci.com/gh/rethinkdb/rethinkdb-desktop)
4 |
5 | [](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 |
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 |
10 | {records.map(({ id, level, message, timestamp, server }) => (
11 | -
12 |
13 |
14 |
{message}
15 |
16 | Posted By: {server}
17 |
18 |
19 |
20 |
21 | ))}
22 |
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 |
57 |
58 | Connected to
59 | {name}
60 |
61 |
62 | -
63 |
66 |
67 | Issues
68 | {issues.length || 'No'} Issues
69 |
70 |
71 | -
72 |
75 |
76 | Servers
77 | {serversConnected} connected
78 |
79 |
80 | -
81 |
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 |
23 |
24 | )
25 |
26 | export default Page
27 |
--------------------------------------------------------------------------------
/app/renderer/components/SideBar/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | // import { slide as Menu } from 'react-burger-menu'
3 | import { StyledSideBar, Logo } from './styles.js'
4 | const SideBar = ({ children }) => {
5 | return (
6 |
7 | {children}
8 |
9 | ReBirth
10 |
11 |
12 | )
13 | }
14 |
15 | export default SideBar
16 |
--------------------------------------------------------------------------------
/app/renderer/components/SideBar/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'react-emotion'
2 | import theme from '@/style/common'
3 |
4 | export const StyledSideBar = styled('aside')(props => ({
5 | position: 'fixed',
6 | top: theme.appHeaderHeight,
7 | height: '100vh',
8 | width: theme.sideBarWidth,
9 | background: theme.mainColor,
10 | zIndex: 2
11 | }))
12 |
13 | export const Logo = styled('div')(props => ({
14 | position: 'absolute',
15 | bottom: '70px',
16 | textAlign: 'center',
17 | width: '100%',
18 |
19 | '> span': {
20 | fontSize: '18px',
21 | fontFamily: 'Quicksand',
22 | fontWeight: 700,
23 | color: theme.mainColorLight,
24 | border: `solid ${theme.mainColorLight}`,
25 | borderWidth: '1px 0'
26 | }
27 | }))
28 |
--------------------------------------------------------------------------------
/app/renderer/components/Switch/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactSwitch from 'react-switch'
3 | import theme from '@/style/common'
4 |
5 | export const Switch = props => {
6 | return
7 | }
8 |
9 | export default Switch
10 |
--------------------------------------------------------------------------------
/app/renderer/components/TimeChart/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import moment from 'moment'
3 |
4 | import { VictoryChart, VictoryLine, VictoryClipContainer, VictoryTheme, VictoryAxis } from 'victory'
5 |
6 | const TimeChart = ({ chartWidth, rows }) => (
7 |
40 | )
41 |
42 | export default TimeChart
43 |
--------------------------------------------------------------------------------
/app/renderer/components/Toast/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Alert from 'react-s-alert'
3 | import './toast.css'
4 | import 'react-s-alert/dist/s-alert-css-effects/slide.css'
5 |
6 | const Toast = Alert
7 | export const ToastContainer = props => {
8 | return
9 | }
10 |
11 | export default Toast
12 |
--------------------------------------------------------------------------------
/app/renderer/components/Toast/toast.css:
--------------------------------------------------------------------------------
1 | .s-alert-box,
2 | .s-alert-box * {
3 | box-sizing: border-box;
4 | }
5 |
6 | .s-alert-box {
7 | position: fixed;
8 | background: rgba(42,45,50,0.85);
9 | padding: 10px;
10 | line-height: 1.4;
11 | border-radius: 5px;
12 | z-index: 1000;
13 | pointer-events: none;
14 | color: rgba(250,251,255,0.95);
15 | font-size: 100%;
16 | max-width: 300px;
17 | -webkit-transition: top .4s, bottom .4s;
18 | transition: top .4s, bottom .4s;
19 | }
20 |
21 | .s-alert-box.s-alert-show {
22 | pointer-events: auto;
23 | }
24 |
25 | .s-alert-box a {
26 | color: inherit;
27 | opacity: 0.7;
28 | font-weight: 700;
29 | }
30 |
31 | .s-alert-box a:hover,
32 | .s-alert-box a:focus {
33 | opacity: 1;
34 | }
35 |
36 | .s-alert-box p {
37 | margin: 0;
38 | }
39 |
40 | .s-alert-box.s-alert-show,
41 | .s-alert-box.s-alert-visible {
42 | pointer-events: auto;
43 | }
44 |
45 | .s-alert-close {
46 | /*width: 12px;*/
47 | /*height: 12px;*/
48 | /*position: absolute;*/
49 | /*right: 4px;*/
50 | /*top: 4px;*/
51 | /*overflow: hidden;*/
52 | /*text-indent: 100%;*/
53 | /*cursor: pointer;*/
54 | /*-webkit-backface-visibility: hidden;*/
55 | /*backface-visibility: hidden;*/
56 | display: none;
57 | }
58 |
59 | .s-alert-close:hover,
60 | .s-alert-close:focus {
61 | outline: none;
62 | }
63 |
64 | .s-alert-close::before,
65 | .s-alert-close::after {
66 | content: '';
67 | position: absolute;
68 | width: 3px;
69 | height: 60%;
70 | top: 50%;
71 | left: 50%;
72 | background: #fff;
73 | }
74 |
75 | .s-alert-close:hover::before,
76 | .s-alert-close:hover::after {
77 | background: #fff;
78 | }
79 |
80 | .s-alert-close::before {
81 | -webkit-transform: translate(-50%,-50%) rotate(45deg);
82 | transform: translate(-50%,-50%) rotate(45deg);
83 | }
84 |
85 | .s-alert-close::after {
86 | -webkit-transform: translate(-50%,-50%) rotate(-45deg);
87 | transform: translate(-50%,-50%) rotate(-45deg);
88 | }
89 |
90 | /* positions */
91 |
92 | .s-alert-bottom-left {
93 | top: auto;
94 | right: auto;
95 | bottom: 30px;
96 | left: 30px;
97 | }
98 | .s-alert-top-left {
99 | top: 30px;
100 | right: auto;
101 | bottom: auto;
102 | left: 30px;
103 | }
104 | .s-alert-top-right {
105 | top: 30px;
106 | right: 30px;
107 | bottom: auto;
108 | left: auto;
109 | }
110 | .s-alert-bottom-right { /*default*/
111 | top: auto;
112 | right: 30px;
113 | bottom: 30px;
114 | left: auto;
115 | }
116 | .s-alert-bottom {
117 | width: 100%;
118 | max-width: 100%;
119 | bottom: 0;
120 | left: 0;
121 | right: 0;
122 | top: auto;
123 | }
124 | .s-alert-top {
125 | width: 100%;
126 | max-width: 100%;
127 | top: 0;
128 | left: 0;
129 | right: 0;
130 | bottom: auto;
131 | }
132 |
133 | /* conditions */
134 |
135 | .s-alert-info {
136 | background: #00A2D3;
137 | color: #fff;
138 | }
139 | .s-alert-success {
140 | background: #1ad1b5;
141 | color: #fff;
142 | }
143 | .s-alert-warning {
144 | background: #F1C40F;
145 | color: #fff;
146 | }
147 | .s-alert-error {
148 | background: #E74C3C;
149 | color: #fff;
150 | }
151 |
152 | [class^="s-alert-effect-"].s-alert-hide,
153 | [class*=" s-alert-effect-"].s-alert-hide {
154 | -webkit-animation-direction: reverse;
155 | animation-direction: reverse;
156 | }
157 |
158 | /* height measurement helper */
159 | .s-alert-box-height {
160 | visibility: hidden;
161 | position: fixed;
162 | }
--------------------------------------------------------------------------------
/app/renderer/contexts/LogsContext.js:
--------------------------------------------------------------------------------
1 | import React, { Component, createContext } from 'react'
2 | import { Route } from 'react-router'
3 | import { query } from '../service/ipc'
4 |
5 | export const LogsContext = createContext()
6 | const LogsContextProvider = LogsContext.Provider
7 |
8 | class LogsProviderRoute extends Component {
9 | state = {
10 | logs: []
11 | }
12 |
13 | async componentDidMount () {
14 | const logs = await query({ name: 'getLogs' })
15 | this.setState({ logs })
16 | }
17 |
18 | render () {
19 | const { logs } = this.state
20 | const { component: Component, ...rest } = this.props
21 | return (
22 |
23 |
24 |
25 |
26 |
27 | )
28 | }
29 | }
30 |
31 | export default LogsProviderRoute
32 |
--------------------------------------------------------------------------------
/app/renderer/contexts/StatsContext.js:
--------------------------------------------------------------------------------
1 | import React, { Component, createContext } from 'react'
2 | import { liveStats, liveClusterReadWrite } from '../service/ipc'
3 |
4 | export const StatsContext = createContext()
5 | const StatsContextProvider = StatsContext.Provider
6 |
7 | class StatsProvider extends Component {
8 | state = {
9 | stats: {}
10 | }
11 |
12 | onLiveStats = statsData => {
13 | this.setState({ stats: statsData })
14 | }
15 |
16 | onLiveClusterReadWrites = data => {
17 | this.setState({ cluster: data })
18 | }
19 |
20 | componentDidMount () {
21 | liveStats(this.onLiveStats)
22 | liveClusterReadWrite(this.onLiveClusterReadWrites)
23 | }
24 |
25 | render () {
26 | const { stats, cluster } = this.state
27 | const { children } = this.props
28 | return {children}
29 | }
30 | }
31 |
32 | export default StatsProvider
33 |
--------------------------------------------------------------------------------
/app/renderer/helpers/__tests__/connectionStore.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env jest */
2 |
3 | import {
4 | CONNECTION_DEFAULT_HOST,
5 | CONNECTION_DEFAULT_NAME,
6 | CONNECTION_DEFAULT_PORT,
7 | CONNECTION_DEFAULT_USERNAME,
8 | CONNECTION_DEFAULT_PASSWORD
9 | } from '../constants'
10 |
11 | import {
12 | getConnection,
13 | saveConnection,
14 | getConnectionList,
15 | removeConnection,
16 | updateConnection,
17 | clear
18 | } from '../connectionStore'
19 |
20 | function cleanTest() {
21 | clear()
22 | }
23 |
24 | beforeEach(() => {
25 | cleanTest()
26 | })
27 |
28 | test('use defaults ---> default name', () => {
29 | let uid = saveConnection({ host: 'foo' })
30 | const result = getConnection(uid)
31 | expect(result.name).toBe(CONNECTION_DEFAULT_NAME)
32 | })
33 |
34 | test('use defaults ---> default address', () => {
35 | let uid = saveConnection({ name: 'foo' })
36 | const result = getConnection(uid)
37 | expect(result.address).toBe(`${CONNECTION_DEFAULT_HOST}:${CONNECTION_DEFAULT_PORT}`)
38 | })
39 |
40 | test('use defaults ---> default username', () => {
41 | let uid = saveConnection({ name: 'foo' })
42 | const result = getConnection(uid)
43 | expect(result.username).toBe(CONNECTION_DEFAULT_USERNAME)
44 | })
45 |
46 | test('use defaults ---> default password', () => {
47 | let uid = saveConnection({ name: 'foo' })
48 | const result = getConnection(uid)
49 | expect(result.password).toBe(CONNECTION_DEFAULT_PASSWORD)
50 | })
51 |
52 | test('remove connection', () => {
53 | let uid = saveConnection({ address: 'test:123' })
54 | const result = getConnection(uid)
55 | expect(result.name).toBe(CONNECTION_DEFAULT_NAME)
56 | removeConnection(uid)
57 | const deleteResult = getConnection(uid)
58 | expect(deleteResult).toEqual({})
59 | })
60 |
61 | test('prevents duplicate entries', () => {
62 | saveConnection({ name: 'test', address: 'test:8888' })
63 | saveConnection({ name: 'test', address: 'test:8888' })
64 | expect(getConnectionList()).toHaveLength(1)
65 | })
66 |
67 | test('saving multiple', () => {
68 | saveConnection({ name: 'test', address: 'test:8888' })
69 | saveConnection({ name: 'test', address: 'test:8889' })
70 | expect(getConnectionList()).toHaveLength(2)
71 | })
72 |
73 | test('able to update connection', () => {
74 | let uid = saveConnection({ name: 'test1', address: 'test:8888' })
75 | updateConnection(uid, { name: 'test2' })
76 | const result = getConnection(uid)
77 | expect(result.name).toBe('test2')
78 | })
79 |
80 | test('able to update username', () => {
81 | let uid = saveConnection({ name: 'test-username', address: 'test:8888' })
82 | updateConnection(uid, { username: 'user' })
83 | const result = getConnection(uid)
84 | expect(result.username).toBe('user')
85 | })
86 |
87 | test('able to update password', () => {
88 | let uid = saveConnection({ name: 'test-password', address: 'test:8888' })
89 | updateConnection(uid, { password: 'password' })
90 | const result = getConnection(uid)
91 | expect(result.password).toBe('password')
92 | })
93 |
94 | test('getConnectionList can handle empty store', () => {
95 | expect(getConnectionList()).toBeDefined()
96 | expect(getConnectionList()).toHaveLength(0)
97 | })
98 |
--------------------------------------------------------------------------------
/app/renderer/helpers/connectionStore.js:
--------------------------------------------------------------------------------
1 | import shortid from 'shortid'
2 | import stringHash from '../helpers/stringHash'
3 | import storage from '../helpers/storage'
4 | import {
5 | CONNECTION_DEFAULT_NAME,
6 | CONNECTION_DEFAULT_HOST,
7 | CONNECTION_DEFAULT_PORT,
8 | CONNECTION_DEFAULT_USERNAME,
9 | CONNECTION_DEFAULT_PASSWORD
10 | } from './constants'
11 |
12 | const getHash = str => stringHash(str)
13 |
14 | const isExist = connectionId => {
15 | const list = getConnectionList()
16 | const result = list.find(el => el.connectionId === connectionId)
17 | return !!result
18 | }
19 | export const removeConnection = uid => storage.delete(uid)
20 | export const clear = () => storage.clear()
21 |
22 | export const saveConnection = ({
23 | name = CONNECTION_DEFAULT_NAME,
24 | address = `${CONNECTION_DEFAULT_HOST}:${CONNECTION_DEFAULT_PORT}`,
25 | username = CONNECTION_DEFAULT_USERNAME,
26 | password = CONNECTION_DEFAULT_PASSWORD
27 | }) => {
28 | const connectionId = getHash(name + address)
29 | if (!isExist(connectionId)) {
30 | const uid = shortid.generate()
31 | storage.set(uid, { connectionId, name, address, username, password })
32 | return uid
33 | }
34 | }
35 |
36 | export const updateConnection = (uid, values) => {
37 | const newConnectionId = getHash(values.name + values.address)
38 | const current = getConnection(uid)
39 | Object.keys(values).forEach(key => values[key] === undefined && delete values[key])
40 |
41 | const updated = Object.assign({}, current, values, { connectionId: newConnectionId })
42 | storage.set(uid, updated)
43 | return updated
44 | }
45 |
46 | export const getConnection = uid => {
47 | const connection = storage.get(uid)
48 | return connection || {}
49 | }
50 |
51 | export const getConnectionList = () => {
52 | // transform from key:value to collection
53 | const items = storage.store
54 | const keys = Object.keys(items)
55 | if (keys.length) {
56 | return keys.map(key => ({ id: key, ...items[key] }))
57 | } else {
58 | return []
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/renderer/helpers/constants.js:
--------------------------------------------------------------------------------
1 | export const CONNECTION_DEFAULT_NAME = 'local'
2 | export const CONNECTION_DEFAULT_HOST = 'localhost'
3 | export const CONNECTION_DEFAULT_PORT = 28015
4 | export const CONNECTION_DEFAULT_USERNAME = 'admin'
5 | export const CONNECTION_DEFAULT_PASSWORD = ''
6 |
--------------------------------------------------------------------------------
/app/renderer/helpers/format.js:
--------------------------------------------------------------------------------
1 | // formatBytes
2 | // from http://stackoverflow.com/a/18650828/180718
3 | export const formatBytes = (bytes, decimals) => {
4 | if (decimals == null) {
5 | decimals = 1
6 | }
7 | if (bytes === 0) {
8 | return '0 Bytes'
9 | }
10 | const k = 1024
11 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
12 | const i = Math.floor(Math.log(bytes) / Math.log(k))
13 | return (bytes / Math.pow(k, i)).toFixed(decimals) + ' ' + sizes[i]
14 | }
15 |
16 | const humanizeTableStatus = status => {
17 | if (!status) {
18 | return ''
19 | } else if (status.all_replicas_ready || status.ready_for_writes) {
20 | return 'Ready'
21 | } else if (status.ready_for_reads) {
22 | return 'Reads only'
23 | } else if (status.ready_for_outdated_reads) {
24 | return 'Outdated reads'
25 | } else {
26 | return 'Unavailable'
27 | }
28 | }
29 |
30 | export const humanizeTableReadiness = (status, num, denom) => {
31 | let label, value
32 | if (!status) {
33 | label = 'failure'
34 | value = 'unknown'
35 | } else if (status.all_replicas_ready) {
36 | label = 'success'
37 | value = `${humanizeTableStatus(status)} ${num}/${denom}`
38 | } else if (status.ready_for_writes) {
39 | label = 'partial-success'
40 | value = `${humanizeTableStatus(status)} ${num}/${denom}`
41 | } else {
42 | label = 'failure'
43 | value = humanizeTableStatus(status)
44 | }
45 | return { label, value }
46 | }
47 |
--------------------------------------------------------------------------------
/app/renderer/helpers/storage.js:
--------------------------------------------------------------------------------
1 | import Store from 'electron-store'
2 | const storage = new Store()
3 |
4 | export default storage
5 |
--------------------------------------------------------------------------------
/app/renderer/helpers/stringHash.js:
--------------------------------------------------------------------------------
1 | const hash = str => {
2 | let hash = 5381
3 | let i = str.length
4 | while (i) {
5 | hash = (hash * 33) ^ str.charCodeAt(--i)
6 | }
7 | /* JavaScript does bitwise operations (like XOR, above) on 32-bit signed
8 | * integers. Since we want the results to be always positive, convert the
9 | * signed int to an unsigned by doing an unsigned bitshift. */
10 | return (hash >>> 0).toString()
11 | }
12 |
13 | export default hash
14 |
--------------------------------------------------------------------------------
/app/renderer/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | RethinkDB - Desktop Client
12 |
13 |
14 |
15 |
18 |
19 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/renderer/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from 'react-dom'
3 | import App from './App'
4 |
5 | render(, document.getElementById('root'))
6 |
--------------------------------------------------------------------------------
/app/renderer/routes/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import ErrorBoundary from 'react-error-boundary'
3 | import Error from '../components/Error'
4 | import { Switch, Route } from 'react-router'
5 | import Home from '../views/Home'
6 | import NewConnection from '../views/Connection/NewConnection'
7 | import EditConnection from '../views/Connection/EditConnection'
8 | import Dashboard from '../views/Dashboard'
9 | import Tables from '../views/Tables'
10 | import Servers from '../views/Servers'
11 | import Explorer from '../views/Explorer'
12 | import Logs from '../views/Logs'
13 | import RouteWithLogs from '../contexts/LogsContext'
14 |
15 | class Routes extends Component {
16 | render () {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | )
31 | }
32 | }
33 |
34 | export default Routes
35 |
--------------------------------------------------------------------------------
/app/renderer/service/__mocks__/ipc.js:
--------------------------------------------------------------------------------
1 | const connect = jest.fn()
2 |
3 | module.exports = {
4 | connect
5 | }
6 |
--------------------------------------------------------------------------------
/app/renderer/service/__tests__/connection.test.js:
--------------------------------------------------------------------------------
1 | import connection from '../connection'
2 | import { connect } from '../ipc'
3 | import { saveConnection as mockedSaveConnection } from '../../helpers/connectionStore'
4 | import { getConnectionList as mockedGetConnectionList } from '../../helpers/connectionStore'
5 |
6 | jest.mock('../ipc')
7 | jest.mock('../../helpers/connectionStore')
8 |
9 | const conn = { name: 'test', address: 'foo:3000' }
10 | const conn2 = { address: 'foo:3000' }
11 | const error = new Error('oi vy')
12 |
13 | test('create - unsuccessful connection return error object', async () => {
14 | connect.mockImplementationOnce(() => Promise.reject(error))
15 | const result = await connection.create(conn)
16 | expect(result.error).toEqual(error)
17 | })
18 |
19 | test('create - verified successful connection return status', async () => {
20 | connect.mockImplementationOnce(() => Promise.resolve({ socket: { isOpen: true } }))
21 | const result = await connection.create(conn)
22 | expect(result).toEqual({ status: 'OK' })
23 | })
24 |
25 | test('create - verified successful connection trigger save', async () => {
26 | connect.mockImplementationOnce(() => Promise.resolve({ socket: { isOpen: true } }))
27 | await connection.create(conn)
28 | const m = mockedSaveConnection
29 | expect(m).toHaveBeenCalledTimes(1)
30 | expect(m).toHaveBeenCalledWith(conn)
31 | })
32 |
33 | test('create - does not trigger save if there is no name argument', async () => {
34 | connect.mockImplementationOnce(() => Promise.resolve({ socket: { isOpen: true } }))
35 | await connection.create(conn2)
36 | const m = mockedSaveConnection
37 | expect(m).not.toBeCalled()
38 | })
39 |
40 | test('create - un-verified successful connection return error', async () => {
41 | connect.mockImplementationOnce(() => Promise.resolve({ socket: { isOpen: false } }))
42 | const result = await connection.create(conn)
43 | expect(result).toEqual({ error: 'could not establish connection' })
44 | })
45 |
46 | test('getConnections - calls the right store method', () => {
47 | connection.getConnections()
48 | expect(mockedGetConnectionList).toBeCalled()
49 | })
50 |
--------------------------------------------------------------------------------
/app/renderer/service/connection.js:
--------------------------------------------------------------------------------
1 | import { connect } from './ipc'
2 | import {
3 | saveConnection,
4 | getConnectionList,
5 | removeConnection,
6 | getConnection,
7 | updateConnection
8 | } from '../helpers/connectionStore'
9 |
10 | const connection = {
11 | async create ({ name, address, username, password }) {
12 | try {
13 | const result = await connect({ name, address, username, password })
14 | if (result.socket.isOpen) {
15 | // if we got name in args - it's a new connection, we need to save it
16 | if (name) saveConnection({ name, address, username, password })
17 | return { status: 'OK' }
18 | } else {
19 | return { error: 'could not establish connection' }
20 | }
21 | } catch (e) {
22 | return { error: e }
23 | }
24 | },
25 | update (id, values) {
26 | updateConnection(id, values)
27 | },
28 | deleteConnection (id) {
29 | removeConnection(id)
30 | },
31 | getConnectionById (id) {
32 | return getConnection(id)
33 | },
34 | getConnections () {
35 | return getConnectionList()
36 | }
37 | }
38 |
39 | export default connection
40 |
--------------------------------------------------------------------------------
/app/renderer/service/ipc.js:
--------------------------------------------------------------------------------
1 | const ipc = require('electron-better-ipc')
2 | const {
3 | CONNECT_CHANNEL_NAME,
4 | STATS_CHANNEL_NAME,
5 | QUERIES_CHANNEL_NAME,
6 | ACTIONS_CHANNEL_NAME,
7 | CLUSTER_CHANNEL_NAME,
8 | EVAL_QUERY_CHANNEL_NAME
9 | } = require('../../shared/channels')
10 |
11 | // ToDo: instead of hard coding the channels, we should create a channel factory
12 | /*
13 | i.e:
14 | export const method = (callback) => {
15 | ipc.answerMain(topic, message => {
16 | callback(message)
17 | })
18 | }
19 | */
20 | export const connect = ({ name, address, username, password }) => {
21 | return ipc.callMain(CONNECT_CHANNEL_NAME, { name, address, username, password })
22 | }
23 |
24 | export const query = (query = '', args = {}) => {
25 | return ipc.callMain(QUERIES_CHANNEL_NAME, query, args)
26 | }
27 |
28 | export const action = (action = '', args = {}) => {
29 | return ipc.callMain(ACTIONS_CHANNEL_NAME, action, args)
30 | }
31 |
32 | export const evalQuery = (code) => {
33 | return ipc.callMain(EVAL_QUERY_CHANNEL_NAME, code)
34 | }
35 |
36 | // the following channels are for "push" updates from main to renderer
37 | // answerMain is not really answering anything here...just handling the event sent from main
38 | // each time the main will "push" a message the callback will be executed
39 | export const liveStats = callback => {
40 | ipc.answerMain(STATS_CHANNEL_NAME, data => callback(data))
41 | }
42 |
43 | export const liveClusterReadWrite = callback => {
44 | ipc.answerMain(CLUSTER_CHANNEL_NAME, data => callback(data))
45 | }
46 |
--------------------------------------------------------------------------------
/app/renderer/static/png/rebirth_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rethinkdb/rethinkdb-desktop/0e2daff6ceff0be2eb9a7f56068c81e95fc76f1f/app/renderer/static/png/rebirth_logo.png
--------------------------------------------------------------------------------
/app/renderer/static/svg/icons/cloud.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
47 |
--------------------------------------------------------------------------------
/app/renderer/static/svg/icons/connections.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
51 |
--------------------------------------------------------------------------------
/app/renderer/static/svg/icons/copy.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
52 |
--------------------------------------------------------------------------------
/app/renderer/static/svg/icons/danger.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
50 |
--------------------------------------------------------------------------------
/app/renderer/static/svg/icons/database.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
93 |
--------------------------------------------------------------------------------
/app/renderer/static/svg/icons/error.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
175 |
--------------------------------------------------------------------------------
/app/renderer/static/svg/icons/plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
53 |
--------------------------------------------------------------------------------
/app/renderer/static/svg/icons/servers.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
132 |
--------------------------------------------------------------------------------
/app/renderer/static/svg/icons/table.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
115 |
--------------------------------------------------------------------------------
/app/renderer/style/app.js:
--------------------------------------------------------------------------------
1 | import { injectGlobal } from 'react-emotion'
2 | import theme from './common.js'
3 |
4 | injectGlobal`
5 | * {
6 | margin: 0;
7 | padding: 0;
8 | box-sizing: border-box;
9 | }
10 |
11 | ::selection {
12 | background: rgba(106, 180, 173, 0.99);
13 | color: #d8fdff;
14 | }
15 | h1,
16 | h2,
17 | h3,
18 | h4,
19 | h5,
20 | h6 {
21 | font-weight: 300;
22 | }
23 |
24 | body {
25 | background: ${theme.secColor};
26 | color: ${theme.mainTextColor};
27 | font-family: ${theme.fontStack};
28 | font-weight: 300;
29 | font-size: 14px;
30 | }
31 |
32 | #root > header {
33 | -webkit-app-region: drag;
34 | }
35 | `
36 |
--------------------------------------------------------------------------------
/app/renderer/style/common.js:
--------------------------------------------------------------------------------
1 | export default {
2 | // colors
3 | mainColor: '#603e85',
4 | mainColorDark: '#321353',
5 | mainColorLight: '#ad8cd1',
6 | secColor: '#F1F0F9',
7 | contrastColor: '#FDFDFD',
8 | mainGradient: 'linear-gradient(180deg, #FF6FD8 10%, #3813C2 100%)',
9 | mainBorderColor: '#d9d8ff',
10 | headerBackground: '#272729',
11 | headerGradient: '0 1px 3px rgba(0,0,0,.2), inset 0 -2px 4px rgba(0,0,0,.15)',
12 | success: '#1ad1b5',
13 | error: '#FD6585',
14 | warning: '#fda94e',
15 | info: '#4C83FF',
16 | // text
17 | fontStack: `'Lato', sans-serif`,
18 | mainTextColor: '#444258',
19 | secTextColor: '#82809a',
20 | contrastTextColor: '#fff',
21 | secContrast: 'rgba(255, 255, 255, 0.5)',
22 | grayTextColor: '#8f8e9b',
23 | // elements dimensions
24 | appHeaderHeight: '38px',
25 | sideBarWidth: '180px'
26 | }
27 |
--------------------------------------------------------------------------------
/app/renderer/views/Connection/Connections/ConnectionItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import randomColor from 'randomcolor'
3 | import ConnectionItemActions from './ConnectionItemActions'
4 | import { StyledConnectionItem, StyledConnectionName } from './styles'
5 |
6 | const ConnectionItem = props => {
7 | const { item, onItemClick, onEdit, onDelete } = props
8 | const { id, name, address, connectionId } = item
9 | const color = randomColor({ seed: connectionId, luminosity: 'bright' })
10 | const handleClick = () => onItemClick({ id, name, address })
11 |
12 | return (
13 |
14 | {name}
15 |
16 |
17 | )
18 | }
19 |
20 | export default ConnectionItem
21 |
--------------------------------------------------------------------------------
/app/renderer/views/Connection/Connections/ConnectionItemActions.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react'
2 | import Menu, { Item as MenuItem, Divider } from 'rc-menu'
3 | import Dropdown from '../../../components/Dropdown'
4 | import { StyledActionsButton, MenuItemIcon } from './styles'
5 |
6 | class ConnectionItemActions extends PureComponent {
7 | constructor (props) {
8 | super(props)
9 | this.state = {
10 | showActions: false
11 | }
12 | }
13 |
14 | onAction = ({ key }) => {
15 | const { connectionId, onEdit, onDelete } = this.props
16 | if (key === 'edit') {
17 | onEdit(connectionId)
18 | }
19 | if (key === 'delete') {
20 | onDelete(connectionId)
21 | }
22 | }
23 |
24 | onVisibleChange = visible => {
25 | console.log(visible)
26 | }
27 |
28 | menu = (
29 |
38 | )
39 |
40 | render () {
41 | return (
42 |
48 |
49 |
50 |
51 |
52 | )
53 | }
54 | }
55 |
56 | export default ConnectionItemActions
57 |
--------------------------------------------------------------------------------
/app/renderer/views/Connection/Connections/ConnectionList.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react'
2 | import ConnectionListHeader from './ConnectionListHeader'
3 | import ConnectionItem from './ConnectionItem'
4 | import { StyledConnectionList } from './styles'
5 | import connection from '../../../service/connection'
6 |
7 | class ConnectionList extends PureComponent {
8 | constructor (props) {
9 | super(props)
10 | this.state = {
11 | connections: []
12 | }
13 | }
14 |
15 | fetchConnections = () => {
16 | const connectionList = connection.getConnections()
17 | this.setState({ connections: connectionList })
18 | }
19 |
20 | onEditConnection = id => {
21 | window.location.hash = `#/editConnection/${id}`
22 | }
23 |
24 | onDeleteConnection = id => {
25 | connection.deleteConnection(id)
26 | this.fetchConnections()
27 | }
28 |
29 | componentDidMount () {
30 | this.fetchConnections()
31 | }
32 |
33 | render () {
34 | const { onItemClick } = this.props
35 | const { connections } = this.state
36 | return (
37 |
38 |
39 |
40 | {connections.map(c => (
41 |
48 | ))}
49 |
50 |
51 | )
52 | }
53 | }
54 |
55 | export default ConnectionList
56 |
--------------------------------------------------------------------------------
/app/renderer/views/Connection/Connections/ConnectionListHeader.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Icon from '../../../components/Icon'
3 | import { css } from 'react-emotion'
4 | import theme from '@/style/common'
5 |
6 | const ConnectionListHeader = () => {
7 | const connectionListHeader = css({
8 | textAlign: 'center',
9 | paddingBottom: '8px'
10 | })
11 |
12 | return (
13 |
14 |
15 |
16 | )
17 | }
18 |
19 | export default ConnectionListHeader
20 |
--------------------------------------------------------------------------------
/app/renderer/views/Connection/Connections/__tests__/ConnectionItem.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ConnectionItem from '../ConnectionItem'
3 | import renderer from 'react-test-renderer'
4 |
5 | jest.mock('../styles', () => {
6 | return {
7 | StyledConnectionName: '',
8 | StyledConnectionItem: ''
9 | }
10 | })
11 |
12 | jest.mock('../ConnectionItemActions', () => () => (
13 | ConnectionItemActions
14 | ))
15 |
16 | test.only('ConnectionList render items', () => {
17 | const connection = { id: 'H53er6x', name: 'test', connectionId: '123' }
18 | const component = renderer.create()
19 | let tree = component.toJSON()
20 | expect(tree).toMatchSnapshot()
21 | })
22 |
--------------------------------------------------------------------------------
/app/renderer/views/Connection/Connections/__tests__/__snapshots__/ConnectionItem.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`ConnectionList render items 1`] = `
4 | <
5 | color="#fff"
6 | >
7 | <
8 | onClick={[Function]}
9 | >
10 | test
11 | >
12 |
15 | ConnectionItemActions
16 |
17 | >
18 | `;
19 |
--------------------------------------------------------------------------------
/app/renderer/views/Connection/Connections/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'react-emotion'
2 | import theme from '@/style/common'
3 |
4 | export const StyledConnectionList = styled('ul')({
5 | padding: '10px',
6 | listStyle: 'none'
7 | })
8 |
9 | export const StyledConnectionItem = styled('li')(props => ({
10 | fontSize: '14px',
11 | padding: '5px 0',
12 | marginBottom: '5px',
13 | position: 'relative',
14 | paddingLeft: '15px',
15 | color: theme.mainColorLight,
16 | display: 'flex',
17 | '&:before': {
18 | content: '""',
19 | position: 'absolute',
20 | left: 0,
21 | top: '9px',
22 | background: props.color,
23 | width: '8px',
24 | height: '8px',
25 | borderRadius: '50%'
26 | },
27 | '&.active': {
28 | color: theme.contrastTextColor
29 | }
30 | }))
31 |
32 | export const StyledConnectionName = styled('span')(props => ({
33 | marginRight: 'auto',
34 | overflow: 'hidden',
35 | textOverflow: 'ellipsis',
36 | whiteSpace: 'nowrap',
37 | cursor: 'pointer',
38 | '&:hover': {
39 | color: theme.contrastTextColor
40 | }
41 | }))
42 |
43 | export const StyledActionsButton = styled('span')(props => ({
44 | cursor: 'pointer',
45 | '&:hover': {
46 | color: theme.contrastTextColor
47 | }
48 | }))
49 |
50 | export const MenuItemIcon = css({
51 | i: {
52 | marginRight: '10px'
53 | }
54 | })
55 |
--------------------------------------------------------------------------------
/app/renderer/views/Connection/EditConnection/EditConnectionForm.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import Icon from '../../../components/Icon'
3 | import { withFormik, Form, Field } from 'formik'
4 | import { StyledEditForm, GoBackLink } from './styles'
5 | const EditConnectionForm = () => {
6 | return (
7 |
8 |
9 | Go Back
10 |
11 |
12 |
13 |
14 |
Manage Connection
15 |
16 |
33 |
34 |
35 | )
36 | }
37 |
38 | export default withFormik({
39 | enableReinitialize: true,
40 | mapPropsToValues: ({ connection }) => {
41 | return {
42 | name: connection.name || '',
43 | address: connection.address || '',
44 | username: connection.username || '',
45 | password: connection.password || ''
46 | }
47 | },
48 | handleSubmit (values, { props }) {
49 | props.onSave(values)
50 | }
51 | })(EditConnectionForm)
52 |
--------------------------------------------------------------------------------
/app/renderer/views/Connection/EditConnection/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent, Fragment } from 'react'
2 | import connection from '../../../service/connection'
3 | import AppHeader from '../../../components/AppHeader'
4 | import Toast from '../../../components/Toast'
5 | import EditConnectionForm from './EditConnectionForm'
6 |
7 | class EditConnection extends PureComponent {
8 | constructor (props) {
9 | super(props)
10 | this.state = {
11 | id: '',
12 | selected: {}
13 | }
14 | }
15 |
16 | onSaveConnection = values => {
17 | try {
18 | connection.update(this.state.id, values)
19 | Toast.success('Connection saved!')
20 | } catch (e) {}
21 | }
22 |
23 | componentDidMount () {
24 | const {
25 | match: {
26 | params: { id }
27 | }
28 | } = this.props
29 | const selected = connection.getConnectionById(id)
30 | this.setState({ selected, id })
31 | }
32 |
33 | render () {
34 | const { selected } = this.state
35 | return (
36 |
37 |
38 |
39 |
40 | )
41 | }
42 | }
43 |
44 | export default EditConnection
45 |
--------------------------------------------------------------------------------
/app/renderer/views/Connection/EditConnection/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'react-emotion'
2 | import theme from '@/style/common'
3 |
4 | export const StyledEditForm = styled('div')({
5 | position: 'absolute',
6 | top: '50%',
7 | left: '50%',
8 | transform: 'translate(-50%, -50%)',
9 | width: '280px',
10 | '.title': {
11 | textAlign: 'center',
12 | h2: {
13 | margin: '15px 0'
14 | }
15 | },
16 | '.row': {
17 | width: '100%',
18 | padding: '15px 0',
19 | 'input[type="text"],input[type="password"]': {
20 | width: '100%',
21 | height: '38px',
22 | background: 'transparent',
23 | border: 'none',
24 | borderBottom: '1px solid #603e85',
25 | transition: 'all ease-in 500ms',
26 | color: theme.mainTextColor,
27 | '&:focus': {
28 | outline: 'none',
29 | borderBottomColor: '#eb48ca'
30 | }
31 | },
32 | button: {
33 | width: '90px',
34 | height: '34px',
35 | background: 'transparent',
36 | border: '1px solid #603e85',
37 | color: '#603e85',
38 | transition: 'all ease-in 500ms',
39 | cursor: 'pointer',
40 | '&:hover': {
41 | borderColor: '#eb48ca',
42 | color: '#eb48ca'
43 | },
44 | '&:focus': {
45 | outline: 'none'
46 | }
47 | },
48 | '&.actions': {
49 | display: 'flex',
50 | flexFlow: 'row-reverse'
51 | }
52 | }
53 | })
54 |
55 | export const GoBackLink = css({
56 | fontSize: '14px',
57 | textDecoration: 'none',
58 | letterSpacing: '1px',
59 | color: theme.secTextColor,
60 | position: 'absolute',
61 | top: '58px',
62 | left: '20px',
63 | '&:hover': {
64 | color: '#eb48ca'
65 | }
66 | })
67 |
--------------------------------------------------------------------------------
/app/renderer/views/Connection/NewConnection/NewConnectionForm.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent, Fragment } from 'react'
2 | import { Title, StyledNewConnection } from './styles'
3 |
4 | class NewConnectionForm extends PureComponent {
5 | constructor (props) {
6 | super(props)
7 | this.state = {
8 | name: '',
9 | address: '',
10 | username: '',
11 | password: ''
12 | }
13 | }
14 |
15 | onNameChange = event => {
16 | this.setState({ name: event.target.value })
17 | }
18 |
19 | onAddressChange = event => {
20 | this.setState({ address: event.target.value })
21 | }
22 |
23 | onUsernameChange = event => {
24 | this.setState({ username: event.target.value })
25 | }
26 |
27 | onPasswordChange = event => {
28 | this.setState({ password: event.target.value })
29 | }
30 |
31 | handleCreate = () => {
32 | const { onCreate } = this.props
33 | const { name, address, username, password } = this.state
34 | onCreate(name, address, username, password)
35 | }
36 |
37 | render () {
38 | const { defaultName, defaultAddress, defaultUsername, defaultPassword } = this.props
39 | return (
40 |
41 | Add New Connection
42 |
43 |
44 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | )
66 | }
67 | }
68 |
69 | export default NewConnectionForm
70 |
--------------------------------------------------------------------------------
/app/renderer/views/Connection/NewConnection/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent, Fragment } from 'react'
2 | import { withRouter } from 'react-router'
3 | import connection from '../../../service/connection'
4 | import AppHeader from '../../../components/AppHeader'
5 | import SideBar from '../../../components/SideBar'
6 | import ConnectionList from '../Connections/ConnectionList'
7 | import NewConnectionForm from './NewConnectionForm'
8 |
9 | import {
10 | CONNECTION_DEFAULT_NAME,
11 | CONNECTION_DEFAULT_HOST,
12 | CONNECTION_DEFAULT_PASSWORD,
13 | CONNECTION_DEFAULT_PORT,
14 | CONNECTION_DEFAULT_USERNAME
15 | } from '../../../helpers/constants'
16 |
17 | import { MainContent, ConnectionInfo, ConnectionError, Connecting, Logo } from './styles'
18 | import logoImg from '../../../static/png/rebirth_logo.png'
19 |
20 | class NewConnection extends PureComponent {
21 | constructor (props) {
22 | super(props)
23 | this.defaultName = CONNECTION_DEFAULT_NAME
24 | this.defaultAddress = `${CONNECTION_DEFAULT_HOST}:${CONNECTION_DEFAULT_PORT}`
25 | this.defaultUsername = CONNECTION_DEFAULT_USERNAME
26 | this.defaultPassword = CONNECTION_DEFAULT_PASSWORD
27 | this.state = {
28 | error: undefined,
29 | connecting: false
30 | }
31 | }
32 |
33 | makeConnectionRequest = async ({ name, address, username, password }) => {
34 | this.setState({ error: undefined, connecting: true })
35 | const result = await connection.create({ name, address, username, password })
36 | if (result.error) {
37 | this.setState({ error: result.error.code, connecting: false })
38 | } else {
39 | const { history } = this.props
40 | this.setState({ connecting: false })
41 | // this.props.onConnected(connection)
42 | history.push('/dashboard')
43 | }
44 | }
45 | onCreate = (name, address, username, password) => {
46 | if (!name.trim().length) {
47 | name = this.defaultName
48 | }
49 | if (!address.trim().length) {
50 | address = this.defaultAddress
51 | }
52 | if (!username.trim().length) {
53 | username = this.defaultUsername
54 | }
55 | if (!password.trim().length) {
56 | password = this.defaultPassword
57 | }
58 | return this.makeConnectionRequest({ name, address, username, password })
59 | }
60 |
61 | onQuickConnect = ({ address }) => this.makeConnectionRequest({ address })
62 |
63 | render () {
64 | const { error, connecting } = this.state
65 | return (
66 |
67 |
68 |
69 |
70 |
71 |
72 | {error && {error}}
73 | {connecting && Connecting...}
74 |
75 |
82 |
83 | By default RethinkDB will connect to {this.defaultAddress} with connection
84 | name {this.defaultName}
85 |
86 |
87 |
88 | )
89 | }
90 | }
91 | NewConnection.propTypes = {}
92 |
93 | export default withRouter(NewConnection)
94 |
--------------------------------------------------------------------------------
/app/renderer/views/Connection/NewConnection/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'react-emotion'
2 | import theme from '@/style/common'
3 |
4 | export const MainContent = styled('main')(props => ({
5 | position: 'relative',
6 | margin: `0 0 0 ${theme.sideBarWidth}`,
7 | top: theme.appHeaderHeight,
8 | height: `calc(100vh - ${theme.appHeaderHeight})`
9 | }))
10 |
11 | export const Title = styled('h2')({
12 | textAlign: 'center',
13 | position: 'absolute',
14 | top: '20%',
15 | left: '50%',
16 | transform: 'translate(-50%, -50%)',
17 | width: '280px'
18 | })
19 |
20 | export const StyledNewConnection = styled('div')({
21 | position: 'absolute',
22 | top: '50%',
23 | left: '50%',
24 | transform: 'translate(-50%, -50%)',
25 | width: '280px',
26 | '.row': {
27 | width: '100%',
28 | padding: '15px 0',
29 | 'input[type="text"],input[type="password"]': {
30 | width: '100%',
31 | height: '38px',
32 | background: 'transparent',
33 | border: 'none',
34 | borderBottom: '1px solid #603e85',
35 | transition: 'all ease-in 500ms',
36 | color: theme.mainTextColor,
37 | '&:focus': {
38 | outline: 'none',
39 | borderBottomColor: '#eb48ca'
40 | }
41 | },
42 | button: {
43 | width: '90px',
44 | height: '34px',
45 | background: 'transparent',
46 | border: '1px solid #603e85',
47 | color: '#603e85',
48 | transition: 'all ease-in 500ms',
49 | cursor: 'pointer',
50 | '&:hover': {
51 | borderColor: '#eb48ca',
52 | color: '#eb48ca'
53 | },
54 | '&:focus': {
55 | outline: 'none'
56 | }
57 | },
58 | '&.actions': {
59 | display: 'flex',
60 | flexFlow: 'row-reverse'
61 | }
62 | }
63 | })
64 |
65 | export const ConnectionInfo = styled('p')({
66 | fontSize: '10px',
67 | letterSpacing: '1px',
68 | color: theme.secTextColor,
69 | position: 'absolute',
70 | bottom: '20%',
71 | left: '50%',
72 | transform: 'translate(-50%, -50%)',
73 | '> span': {
74 | fontWeight: 700
75 | }
76 | })
77 |
78 | export const ConnectionError = styled('p')({
79 | fontSize: '12px',
80 | fontWeight: 700,
81 | letterSpacing: '1px',
82 | color: theme.error,
83 | position: 'absolute',
84 | top: '20%',
85 | left: '50%',
86 | transform: 'translate(-50%, -50%)'
87 | })
88 |
89 | export const Connecting = styled('p')({
90 | fontSize: '14px',
91 | fontWeight: 700,
92 | color: theme.info,
93 | position: 'absolute',
94 | top: '20%',
95 | left: '50%',
96 | transform: 'translate(-50%, -50%)'
97 | })
98 |
99 | export const Logo = styled('img')({
100 | position: 'absolute',
101 | top: '45%',
102 | left: '30%',
103 | transform: 'translate(-50%, -50%)'
104 | })
105 |
--------------------------------------------------------------------------------
/app/renderer/views/Dashboard/Chart/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react'
2 | import moment from 'moment'
3 | import throttle from 'lodash.throttle'
4 | import theme from '@/style/common'
5 | import { Panel } from './styles'
6 | import TimeChart from '../../../components/TimeChart/index'
7 |
8 | export default class Chart extends PureComponent {
9 | state = {
10 | chartWidth: 0,
11 | reads: [],
12 | writes: []
13 | }
14 |
15 | componentDidMount () {
16 | window.addEventListener('resize', this.updateDimensions)
17 |
18 | this.updateReads = throttle(() => {
19 | this.setState(({ reads }) => ({
20 | reads: [
21 | ...reads.slice(1),
22 | {
23 | a: new Date(),
24 | b: this.props.reads || 0
25 | }
26 | ]
27 | }))
28 | }, 1000)
29 |
30 | this.updateWrites = throttle(() => {
31 | this.setState(({ writes }) => ({
32 | writes: [
33 | ...writes.slice(1),
34 | {
35 | a: new Date(),
36 | b: this.props.writes || 0
37 | }
38 | ]
39 | }))
40 | }, 1000)
41 |
42 | this.updateReadsInterval = setInterval(this.updateReads, 1000)
43 | this.updateWritesInterval = setInterval(this.updateWrites, 1000)
44 |
45 | this.setState({
46 | chartWidth: window.innerWidth,
47 | reads: Array(8)
48 | .fill()
49 | .map(i => ({
50 | a: moment()
51 | .subtract(i + 1, 'second')
52 | .toDate(),
53 | b: 0
54 | }))
55 | .reverse(),
56 | writes: Array(8)
57 | .fill()
58 | .map(i => ({
59 | a: moment()
60 | .subtract(i + 1, 'second')
61 | .toDate(),
62 | b: 0
63 | }))
64 | .reverse()
65 | })
66 | }
67 |
68 | componentDidUpdate (prevProps, prevState, snapshot) {
69 | if (!prevProps || !prevProps.reads || prevProps.reads !== this.props.reads) {
70 | this.updateReads()
71 | }
72 |
73 | if (!prevProps || !prevProps.writes || prevProps.writes !== this.props.writes) {
74 | this.updateWrites()
75 | }
76 | }
77 |
78 | componentWillUnmount () {
79 | window.removeEventListener('resize', this.updateDimensions)
80 | this.updateReads.cancel()
81 | this.updateWrites.cancel()
82 | clearInterval(this.updateReadsInterval)
83 | clearInterval(this.updateWritesInterval)
84 | }
85 |
86 | updateDimensions = event => {
87 | this.setState({
88 | chartWidth: event.target.innerWidth
89 | })
90 | }
91 |
92 | render () {
93 | const { chartWidth, reads, writes } = this.state
94 |
95 | return (
96 |
97 |
104 |
105 | )
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/app/renderer/views/Dashboard/Chart/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'react-emotion'
2 | import theme from '@/style/common'
3 |
4 | export const Panel = styled('div')({
5 | display: 'flex',
6 | flexWrap: 'wrap',
7 | justifyContent: 'space-around',
8 | height: '20rem',
9 | margin: '2rem',
10 | borderBottom: `1px solid ${theme.mainBorderColor}`,
11 | border: `1px solid ${theme.mainBorderColor}`,
12 | background: `${theme.contrastColor}`,
13 | boxShadow: '0 2px 1px rgba(0, 0, 0, 0.04), inset 0 -1px 2px rgba(0, 0, 0, 0.05)'
14 | })
15 |
--------------------------------------------------------------------------------
/app/renderer/views/Dashboard/Dashboard.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Page from '../../components/Page'
3 | import LogsPanel from '../../components/LogsPanel'
4 | import Panels from './DashboardPanels/Panels'
5 | import Chart from './Chart'
6 | import { LogsContext } from '../../contexts/LogsContext'
7 |
8 | const Dashboard = props => {
9 | const { servers, tables, indexes, resources } = props.stats
10 | const { read_docs_per_sec: reads = 0, written_docs_per_sec: writes = 0 } = props.cluster || {}
11 |
12 | return (
13 |
14 |
15 |
16 | {logs => }
17 |
18 | )
19 | }
20 |
21 | export default Dashboard
22 |
--------------------------------------------------------------------------------
/app/renderer/views/Dashboard/DashboardPanels/Indexes.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PanelContent from './PanelContent'
3 |
4 | const IndexesPanel = props => {
5 | const {
6 | data: { secondaryIndexes, secondaryIndexesConstructing }
7 | } = props
8 |
9 | const indexesData = [
10 | { text: 'secondary indexes', value: secondaryIndexes },
11 | { text: 'indexes building', value: secondaryIndexesConstructing.length }
12 | ]
13 |
14 | return
15 | }
16 |
17 | export default IndexesPanel
18 |
--------------------------------------------------------------------------------
/app/renderer/views/Dashboard/DashboardPanels/PanelContent.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StyledPanelContent } from './panelStyles'
3 |
4 | const PanelContent = props => {
5 | const { hasIssues, title, statsData } = props
6 | return (
7 |
8 | {title}
9 |
10 | {statsData.map(item => {
11 | return (
12 | -
13 | {item.value}
14 | {item.text}
15 |
16 | )
17 | })}
18 |
19 |
20 | )
21 | }
22 |
23 | export default PanelContent
24 |
--------------------------------------------------------------------------------
/app/renderer/views/Dashboard/DashboardPanels/Panels.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ServersPanel from './Servers'
3 | import TablesPanel from './Tables'
4 | import IndexesPanel from './Indexes'
5 | import ResourcesPanel from './Resources'
6 | import { StyledPanels, StyledPanel } from './panelStyles'
7 |
8 | const Panels = props => {
9 | const { servers, tables, indexes, resources } = props
10 |
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 |
29 | Panels.defaultProps = {
30 | servers: {
31 | serversMissing: [],
32 | unknownMissing: 0,
33 | serversConnected: 0
34 | },
35 | tables: {
36 | tablesReady: 0,
37 | tablesNotReady: 0
38 | },
39 | indexes: {
40 | secondaryIndexes: 0,
41 | secondaryIndexesConstructing: []
42 | },
43 | resources: {
44 | cacheUsed: 0,
45 | cacheTotal: 0,
46 | diskUsed: 0
47 | }
48 | }
49 |
50 | export default Panels
51 |
--------------------------------------------------------------------------------
/app/renderer/views/Dashboard/DashboardPanels/Resources.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PanelContent from './PanelContent'
3 | import { formatBytes } from '../../../helpers/format'
4 |
5 | const ResourcesPanel = props => {
6 | const {
7 | data: { cacheUsed, cacheTotal, diskUsed }
8 | } = props
9 |
10 | const cachePercent = Math.ceil((cacheUsed / cacheTotal) * 100)
11 | const diskUsedFormat = formatBytes(diskUsed)
12 |
13 | const resourcesData = [
14 | { text: 'cache used', value: `${cachePercent}%` },
15 | { text: 'disk used', value: diskUsedFormat }
16 | ]
17 |
18 | return
19 | }
20 |
21 | export default ResourcesPanel
22 |
--------------------------------------------------------------------------------
/app/renderer/views/Dashboard/DashboardPanels/Servers.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import pluralize from 'pluralize'
3 | import PanelContent from './PanelContent'
4 |
5 | const ServersPanel = props => {
6 | const {
7 | data: { serversMissing = [], unknownMissing = 0, serversConnected = 0 }
8 | } = props
9 | const numMissing = serversMissing.length
10 | const missing =
11 | unknownMissing && numMissing > 0
12 | ? `${numMissing}+`
13 | : unknownMissing && numMissing <= 0
14 | ? '1+'
15 | : `${numMissing}`
16 | const hasIssues = unknownMissing || numMissing !== 0
17 |
18 | const serversData = [
19 | { text: `${pluralize('server', serversConnected)} connected`, value: serversConnected },
20 | { text: `${pluralize('server', missing)} missing`, value: numMissing }
21 | ]
22 |
23 | return
24 | }
25 |
26 | export default ServersPanel
27 |
--------------------------------------------------------------------------------
/app/renderer/views/Dashboard/DashboardPanels/Tables.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import pluralize from 'pluralize'
3 | import PanelContent from './PanelContent'
4 |
5 | const TablesPanel = props => {
6 | const {
7 | data: { tablesReady, tablesNotReady }
8 | } = props
9 |
10 | const hasIssues = tablesNotReady > 0
11 | const tablesData = [
12 | { text: `${pluralize('table', tablesReady)} ready`, value: tablesReady },
13 | { text: `${pluralize('table', tablesNotReady)} with issues`, value: tablesNotReady }
14 | ]
15 |
16 | return
17 | }
18 |
19 | export default TablesPanel
20 |
--------------------------------------------------------------------------------
/app/renderer/views/Dashboard/DashboardPanels/panelStyles.js:
--------------------------------------------------------------------------------
1 | import styled from 'react-emotion'
2 | import theme from '@/style/common'
3 |
4 | export const StyledPanels = styled('div')({
5 | padding: '10px',
6 | display: 'flex',
7 | justifyContent: 'space-around',
8 | width: '100%',
9 | backgroundColor: '#fff',
10 | borderBottom: `1px solid ${theme.mainBorderColor}`
11 | })
12 |
13 | export const StyledPanel = styled('div')({
14 | outline: 'none',
15 | padding: '10px',
16 | margin: '5px'
17 | })
18 |
19 | export const StyledPanelContent = styled('div')(({ hasIssues }) => ({
20 | h3: {
21 | fontWeight: 700,
22 | position: 'relative',
23 | marginBottom: '5px',
24 | '&:before': {
25 | content: "''",
26 | display: 'inline-block',
27 | marginRight: '6px',
28 | width: '12px',
29 | height: '12px',
30 | borderRadius: '50%',
31 | background: hasIssues ? theme.error : theme.success
32 | }
33 | },
34 | ul: {
35 | listStyle: 'none',
36 | li: {
37 | lineHeight: '22px',
38 | span: {
39 | fontWeight: 700,
40 | marginRight: '4px'
41 | }
42 | }
43 | }
44 | }))
45 |
--------------------------------------------------------------------------------
/app/renderer/views/Dashboard/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StatsContext } from '../../contexts/StatsContext'
3 | import Dashboard from './Dashboard'
4 |
5 | export default props => (
6 |
7 | {stats => }
8 |
9 | )
10 |
--------------------------------------------------------------------------------
/app/renderer/views/Explorer/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import MonacoEditor from 'react-monaco-editor'
3 | import * as monaco from 'monaco-editor'
4 | import Page from '../../components/Page'
5 | import { evalQuery } from '../../service/ipc'
6 | import styled from 'react-emotion'
7 | import { Button as BaseButton } from '../../components/Button'
8 | import { rebirthdbTypes } from './types'
9 |
10 | const options = {
11 | selectOnLineNumbers: true,
12 | roundedSelection: false,
13 | readOnly: false,
14 | cursorStyle: 'line',
15 | automaticLayout: false
16 | }
17 |
18 | const Container = styled.div`
19 | padding: 1rem;
20 | `
21 |
22 | const Pre = styled.pre`
23 | border: 1px solid black;
24 | margin: 1rem 0;
25 | padding: 1rem;
26 | `
27 |
28 | const Button = styled(BaseButton)`
29 | margin: 1rem 0;
30 | padding: 1rem 3rem;
31 | `
32 |
33 | const Explorer = () => {
34 | const [code, setCode] = useState('// type your code... \n \n r.dbList()')
35 | const [result, setResult] = useState('')
36 |
37 | useEffect(() => {
38 | // https://microsoft.github.io/monaco-editor/playground.html#extending-language-services-configure-javascript-defaults
39 | monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
40 | noSemanticValidation: true,
41 | noSyntaxValidation: false
42 | })
43 | monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
44 | target: monaco.languages.typescript.ScriptTarget.ES6,
45 | allowNonTsExtensions: true
46 | })
47 | monaco.languages.typescript.javascriptDefaults.addExtraLib(rebirthdbTypes, 'rebirthdb-ts.d.ts')
48 | }, [])
49 |
50 | function textChange (value) {
51 | setCode(value)
52 | }
53 |
54 | function query () {
55 | evalQuery(code).then(setResult)
56 | }
57 |
58 | return (
59 |
60 |
61 |
68 |
71 |
72 |
73 | {result}
74 |
75 |
76 |
77 |
78 | )
79 | }
80 |
81 | export default Explorer
82 |
--------------------------------------------------------------------------------
/app/renderer/views/Home/index.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import AppHeader from '../../components/AppHeader'
4 | import Icon from '../../components/Icon'
5 |
6 | import { StyledHome } from './styles.js'
7 |
8 | function Home (props) {
9 | return (
10 |
11 |
12 |
13 |
14 |
RethinkDB
15 |
16 |
17 |
No database connections added.
18 |
Click the "+" to add a RethinkDB connection.
19 |
20 |
21 |
22 |
23 |
24 |
25 | )
26 | }
27 |
28 | export default Home
29 |
--------------------------------------------------------------------------------
/app/renderer/views/Home/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'react-emotion'
2 |
3 | import theme from '@/style/common'
4 |
5 | export const StyledHome = styled.div`
6 | position: absolute;
7 | top: 50%;
8 | left: 50%;
9 | transform: translate(-50%, -50%);
10 | width: 760px;
11 |
12 | > .banner,
13 | > .empty__message {
14 | text-align: center;
15 | }
16 |
17 | > .banner {
18 | > .banner__title {
19 | font-size: 50px;
20 | font-weight: 900;
21 | background: ${theme.mainGradient};
22 | -webkit-background-clip: text;
23 | -webkit-text-fill-color: transparent;
24 | }
25 | }
26 |
27 | > .empty__message {
28 | margin-top: 50px;
29 |
30 | > h2 {
31 | font-size: 22px;
32 | }
33 |
34 | > p {
35 | margin: 10px 0 30px;
36 | }
37 | }
38 | `
39 |
--------------------------------------------------------------------------------
/app/renderer/views/Logs/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Page from '../../components/Page'
3 | import LogsPanel from '../../components/LogsPanel'
4 | import { LogsContext } from '../../contexts/LogsContext'
5 |
6 | const Logs = () => (
7 |
8 | {logs => }
9 |
10 | )
11 |
12 | export default Logs
13 |
--------------------------------------------------------------------------------
/app/renderer/views/Servers/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Page from '../../components/Page'
3 |
4 | const Tables = () => servers
5 |
6 | export default Tables
7 |
--------------------------------------------------------------------------------
/app/renderer/views/Tables/Database/DatabaseItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import TableItem from '../Table/TableItem'
3 | import { StyledDatabaseItem, DBLabel, DBActions, DBActionButton, EmptyList } from './styles'
4 | import { StyledTableList } from '../Table/styles'
5 |
6 | const DatabaseItem = props => {
7 | const { item, onTableSelect, openDeleteDatabaseModal, openAddTableModal } = props
8 | const { name, tables } = item
9 |
10 | const renderEmptyList = () => There are no tables in this database.
11 |
12 | const renderTables = () =>
13 | tables.map(t => )
14 |
15 | const onDBDelete = () => {
16 | openDeleteDatabaseModal(name)
17 | }
18 |
19 | const onAddTable = () => {
20 | openAddTableModal(name)
21 | }
22 |
23 | return (
24 |
25 |
37 | {tables.length ? renderTables() : renderEmptyList()}
38 |
39 | )
40 | }
41 |
42 | export default DatabaseItem
43 |
--------------------------------------------------------------------------------
/app/renderer/views/Tables/Database/DatabaseList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { StyledDatabaseList, EmptyList } from './styles'
3 | import DatabaseItem from './DatabaseItem'
4 |
5 | const DatabaseList = props => {
6 | const { list, onTableSelect, openDeleteDatabaseModal, openAddTableModal } = props
7 |
8 | const renderEmptyList = () => There are no databases in this cluster.
9 | const renderList = () =>
10 | list.map(db => (
11 |
18 | ))
19 |
20 | return {list.length ? renderList() : renderEmptyList()}
21 | }
22 |
23 | export default DatabaseList
24 |
--------------------------------------------------------------------------------
/app/renderer/views/Tables/Database/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'react-emotion'
2 | import theme from '@/style/common'
3 |
4 | export const StyledDatabaseList = styled('ul')({
5 | padding: '15px',
6 | listStyle: 'none'
7 | })
8 |
9 | export const StyledDatabaseItem = styled('li')({
10 | border: `1px solid ${theme.mainBorderColor}`,
11 | marginBottom: '10px',
12 | header: {
13 | background: theme.contrastColor,
14 | padding: '8px',
15 | borderBottom: `1px solid ${theme.mainBorderColor}`,
16 | display: 'flex',
17 | '.db-name': {
18 | paddingLeft: '20px',
19 | fontWeight: 500,
20 | fontSize: '16px'
21 | }
22 | }
23 | })
24 |
25 | export const DBLabel = styled('span')({
26 | display: 'inline-block',
27 | padding: '0 7px',
28 | height: '22px',
29 | lineHeight: '20px',
30 | textAlign: 'center',
31 | color: '#eb2f96',
32 | background: '#fff0f6',
33 | border: '1px solid #ffadd2',
34 | fontSize: '12px'
35 | })
36 |
37 | export const DBActions = css({
38 | marginLeft: 'auto'
39 | })
40 |
41 | export const DBActionButton = css({
42 | marginLeft: '8px',
43 | padding: '5px 12px',
44 | background: 'transparent',
45 | border: '1px solid #603e85',
46 | color: '#603e85',
47 | transition: 'all ease-in 250ms',
48 | cursor: 'pointer',
49 | '&:hover': {
50 | borderColor: '#eb48ca',
51 | color: '#eb48ca'
52 | },
53 | '&:focus': {
54 | outline: 'none'
55 | },
56 | '&:disabled': {
57 | border: '1px solid #aaa',
58 | color: '#aaa',
59 | cursor: 'auto'
60 | }
61 | })
62 |
63 | export const EmptyList = styled('div')({
64 | textAlign: 'center',
65 | fontSize: '15px',
66 | padding: '14px 0'
67 | })
68 |
--------------------------------------------------------------------------------
/app/renderer/views/Tables/Modals/AddDatabase.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Modal from '../../../components/Modal'
3 | import Alert from '../../../components/Alert'
4 | import { StyledModal, StyledModalActions, ActionButton, ModalTextInput } from './styles'
5 |
6 | class AddDatabase extends Component {
7 | state = {
8 | dbName: ''
9 | }
10 |
11 | onNameChange = e => {
12 | this.setState({ dbName: e.currentTarget.value })
13 | }
14 | onAdd = () => {
15 | const { onSubmit } = this.props
16 | const { dbName } = this.state
17 | this.setState({ dbName: '' })
18 | onSubmit(dbName)
19 | }
20 | renderError = error => {
21 | return {error}
22 | }
23 |
24 | render () {
25 | const { error, onCancel } = this.props
26 | const { dbName } = this.state
27 | return (
28 |
29 |
30 | {error ? this.renderError(error) : null}
31 |
40 |
41 |
44 |
47 |
48 |
49 |
50 | )
51 | }
52 | }
53 |
54 | export default AddDatabase
55 |
--------------------------------------------------------------------------------
/app/renderer/views/Tables/Modals/AddTable.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Modal from '../../../components/Modal'
3 | import Alert from '../../../components/Alert'
4 | import Switch from '../../../components/Switch'
5 | import { StyledModal, StyledModalActions, ActionButton, ModalTextInput, ModalRow } from './styles'
6 |
7 | class AddTable extends Component {
8 | state = {
9 | tblName: '',
10 | primaryKey: '',
11 | durability: true
12 | }
13 |
14 | onClose = e => {
15 | this.setState({
16 | tblName: '',
17 | primaryKey: '',
18 | durability: true
19 | })
20 | }
21 |
22 | onNameChange = e => {
23 | this.setState({ tblName: e.currentTarget.value })
24 | }
25 | onPrimaryKeyChange = e => {
26 | this.setState({ primaryKey: e.currentTarget.value })
27 | }
28 | onDurabilityChange = checked => {
29 | this.setState({ durability: checked })
30 | }
31 | onAdd = () => {
32 | const { onSubmit } = this.props
33 | const { tblName, primaryKey, durability } = this.state
34 | this.setState({
35 | tblName: '',
36 | primaryKey: '',
37 | durability: true
38 | })
39 | onSubmit({ name: tblName, primaryKey, durability })
40 | }
41 | renderError = error => {
42 | return {error}
43 | }
44 |
45 | render () {
46 | const { error, onCancel } = this.props
47 | const { dbName, primaryKey, durability } = this.state
48 | return (
49 |
50 |
51 | {error ? this.renderError(error) : null}
52 |
61 |
70 |
71 |
72 |
73 |
74 |
81 |
82 |
83 |
86 |
89 |
90 |
91 |
92 | )
93 | }
94 | }
95 |
96 | export default AddTable
97 |
--------------------------------------------------------------------------------
/app/renderer/views/Tables/Modals/DeleteDatabase.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Modal from '../../../components/Modal'
3 | import Alert from '../../../components/Alert'
4 | import { StyledModal, StyledModalActions, ActionButton, ModalTextInput } from './styles'
5 |
6 | class DeleteDatabase extends Component {
7 | state = {
8 | dbName: '',
9 | validationError: undefined
10 | }
11 |
12 | onNameChange = e => {
13 | this.setState({ dbName: e.currentTarget.value })
14 | }
15 | onClose = e => {
16 | this.setState({ dbName: '', validationError: undefined })
17 | }
18 | handleDelete = () => {
19 | const { onDelete, dbToDelete } = this.props
20 | const { dbName } = this.state
21 | if (dbName !== dbToDelete) {
22 | this.setState({
23 | validationError: "This name doesn't match the name of the database you're trying to delete."
24 | })
25 | } else {
26 | this.setState({ dbName: '', validationError: undefined })
27 | onDelete(dbToDelete)
28 | }
29 | }
30 |
31 | renderError = error => {
32 | return {error}
33 | }
34 |
35 | render () {
36 | const { error, onCancel } = this.props
37 | const { dbName, validationError } = this.state
38 | const err = error || validationError
39 | return (
40 |
41 |
42 |
43 | Deleting the database will delete all the tables in it.
44 |
45 | This action cannot be undone.
46 |
47 | {err ? this.renderError(err) : null}
48 |
57 |
58 |
61 |
64 |
65 |
66 |
67 | )
68 | }
69 | }
70 |
71 | export default DeleteDatabase
72 |
--------------------------------------------------------------------------------
/app/renderer/views/Tables/Modals/DeleteTables.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Modal from '../../../components/Modal'
3 | import Alert from '../../../components/Alert'
4 | import { StyledModal, StyledModalActions, ActionButton } from './styles'
5 |
6 | const DeleteTables = props => {
7 | const { selectedTables, onCancel, onDelete } = props
8 | const deleteText = selectedTables.length === 1 ? 'this table' : 'these tables'
9 | return (
10 |
11 |
12 |
13 | Deleting a table will delete all its data. This action cannot be reversed.
14 |
15 |
16 | {`Are you sure you want to delete ${deleteText}:`}
17 |
18 |
27 |
28 |
31 |
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | export default DeleteTables
41 |
--------------------------------------------------------------------------------
/app/renderer/views/Tables/Modals/styles.js:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'react-emotion'
2 | import theme from '@/style/common'
3 |
4 | export const StyledModal = styled('div')({
5 | h4: {
6 | margin: '15px 0'
7 | },
8 | ul: {
9 | listStyle: 'none',
10 | li: {
11 | marginBottom: '10px',
12 | a: {
13 | fontSize: '14px',
14 | fontWeight: 600,
15 | textDecoration: 'none',
16 | color: '#444258',
17 | transition: 'all fadeIn 300ms',
18 | '&:hover': {
19 | color: '#eb48ca'
20 | }
21 | }
22 | }
23 | }
24 | })
25 |
26 | export const StyledModalActions = styled('div')({
27 | display: 'flex',
28 | flexDirection: 'row-reverse',
29 | paddingTop: '10px'
30 | })
31 |
32 | export const ModalTextInput = css({
33 | margin: '10px 0',
34 | width: '100%',
35 | height: '38px',
36 | background: 'transparent',
37 | border: 'none',
38 | borderBottom: '1px solid #603e85',
39 | transition: 'all ease-in 500ms',
40 | color: theme.mainTextColor,
41 | '&:focus': {
42 | outline: 'none',
43 | borderBottomColor: '#eb48ca'
44 | }
45 | })
46 |
47 | export const ModalRow = css({
48 | margin: '10px 0',
49 | width: '100%'
50 | })
51 |
52 | export const ActionButton = css({
53 | marginLeft: '8px',
54 | padding: '5px 12px',
55 | background: 'transparent',
56 | border: '1px solid #603e85',
57 | color: '#603e85',
58 | transition: 'all ease-in 250ms',
59 | cursor: 'pointer',
60 | '&:hover': {
61 | borderColor: '#eb48ca',
62 | color: '#eb48ca'
63 | },
64 | '&:focus': {
65 | outline: 'none'
66 | },
67 | '&:disabled': {
68 | border: '1px solid #aaa',
69 | color: '#aaa',
70 | cursor: 'auto'
71 | }
72 | })
73 |
--------------------------------------------------------------------------------
/app/renderer/views/Tables/Table/TableItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { humanizeTableReadiness } from '../../../helpers/format'
3 | import Icon from '../../../components/Icon'
4 | import { StyledTableItem, TableStatus } from './styles'
5 |
6 | const TableItem = props => {
7 | const { table, onTableSelect } = props
8 | const { id, name, db, replicas, replicasReady, shards, status } = table
9 | const { label, value } = humanizeTableReadiness(status, replicasReady, replicas)
10 |
11 | const handleSelect = () => onTableSelect({ id, name, db })
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
21 |
22 |
23 |
24 | replicas: {replicas}, shards: {shards}
25 |
26 |
27 |
30 |
31 | )
32 | }
33 |
34 | export default TableItem
35 |
--------------------------------------------------------------------------------
/app/renderer/views/Tables/Table/styles.js:
--------------------------------------------------------------------------------
1 | import styled from 'react-emotion'
2 | import theme from '@/style/common'
3 |
4 | export const StyledTableList = styled('ul')({
5 | listStyle: 'none',
6 | background: '#fff'
7 | })
8 |
9 | export const StyledTableItem = styled('li')({
10 | padding: '12px 8px',
11 | display: 'grid',
12 | gridTemplateColumns: '42px 1fr 300px 130px',
13 | gridTemplateRows: 'auto',
14 | gridTemplateAreas: "'select name shardsReplicas status'",
15 | borderBottom: `1px solid ${theme.mainBorderColor}`,
16 | '.tableSelect': {
17 | gridArea: 'select'
18 | },
19 | '.tableName': {
20 | gridArea: 'name',
21 | a: {
22 | fontSize: '16px',
23 | fontWeight: 600,
24 | textDecoration: 'none',
25 | color: '#444258',
26 | transition: 'all fadeIn 300ms',
27 | '&:hover': {
28 | color: '#eb48ca'
29 | }
30 | }
31 | },
32 | '.tableShardsReplicas': {
33 | gridArea: 'shardsReplicas',
34 | '.tableShardsReplicasIcon': {
35 | position: 'relative',
36 | top: '3px'
37 | },
38 | '.tableShardsReplicasText': {
39 | paddingLeft: '8px'
40 | }
41 | },
42 | '.tableStatus': {
43 | gridArea: 'status'
44 | },
45 | ':last-child': {
46 | borderBottom: 'none'
47 | }
48 | })
49 |
50 | const sLevels = {
51 | success: theme.success,
52 | 'partial-success': theme.warning,
53 | failure: theme.error
54 | }
55 | export const TableStatus = styled('div')(props => ({
56 | '&:before': {
57 | content: "''",
58 | display: 'inline-block',
59 | marginRight: '8px',
60 | width: '11px',
61 | height: '11px',
62 | borderRadius: '50%',
63 | verticalAlign: 'middle',
64 | background: sLevels[props.status]
65 | }
66 | }))
67 |
--------------------------------------------------------------------------------
/app/renderer/views/Tables/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent, Fragment } from 'react'
2 | import { query, action } from '../../service/ipc'
3 | import Page from '../../components/Page'
4 | import ActionsBar from '../../components/ActionsBar'
5 | import Toast from '../../components/Toast'
6 | import DatabaseList from './Database/DatabaseList'
7 | import AddTable from './Modals/AddTable'
8 | import DeleteTables from './Modals/DeleteTables'
9 | import AddDatabase from './Modals/AddDatabase'
10 | import DeleteDatabase from './Modals/DeleteDatabase'
11 | import { validateAddTable, validateAddDatabase } from './tableHelpers'
12 |
13 | import { DBActionButton, DBActions } from './Database/styles'
14 |
15 | class Tables extends PureComponent {
16 | constructor (props) {
17 | super(props)
18 | this.state = {
19 | tablesByDb: [],
20 | selectedTables: [],
21 | addTableVisible: false,
22 | addTableError: undefined,
23 | addToDb: undefined,
24 | deleteTablesVisible: false,
25 | addDatabaseVisible: false,
26 | addDatabaseError: undefined,
27 | dbToDelete: undefined,
28 | deleteDatabaseVisible: false,
29 | deleteDatabaseError: undefined
30 | }
31 | }
32 |
33 | fetchTables = () => {
34 | return query({ name: 'tablesByDb' })
35 | }
36 |
37 | addTable = async ({ name, primaryKey, durability }) => {
38 | const { tablesByDb, addToDb } = this.state
39 | // get all tables of the selected db
40 | const selectedDb = tablesByDb.find(db => db.name === addToDb)
41 | const selectedDbTables = selectedDb.tables
42 | try {
43 | // validation
44 | const validationError = validateAddTable(name, selectedDbTables)
45 | if (validationError) {
46 | this.setState({ addTableError: validationError })
47 | } else {
48 | // prepare payload
49 | durability = durability ? 'hard' : 'soft'
50 | primaryKey = primaryKey || 'id'
51 |
52 | const result = await action({
53 | name: 'addTable',
54 | payload: { db: addToDb, name, primaryKey, durability }
55 | })
56 | if (result.errors === 0) {
57 | this.setState({
58 | addTableError: undefined
59 | })
60 | const tablesByDb = await this.fetchTables()
61 | this.setState({ tablesByDb })
62 | this.closeModals()
63 | Toast.success(`The table ${addToDb}.${name} was successfully created.`)
64 |
65 | // allow outdated reads to update
66 | setTimeout(async () => {
67 | const tablesByDb = await this.fetchTables()
68 | this.setState({ tablesByDb })
69 | }, 3000)
70 | } else {
71 | this.setState({ addTableError: 'The returned result was not `{created: 1}`' })
72 | }
73 | }
74 | } catch (e) {
75 | console.error(e)
76 | this.setState({ addDatabaseError: e.message })
77 | }
78 | }
79 |
80 | addDatabase = async name => {
81 | let tablesByDb = this.state.tablesByDb
82 | try {
83 | // validation
84 | const validationError = validateAddDatabase(name, tablesByDb)
85 | if (validationError) {
86 | this.setState({ addDatabaseError: validationError })
87 | } else {
88 | const result = await action({
89 | name: 'addDatabase',
90 | payload: { name }
91 | })
92 | if (result.inserted === 1) {
93 | this.setState({
94 | addDatabaseError: undefined
95 | })
96 | const tablesByDb = await this.fetchTables()
97 | this.setState({ tablesByDb })
98 | this.closeModals()
99 | Toast.success('Database created!')
100 | } else {
101 | this.setState({ addDatabaseError: result.first_error || 'Unknown error' })
102 | }
103 | }
104 | } catch (e) {
105 | console.error(e)
106 | this.setState({ addDatabaseError: e.message })
107 | }
108 | }
109 |
110 | deleteDatabase = async name => {
111 | try {
112 | const result = await action({
113 | name: 'deleteDatabase',
114 | payload: { name }
115 | })
116 |
117 | if (result.dbs_dropped === 1) {
118 | this.setState({
119 | deleteDatabaseError: undefined
120 | })
121 | const tablesByDb = await this.fetchTables()
122 | this.setState({ tablesByDb })
123 | this.closeModals()
124 | Toast.success('Database deleted!')
125 | } else {
126 | this.setState({ deleteDatabaseError: 'The return result was not `{dbs_dropped: 1}`' })
127 | }
128 | } catch (e) {
129 | console.error(e)
130 | this.setState({ deleteDatabaseError: e.message })
131 | }
132 | }
133 |
134 | openAddTableModal = db => {
135 | this.setState({ addTableVisible: true, addToDb: db, addTableError: undefined })
136 | }
137 | openDeleteTablesModal = () => {
138 | this.setState({ deleteTablesVisible: true })
139 | }
140 | openAddDatabaseModal = () => {
141 | this.setState({ addDatabaseVisible: true })
142 | }
143 | openDeleteDatabaseModal = db => {
144 | this.setState({ deleteDatabaseVisible: true, dbToDelete: db })
145 | }
146 |
147 | deleteTables = async () => {
148 | try {
149 | const tables = this.state.selectedTables
150 | const result = await action({
151 | name: 'deleteTables',
152 | payload: tables.map(table => ({ db: table.db, name: table.name }))
153 | })
154 | if (result.changes) {
155 | if (result.deleted === tables.length) {
156 | this.setState({
157 | selectedTables: []
158 | })
159 | const tablesByDb = await this.fetchTables()
160 | this.setState({ tablesByDb })
161 | this.closeModals()
162 | Toast.success('Table deleted!')
163 | } else {
164 | this.closeModals()
165 | Toast.warning('The value returned for `deleted` did not match the number of tables.')
166 | }
167 | }
168 | } catch (e) {
169 | console.error(e)
170 | this.closeModals()
171 | Toast.error(e.message)
172 | }
173 | }
174 |
175 | closeModals = e => {
176 | this.setState({
177 | addTableVisible: false,
178 | deleteTablesVisible: false,
179 | addDatabaseVisible: false,
180 | deleteDatabaseVisible: false,
181 | addDatabaseError: undefined,
182 | dbToDelete: undefined
183 | })
184 | }
185 |
186 | onTableSelect = table => {
187 | let arr = this.state.selectedTables
188 |
189 | if (arr.find(item => item.id === table.id)) {
190 | arr = arr.filter(item => item.id !== table.id)
191 | } else {
192 | arr = [...arr, table]
193 | }
194 |
195 | this.setState({ selectedTables: arr })
196 | }
197 |
198 | async componentDidMount () {
199 | try {
200 | const tablesByDb = await this.fetchTables()
201 | this.setState({ tablesByDb })
202 | } catch (e) {
203 | console.error(e)
204 | }
205 | }
206 |
207 | render () {
208 | const {
209 | tablesByDb,
210 | selectedTables,
211 | addTableVisible,
212 | addTableError,
213 | deleteTablesVisible,
214 | addDatabaseVisible,
215 | addDatabaseError,
216 | deleteDatabaseVisible,
217 | deleteDatabaseError,
218 | dbToDelete
219 | } = this.state
220 | return (
221 |
222 |
223 |
224 |
225 |
228 |
235 |
236 |
237 |
243 |
244 |
252 |
260 |
268 |
277 |
278 | )
279 | }
280 | }
281 |
282 | export default Tables
283 |
--------------------------------------------------------------------------------
/app/renderer/views/Tables/tableHelpers.js:
--------------------------------------------------------------------------------
1 | export const validateAddDatabase = (name, list) => {
2 | let error
3 | // Empty name is not valid
4 | if (!name.trim().length) {
5 | error = 'Please do not use an empty name for a database'
6 | } else if (!/^[a-zA-Z0-9_]+$/.test(name)) {
7 | // Only alphanumeric char + underscore are allowed
8 | error = 'You can only use alphanumeric characters and underscores for a database name'
9 | } else if (list.find(db => db.name === name)) {
10 | // Check if it's a duplicate
11 | error = "The chosen database's name is already being used. Please choose another one"
12 | }
13 | return error
14 | }
15 |
16 | export const validateAddTable = (name, list) => {
17 | let error
18 | // Need a name
19 | if (!name.trim().length) {
20 | error = 'Please do not use an empty name for a table'
21 | } else if (!/^[a-zA-Z0-9_]+$/.test(name)) {
22 | // Only alphanumeric char + underscore are allowed
23 | error = 'You can only use alphanumeric characters and underscores for a table name'
24 | } else if (list.find(table => table.name === name)) {
25 | // And a name that doesn't exist
26 | error = 'The chosen table name is already exists. Please choose another one'
27 | }
28 | return error
29 | }
30 |
--------------------------------------------------------------------------------
/app/shared/channels.js:
--------------------------------------------------------------------------------
1 | // Channels
2 | const CONNECT_CHANNEL_NAME = 'CONNECT'
3 | const STATS_CHANNEL_NAME = 'STATS'
4 | const QUERIES_CHANNEL_NAME = 'QUERIES'
5 | const ACTIONS_CHANNEL_NAME = 'ACTIONS'
6 | const CLUSTER_CHANNEL_NAME = 'CLUSTER'
7 | const EVAL_QUERY_CHANNEL_NAME = 'EVAL_QUERY'
8 |
9 | module.exports = {
10 | CONNECT_CHANNEL_NAME,
11 | CLUSTER_CHANNEL_NAME,
12 | STATS_CHANNEL_NAME,
13 | QUERIES_CHANNEL_NAME,
14 | ACTIONS_CHANNEL_NAME,
15 | EVAL_QUERY_CHANNEL_NAME
16 | }
17 |
--------------------------------------------------------------------------------
/docs/SystemTables.md:
--------------------------------------------------------------------------------
1 | # System Tables
2 |
3 | ## Cluster
4 | - [cluser_config][1] stores the authentication key for the cluster.
5 | ```
6 | $ r.db("rethinkdb").table("cluser_config")
7 | {
8 | id: "heartbeat",
9 | heartbeat_timeout_secs:
10 | }
11 | ```
12 | - [stats][2] statistics about cluster read/write throughput.
13 | ```
14 | $ r.db("rethinkdb").table("stats").get(["cluster"])
15 | {
16 | id: ["cluster"],
17 | query_engine: {
18 | queries_per_sec: ,
19 | read_docs_per_sec: ,
20 | written_docs_per_sec:
21 | }
22 | }
23 | ```
24 |
25 | ## Servers
26 | - [server_config][1] stores server names and tags.
27 | ```
28 | $ r.db("rethinkdb").table("server_config")
29 | {
30 | id: ,
31 | name:
32 | cache_size_mb: || "auto",
33 | tags: []
34 | }
35 | ```
36 |
37 | - [server_status][1] status and configuration of tables.
38 | ```
39 | $ r.db("rethinkdb").table("server_status")
40 | {
41 | id: ,
42 | name: ,
43 | hostname: ,
44 | network: {
45 | canonical_addresses: [
46 | {
47 | host: ,
48 | port:
49 | }
50 | ],
51 | cluster_port: ,
52 | connected_to: {
53 |
54 | },
55 | http_admin_port: ,
56 | reql_port: ,
57 | time_connected:
58 | },
59 | process: {
60 | argv: [
61 | "rethinkdb"
62 | ],
63 | cache_size_mb: ,
64 | pid: ,
65 | time_started: ,
66 | version: "rethinkdb 2.3.6~0xenial (GCC 5.3.1)"
67 | }
68 | }
69 | ```
70 |
71 | - [stats][2] statistics about server read/write throughput, client connections, and memory usage.
72 | ```
73 | $ r.db("rethinkdb").table("stats").get(["server", "31c92680-f70c-4a4b-a49e-b238eb12c023"])
74 | {
75 | id: ["server", ],
76 | server: or ,
77 | query_engine: {
78 | queries_per_sec: ,
79 | queries_total: ,
80 | read_docs_per_sec: ,
81 | read_docs_total: ,
82 | written_docs_per_sec: ,
83 | written_docs_total: ,
84 | client_connections:
85 | }
86 | }
87 | ```
88 |
89 | ## DB
90 | - [db_config][1] stores database UUIDs and name
91 | ```
92 | $ r.db("rethinkdb").table("db_config").get(["table", "31c92680-f70c-4a4b-a49e-b238eb12c023"])
93 | {
94 | "id": ,
95 | "name":
96 | }
97 | ```
98 |
99 |
100 | ### Tables
101 | - [table_config][1] stores table configurations, including sharding and replication.
102 | ```
103 | $ r.db("rethinkdb").table("table_config")
104 | {
105 | id: ,
106 | name: ,
107 | db: ,
108 | durability: ,
109 | primary_key:
110 | indexs [],
111 | shards: [
112 | {
113 | primary_replica: (db name),
114 | replicas: [(db name)],
115 | }
116 | ]
117 | }
118 | ```
119 |
120 | - [table_status][1] status and configuration of tables.
121 | ```
122 | $ r.db("rethinkdb").table("table_status")
123 | {
124 | id: ,
125 | name: ,
126 | db:
127 | shards: [
128 | primary_replica: [(db name)],
129 | replicas: [(db name)]
130 | ]
131 | }
132 | ```
133 |
134 | - [stats][2] statistics about server read/write throughput, client connections, and memory usage.
135 | ```
136 | $ r.db("rethinkdb").table("stats").get(["table", "31c92680-f70c-4a4b-a49e-b238eb12c023"])
137 | {
138 | id: ["table", ],
139 | table: or ,
140 | db: or ,
141 | query_engine: {
142 | read_docs_per_sec: ,
143 | written_docs_per_sec:
144 | }
145 | }
146 | ```
147 | ```
148 | $ r.db("rethinkdb").table("stats").get(["table_server", "31c92680-f70c-4a4b-a49e-b238eb12c023", "de8b75d1-3184-48f0-b1ef-99a9c04e2be5"]).run(conn, callback);
149 | {
150 | id: ["table_server", , ] // table_id, server_id
151 | server: or ,
152 | table: or ,
153 | db: or ,
154 | query_engine: {
155 | read_docs_per_sec: ,
156 | read_docs_total: ,
157 | written_docs_per_sec: ,
158 | written_docs_total:
159 | },
160 | storage_engine: {
161 | cache: {
162 | in_use_bytes:
163 | },
164 | disk: {
165 | read_bytes_per_sec: ,
166 | read_bytes_total: ,
167 | written_bytes_per_sec: ,
168 | written_bytes_total: ,
169 | space_usage: {
170 | metadata_bytes: ,
171 | data_bytes: ,
172 | garbage_bytes: ,
173 | preallocated_bytes:
174 | }
175 | }
176 | }
177 | }
178 | ```
179 |
180 |
181 | [1]: https://rethinkdb.com/docs/system-tables/
182 | [2]: https://rethinkdb.com/docs/system-stats/
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | verbose: true,
3 | clearMocks: true,
4 | silent: true
5 | }
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow } = require('electron')
2 | const initMain = require('./app/main')
3 | // import MenuBuilder from './menu'
4 |
5 | let mainWindow = null
6 |
7 | // if (process.env.NODE_ENV === 'production') {
8 | // const sourceMapSupport = require('source-map-support')
9 | // sourceMapSupport.install()
10 | // }
11 |
12 | if (
13 | process.env.NODE_ENV === 'development' ||
14 | process.env.DEBUG_PROD === 'true'
15 | ) {
16 | require('electron-debug')()
17 | // const path = require('path')
18 | // const p = path.join(__dirname, '..', 'app', 'node_modules')
19 | // require('module').globalPaths.push(p)
20 | }
21 |
22 | const installExtensions = async () => {
23 | const installer = require('electron-devtools-installer')
24 | const forceDownload = !!process.env.UPGRADE_EXTENSIONS
25 | const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS']
26 |
27 | return Promise.all(
28 | extensions.map(name => installer.default(installer[name], forceDownload))
29 | ).catch(console.log)
30 | }
31 |
32 | /**
33 | * Add event listeners...
34 | */
35 |
36 | app.on('window-all-closed', () => {
37 | // Respect the OSX convention of having the application in memory even
38 | // after all windows have been closed
39 | if (process.platform !== 'darwin') {
40 | app.quit()
41 | }
42 | })
43 |
44 | app.on('ready', async () => {
45 | initMain()
46 |
47 | if (
48 | process.env.NODE_ENV === 'development' ||
49 | process.env.DEBUG_PROD === 'true'
50 | ) {
51 | await installExtensions()
52 | }
53 |
54 | mainWindow = new BrowserWindow({
55 | show: false,
56 | width: 1200,
57 | height: 728,
58 | minWidth: 1200,
59 | minHeight: 728,
60 | titleBarStyle: 'hiddenInset',
61 | movable: true
62 | })
63 |
64 | mainWindow.loadURL(`file://${__dirname}/app/renderer/index.html`)
65 |
66 | // @TODO: Use 'ready-to-show' event
67 | // https://github.com/electron/electron/blob/master/docs/api/browser-window.md#using-ready-to-show-event
68 | mainWindow.webContents.on('did-finish-load', () => {
69 | if (!mainWindow) {
70 | throw new Error('"mainWindow" is not defined')
71 | }
72 | mainWindow.show()
73 | mainWindow.focus()
74 | })
75 |
76 | mainWindow.on('show', event => {
77 | if (process.env.NODE_ENV === 'development')
78 | mainWindow.webContents.openDevTools()
79 | })
80 |
81 | mainWindow.on('close', () => {
82 | mainWindow.webContents.closeDevTools()
83 | })
84 |
85 | mainWindow.on('closed', () => {
86 | mainWindow = null
87 | })
88 |
89 | // const menuBuilder = new MenuBuilder(mainWindow)
90 | // menuBuilder.buildMenu()
91 | })
92 |
93 | app.commandLine.appendSwitch('ignore-certificate-errors', 'true');
94 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rebirthdb-desktop",
3 | "version": "1.0.0",
4 | "description": "RebirthDB desktop client",
5 | "main": "main.js",
6 | "scripts": {
7 | "prod": "webpack-cli --mode production --config webpack.config.prod.js",
8 | "dev": "cross-env START_HOT=1 cross-env START_HOT=1 npm run start-renderer-dev",
9 | "package": "npm run build && build --publish never",
10 | "package-all": "npm run build && build -mwl",
11 | "package-linux": "npm run build && build --linux",
12 | "package-win": "npm run build && build --win --x64",
13 | "precommit": "lint-staged",
14 | "standard": "standard --fix 'app/**/*.js'",
15 | "standard-html": "standard --fix --plugin html '**/*.html'",
16 | "standard-markdown": "standard --fix --plugin markdown '**/*.md'",
17 | "start": "cross-env NODE_ENV=production electron .",
18 | "start-main-dev": "cross-env HOT=1 NODE_ENV=development electron .",
19 | "start-renderer-dev": "cross-env NODE_ENV=development node --trace-warnings -r babel-register ./node_modules/webpack-dev-server/bin/webpack-dev-server --mode development --config webpack.config.dev.js --https",
20 | "test": "npm run standard && jest --env=node",
21 | "test-all": "npm run lint && npm run flow && npm run build && npm run test && npm run test-e2e",
22 | "test-e2e": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 node --trace-warnings -r babel-register ./internals/scripts/RunTests.js e2e",
23 | "test-watch": "npm test -- --watch",
24 | "lint": "prettier 'app/**/*.js' --write && npm run standard && npm run standard-markdown"
25 | },
26 | "repository": {
27 | "type": "git",
28 | "url": "git+https://github.com/RebirthDB/rebirthdb-desktop.git"
29 | },
30 | "keywords": [],
31 | "author": "",
32 | "license": "Apache-2.0",
33 | "bugs": {
34 | "url": "https://github.com/RebirthDB/rebirthdb-desktop/issues"
35 | },
36 | "homepage": "https://github.com/RebirthDB/rebirthdb-desktop#readme",
37 | "dependencies": {
38 | "add": "^2.0.6",
39 | "babel-plugin-emotion": "^9.1.2",
40 | "electron-better-ipc": "^0.1.1",
41 | "electron-store": "^2.0.0",
42 | "emotion": "^9.1.3",
43 | "formik": "^0.11.11",
44 | "history": "^4.9.0",
45 | "lodash": "^4.17.10",
46 | "lodash.throttle": "^4.1.1",
47 | "moment": "^2.22.2",
48 | "pluralize": "^7.0.0",
49 | "prop-types": "^15.7.2",
50 | "randomcolor": "^0.5.3",
51 | "rc-dialog": "^7.1.7",
52 | "rc-dropdown": "^2.2.0",
53 | "rc-menu": "^7.0.5",
54 | "react": "^16.8.4",
55 | "react-burger-menu": "^2.5.0",
56 | "react-dom": "^16.8.4",
57 | "react-emotion": "^9.1.3",
58 | "react-error-boundary": "^1.2.2",
59 | "react-monaco-editor": "^0.25.1",
60 | "react-router": "^4.4.0",
61 | "react-router-dom": "^4.4.0",
62 | "react-s-alert": "^1.4.1",
63 | "react-switch": "^3.0.4",
64 | "rethinkdb": "^2.3.3",
65 | "rethinkdb-ts": "^2.4.0-rc.6",
66 | "shortid": "^2.2.8",
67 | "victory": "^30.0.0"
68 | },
69 | "devDependencies": {
70 | "babel-core": "^6.26.3",
71 | "babel-eslint": "^10.0.1",
72 | "babel-jest": "^23.6.0",
73 | "babel-loader": "^7.1.4",
74 | "babel-plugin-add-module-exports": "^0.2.1",
75 | "babel-plugin-import": "^1.7.0",
76 | "babel-plugin-transform-class-properties": "^6.24.1",
77 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
78 | "babel-plugin-transform-runtime": "^6.23.0",
79 | "babel-preset-env": "^1.6.1",
80 | "babel-preset-react": "^6.24.1",
81 | "babel-preset-stage-0": "^6.24.1",
82 | "cross-env": "^5.1.5",
83 | "css-loader": "^0.28.11",
84 | "electron": "^4.1.0",
85 | "electron-builder": "^20.13.2",
86 | "electron-debug": "^2.1.0",
87 | "electron-devtools-installer": "^2.2.4",
88 | "electron-reload": "^1.2.2",
89 | "error-overlay-webpack-plugin": "^0.1.4",
90 | "eslint-plugin-html": "^5.0.3",
91 | "eslint-plugin-markdown": "^1.0.0-beta.6",
92 | "file-loader": "^1.1.11",
93 | "html-loader": "^0.5.5",
94 | "husky": "^0.14.3",
95 | "jest": "^22.4.3",
96 | "json-loader": "^0.5.7",
97 | "less": "^3.0.2",
98 | "less-loader": "^4.1.0",
99 | "lint-staged": "^8.1.5",
100 | "monaco-editor-webpack-plugin": "^1.5.3",
101 | "prettier": "^1.12.1",
102 | "react-hot-loader": "^4.8.0",
103 | "react-test-renderer": "^16.8.4",
104 | "standard": "^12.0.1",
105 | "style-loader": "^0.23.1",
106 | "svg-sprite-loader": "^3.7.3",
107 | "url-loader": "^1.0.1",
108 | "webpack": "4.25.1",
109 | "webpack-cli": "^3.3.0",
110 | "webpack-dev-server": "^3.1.3"
111 | },
112 | "standard": {
113 | "parser": "babel-eslint",
114 | "ignore": [
115 | "/**/*.test.js"
116 | ],
117 | "env": [
118 | "jest"
119 | ]
120 | },
121 | "lint-staged": {
122 | "*.{html,md,js}": [
123 | "npm run lint",
124 | "git add"
125 | ]
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | const { spawn } = require('child_process')
2 | const path = require('path')
3 | const webpack = require('webpack')
4 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')
5 | const PORT = process.env.PORT || 7000
6 | const PUBLIC_PATH = `https://localhost:${PORT}/dist/`
7 |
8 | module.exports = {
9 | bail: true,
10 | // sourcemaps
11 | devtool: 'cheap-module-eval-source-map',
12 | target: 'electron-renderer',
13 | // input
14 | entry: [
15 | `webpack-dev-server/client?https://localhost:${PORT}/`,
16 | 'webpack/hot/only-dev-server',
17 | './app/renderer/index.js'
18 | ],
19 | // output
20 | output: {
21 | publicPath: PUBLIC_PATH,
22 | filename: 'renderer.dev.js'
23 | },
24 | // transformations
25 | module: {
26 | rules: [
27 | {
28 | test: /\.js$/,
29 | exclude: /(node_modules)/,
30 | loader: 'babel-loader',
31 | options: {
32 | // https://github.com/babel/babel-loader#options
33 | cacheDirectory: true,
34 | }
35 | },
36 | { test: /\.css$/, loader: 'style-loader!css-loader' },
37 | {
38 | test: /\.(gif|png|jpg|jpeg)$/,
39 | loader: 'url-loader?limit=8192&name=[path][name].[ext]?[hash]' // inline base64 URLs for <=8k, direct URLs for the rest
40 | },
41 | {
42 | test: /\.(ico|woff|woff2|ttf|eot)$/,
43 | loader: 'url-loader?limit=1&name=[path][name].[ext]'
44 | },
45 | {
46 | test: /\.svg$/,
47 | loader: 'svg-sprite-loader'
48 | }
49 | ]
50 | },
51 |
52 | // plugins
53 | plugins: [
54 | new webpack.HotModuleReplacementPlugin({
55 | multiStep: true
56 | }),
57 | new webpack.NoEmitOnErrorsPlugin(),
58 | new MonacoWebpackPlugin({
59 | languages: ['javascript', 'typescript']
60 | }),
61 | new webpack.DefinePlugin({
62 | 'process.env': {
63 | NODE_ENV: JSON.stringify('development')
64 | },
65 | 'process.browser': true
66 | })
67 | ],
68 | // manipulations
69 | resolve: {
70 | alias: {
71 | '@': path.resolve(__dirname, 'app', 'renderer')
72 | }
73 | },
74 | // dev server
75 | devServer: {
76 | https: true,
77 | port: PORT,
78 | publicPath: PUBLIC_PATH,
79 | compress: true,
80 | noInfo: true,
81 | stats: 'errors-only',
82 | inline: true,
83 | lazy: false,
84 | hot: true,
85 | headers: { 'Access-Control-Allow-Origin': '*' },
86 | contentBase: path.join(__dirname, 'dist'),
87 | watchOptions: {
88 | aggregateTimeout: 300,
89 | ignored: /node_modules/,
90 | poll: 100
91 | },
92 | historyApiFallback: {
93 | verbose: true,
94 | disableDotRule: false
95 | },
96 | before() {
97 | if (process.env.START_HOT) {
98 | console.log('Starting Main Process...')
99 | spawn('npm', ['run', 'start-main-dev'], {
100 | shell: true,
101 | env: process.env,
102 | stdio: 'inherit'
103 | })
104 | .on('close', code => process.exit(code))
105 | .on('error', spawnError => console.error(spawnError))
106 | }
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 |
4 |
5 | module.exports = {
6 | bail: true,
7 | // sourcemaps
8 | devtool: 'cheap-module-eval-source-map',
9 | target: 'electron-renderer',
10 | // input
11 | entry: path.join(__dirname, './app/renderer/index.js'),
12 | // output
13 | output: {
14 | path: path.join(__dirname, './app/dist'),
15 | filename: 'renderer.prod.js'
16 | },
17 | // transformations
18 | module: {
19 | rules: [
20 | {
21 | test: /\.js$/,
22 | exclude: /(node_modules)/,
23 | loader: 'babel-loader',
24 | options: {
25 | // https://github.com/babel/babel-loader#options
26 | cacheDirectory: true,
27 | }
28 | },
29 | { test: /\.css$/, loader: 'style-loader!css-loader' },
30 | {
31 | test: /\.(gif|png|jpg|jpeg)$/,
32 | loader: 'url-loader?limit=8192&name=[path][name].[ext]?[hash]' // inline base64 URLs for <=8k, direct URLs for the rest
33 | },
34 | {
35 | test: /\.(ico|woff|woff2|ttf|eot)$/,
36 | loader: 'url-loader?limit=1&name=[path][name].[ext]'
37 | },
38 | {
39 | test: /\.svg$/,
40 | loader: 'svg-sprite-loader'
41 | }
42 | ]
43 | },
44 |
45 | // plugins
46 | plugins: [
47 | new webpack.NoEmitOnErrorsPlugin(),
48 | new webpack.DefinePlugin({
49 | 'process.env': {
50 | NODE_ENV: JSON.stringify('production')
51 | }
52 | })
53 | ],
54 | // manipulations
55 | resolve: {
56 | alias: {
57 | '@': path.resolve(__dirname, 'app', 'renderer')
58 | }
59 | },
60 | node: {
61 | __dirname: true,
62 | __filename: true
63 | }
64 | }
65 |
--------------------------------------------------------------------------------