├── client ├── src │ ├── link │ │ ├── index.module.css │ │ └── index.js │ ├── trace │ │ ├── list.module.css │ │ ├── filters.module.css │ │ ├── details.js │ │ ├── TracingResponse.module.css │ │ ├── source.js │ │ ├── index.module.css │ │ ├── TracingRow.module.css │ │ ├── TracingRow.js │ │ ├── TracingReponse.js │ │ ├── filters.css │ │ ├── filters.js │ │ ├── index.js │ │ ├── filtersContext.js │ │ └── list.js │ ├── client.js │ ├── key │ │ ├── index.module.css │ │ └── index.js │ ├── label │ │ ├── index.js │ │ └── index.module.css │ ├── setupTests.js │ ├── header.js │ ├── graph │ │ ├── graph-list.module.css │ │ └── index.js │ ├── App.test.js │ ├── timeline │ │ ├── index.js │ │ ├── chart.js │ │ ├── rpm.js │ │ └── latencyDistribution.js │ ├── pill │ │ ├── index.js │ │ └── index.module.css │ ├── operation │ │ ├── list.module.css │ │ ├── utils.js │ │ ├── index.js │ │ └── list.js │ ├── stats │ │ ├── index.module.css │ │ └── index.js │ ├── nav │ │ ├── index.module.css │ │ └── index.js │ ├── index.js │ ├── logout.js │ ├── orderby │ │ └── index.js │ ├── utils.js │ ├── menu.js │ ├── auth │ │ └── index.js │ ├── user.js │ ├── App.js │ ├── serviceWorker.js │ └── index.css ├── .dockerignore ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── Dockerfile ├── Dockerfile.prod ├── .gitignore ├── package.json └── README.md ├── .dockerignore ├── server ├── .dockerignore ├── src │ ├── persistence │ │ ├── db.js │ │ ├── index.js │ │ ├── redis.js │ │ ├── graphs.js │ │ ├── migrator.js │ │ ├── users.js │ │ ├── postgres-state-storage.js │ │ ├── keys.js │ │ └── traces.js │ ├── config.js │ ├── graphql │ │ ├── index.js │ │ ├── utils.js │ │ ├── __tests__ │ │ │ ├── utils.js │ │ │ ├── user.test.js │ │ │ ├── operations.test.js │ │ │ ├── graphs.test.js │ │ │ └── keys.test.js │ │ ├── typedefs.js │ │ └── resolvers.js │ ├── __mocks__ │ │ └── emailjs.js │ ├── email.js │ ├── setupTests.js │ ├── ingress │ │ ├── queue.js │ │ ├── utils.js │ │ ├── utils.test.js │ │ ├── index.js │ │ ├── __data__ │ │ │ ├── traces.json │ │ │ └── traces-with-error.json │ │ ├── consumer.js │ │ ├── index.test.js │ │ └── consumer.test.js │ ├── migrations │ │ ├── 1550969025172-authentication.js │ │ ├── 1585141743246-graphs.js │ │ ├── 1585147972450-keys.js │ │ └── 1585217960415-traces.js │ ├── magicLink.js │ └── index.js ├── Dockerfile ├── bin │ ├── start.js │ ├── worker.js │ └── migrate.js └── package.json ├── docs └── images │ ├── graph-list.png │ ├── api-key-list.png │ ├── create-graph.png │ ├── create-api-key.png │ ├── global-filters.png │ ├── operation-list.png │ ├── requests-over-time.png │ ├── resolver-waterfall.png │ └── latency-distribution.png ├── ssl-less.yaml ├── nginx ├── user.conf.d │ ├── dev.conf │ ├── prod.conf │ └── ssl.conf └── nginx.conf ├── .editorconfig ├── .env ├── ssl.yaml ├── Makefile ├── .github └── workflows │ └── main.yml ├── prod.yaml ├── LICENSE ├── dev.yaml ├── .gitignore ├── docker-compose.yaml └── README.md /client/src/link/index.module.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | *Dockerfile* 3 | *docker-compose* 4 | node_modules 5 | -------------------------------------------------------------------------------- /client/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | *Dockerfile* 3 | *docker-compose* 4 | node_modules 5 | -------------------------------------------------------------------------------- /server/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | *Dockerfile* 3 | *docker-compose* 4 | node_modules 5 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmulligan/clementine/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmulligan/clementine/HEAD/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmulligan/clementine/HEAD/client/public/logo512.png -------------------------------------------------------------------------------- /docs/images/graph-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmulligan/clementine/HEAD/docs/images/graph-list.png -------------------------------------------------------------------------------- /docs/images/api-key-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmulligan/clementine/HEAD/docs/images/api-key-list.png -------------------------------------------------------------------------------- /docs/images/create-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmulligan/clementine/HEAD/docs/images/create-graph.png -------------------------------------------------------------------------------- /docs/images/create-api-key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmulligan/clementine/HEAD/docs/images/create-api-key.png -------------------------------------------------------------------------------- /docs/images/global-filters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmulligan/clementine/HEAD/docs/images/global-filters.png -------------------------------------------------------------------------------- /docs/images/operation-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmulligan/clementine/HEAD/docs/images/operation-list.png -------------------------------------------------------------------------------- /docs/images/requests-over-time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmulligan/clementine/HEAD/docs/images/requests-over-time.png -------------------------------------------------------------------------------- /docs/images/resolver-waterfall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmulligan/clementine/HEAD/docs/images/resolver-waterfall.png -------------------------------------------------------------------------------- /docs/images/latency-distribution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmulligan/clementine/HEAD/docs/images/latency-distribution.png -------------------------------------------------------------------------------- /server/src/persistence/db.js: -------------------------------------------------------------------------------- 1 | const { createPool } = require('slonik') 2 | 3 | module.exports = createPool(process.env.DATABASE_URL) 4 | -------------------------------------------------------------------------------- /server/src/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | SESSION_SECRET: process.env.SESSION_SECRET, 3 | CLIENT_URL: process.env.CLIENT_URL 4 | } 5 | -------------------------------------------------------------------------------- /server/src/graphql/index.js: -------------------------------------------------------------------------------- 1 | const typeDefs = require('./typedefs') 2 | const resolvers = require('./resolvers') 3 | module.exports = { typeDefs, resolvers } 4 | -------------------------------------------------------------------------------- /client/src/trace/list.module.css: -------------------------------------------------------------------------------- 1 | .row { 2 | display: flex; 3 | align-items: center; 4 | margin: 20px 0; 5 | } 6 | 7 | .row > div { 8 | padding: 0 20px; 9 | } 10 | -------------------------------------------------------------------------------- /server/src/__mocks__/emailjs.js: -------------------------------------------------------------------------------- 1 | const mockSend = (args, cb) => { 2 | cb() 3 | } 4 | 5 | module.exports = { 6 | server: { 7 | connect: () => ({ 8 | send: mockSend 9 | }) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/src/client.js: -------------------------------------------------------------------------------- 1 | import ApolloClient from 'apollo-boost' 2 | 3 | const client = new ApolloClient({ 4 | uri: '/api/graphql', 5 | name: 'webApp', 6 | version: '0.0.2' 7 | }) 8 | 9 | export default client 10 | -------------------------------------------------------------------------------- /client/src/key/index.module.css: -------------------------------------------------------------------------------- 1 | .row { 2 | cursor: pointer; 3 | display: flex; 4 | align-items: center; 5 | margin: 20px 0; 6 | padding: 20px 0; 7 | border-bottom: 1px solid var(--color-grey); 8 | justify-content: space-between; 9 | } 10 | -------------------------------------------------------------------------------- /client/src/label/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from './index.module.css' 3 | 4 | export default function Label({ className, type='green', ...props }) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.16.3-alpine3.11 2 | 3 | ARG NODE_ENV=production 4 | 5 | ENV NODE_ENV $NODE_ENV 6 | 7 | WORKDIR /app 8 | 9 | COPY ./package.json ./package-lock.json ./ 10 | 11 | RUN npm install --no-optional && npm cache clean --force 12 | 13 | COPY . . 14 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.16.3-alpine3.11 2 | 3 | ARG NODE_ENV=production 4 | 5 | ENV NODE_ENV $NODE_ENV 6 | 7 | WORKDIR /app 8 | 9 | COPY ./package.json ./package-lock.json ./ 10 | 11 | RUN npm install --no-optional && npm cache clean --force 12 | 13 | COPY . . 14 | -------------------------------------------------------------------------------- /ssl-less.yaml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml 2 | version: "3" 3 | services: 4 | nginx: 5 | volumes: 6 | - ./nginx/user.conf.d/prod.conf:/etc/nginx/conf.d/default.conf 7 | 8 | postgres: 9 | 10 | redis: 11 | 12 | worker: 13 | 14 | server: 15 | 16 | client: 17 | -------------------------------------------------------------------------------- /client/src/label/index.module.css: -------------------------------------------------------------------------------- 1 | .label { 2 | border-radius: 50%; 3 | min-height: 10px; 4 | min-width: 10px; 5 | display: inline-block; 6 | } 7 | 8 | .green { 9 | background: var(--color-cyan); 10 | } 11 | 12 | .orange { 13 | background: var(--color-orange);; 14 | } 15 | -------------------------------------------------------------------------------- /server/src/email.js: -------------------------------------------------------------------------------- 1 | const emailjs = require('emailjs') 2 | const [host, user, password] = process.env.SMTP.split(':') 3 | 4 | const email = emailjs.server.connect({ 5 | user, 6 | password, 7 | host, 8 | ssl: true, 9 | port: 465 10 | }) 11 | 12 | module.exports = email 13 | -------------------------------------------------------------------------------- /client/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /client/src/trace/filters.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | background: var(--color-black); 3 | width: 100%; 4 | min-height: 300px; 5 | display: flex; 6 | margin: 0; 7 | justify-content: center; 8 | align-items: center; 9 | } 10 | 11 | .wrapper > div { 12 | margin: 0 auto; 13 | } 14 | -------------------------------------------------------------------------------- /server/bin/start.js: -------------------------------------------------------------------------------- 1 | const { app } = require('../src/') 2 | const port = process.env.PORT 3 | const logger = require('loglevel') 4 | if (process.env.LOG_LEVEL != null) logger.setLevel(process.env.LOG_LEVEL) 5 | 6 | app.listen(port, () => { 7 | logger.info(`App started on port ${port}`) 8 | }) 9 | -------------------------------------------------------------------------------- /client/src/header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export function Header() { 4 | return ( 5 |
6 |

7 | Clementine{' '} 8 | 9 | 🍊 10 | 11 |

12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /server/src/setupTests.js: -------------------------------------------------------------------------------- 1 | const { runMigration } = require('./persistence/migrator') 2 | const { redis } = require('./persistence') 3 | 4 | beforeEach(async () => { 5 | await runMigration('up') 6 | await redis.flushdb() 7 | }) 8 | afterEach(async () => { 9 | await runMigration('down') 10 | }) 11 | -------------------------------------------------------------------------------- /client/Dockerfile.prod: -------------------------------------------------------------------------------- 1 | FROM node:12.16.3-alpine3.11 2 | 3 | ARG NODE_ENV=production 4 | 5 | ENV NODE_ENV $NODE_ENV 6 | 7 | WORKDIR /app 8 | 9 | COPY ./package.json ./package-lock.json ./ 10 | 11 | RUN npm install --no-optional && npm cache clean --force 12 | 13 | COPY . . 14 | 15 | RUN npm run build 16 | -------------------------------------------------------------------------------- /client/src/graph/graph-list.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | margin: 0 auto; 3 | } 4 | 5 | .row { 6 | cursor: pointer; 7 | padding: 20px 0; 8 | margin: 20px 0; 9 | border-bottom: 1px solid var(--color-grey); 10 | align-items: center; 11 | display: flex; 12 | justify-content: space-between; 13 | } 14 | -------------------------------------------------------------------------------- /client/src/link/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NavLink } from 'react-router-dom' 3 | 4 | function ActiveLink({ to, ...props }) { 5 | return ( 6 | 7 | {props.children} 8 | 9 | ) 10 | } 11 | 12 | export default ActiveLink 13 | -------------------------------------------------------------------------------- /client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /nginx/user.conf.d/dev.conf: -------------------------------------------------------------------------------- 1 | server { 2 | resolver 127.0.0.11 valid=30s; 3 | listen 80; 4 | 5 | location ^~/api/ { 6 | set $api http://server:3000; 7 | proxy_pass $api; 8 | proxy_redirect off; 9 | } 10 | 11 | location / { 12 | set $client http://client:3000; 13 | proxy_pass $client; 14 | proxy_redirect off; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /nginx/user.conf.d/prod.conf: -------------------------------------------------------------------------------- 1 | server { 2 | resolver 127.0.0.11 valid=30s; 3 | listen 80; 4 | 5 | location ^~/api/ { 6 | set $api http://server:3000; 7 | proxy_pass $api; 8 | proxy_redirect off; 9 | } 10 | 11 | location / { 12 | autoindex on; 13 | root /home/www-data/clementine; 14 | try_files $uri /index.html; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | 13 | # The JSON files contain newlines inconsistently 14 | [*.json] 15 | insert_final_newline = ignore 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | 20 | -------------------------------------------------------------------------------- /client/src/timeline/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Rpm from './rpm' 3 | import LatencyDistribution from './latencyDistribution' 4 | 5 | export function TimeLine({ graphId, operationId }) { 6 | return ( 7 |
8 | 9 | 10 |
11 | ) 12 | } 13 | 14 | export { LatencyDistribution, Rpm } 15 | -------------------------------------------------------------------------------- /client/src/trace/details.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | export default function Details({ children }) { 3 | if (!children.variablesJson) { 4 | return
5 | } 6 | return ( 7 |
8 |

Variables:

9 |
10 |       {JSON.stringify(children.variablesJson, null, 2)}
11 |     
12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /server/src/persistence/index.js: -------------------------------------------------------------------------------- 1 | const Trace = require('./traces') 2 | const Graph = require('./graphs') 3 | const User = require('./users') 4 | const Key = require('./keys') 5 | const db = require('./db') 6 | const redis = require('./redis') 7 | const { sql } = require('slonik') 8 | 9 | module.exports = { 10 | Trace, 11 | Graph, 12 | User, 13 | Key, 14 | db, 15 | sql, 16 | redis 17 | } 18 | -------------------------------------------------------------------------------- /client/src/pill/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from './index.module.css' 3 | 4 | function Pill({ children, isActive, ...props }) { 5 | const cls = [styles.pill] 6 | 7 | if (isActive) { 8 | cls.push(styles.active) 9 | } 10 | return ( 11 | 12 | {children} 13 | 14 | ) 15 | } 16 | 17 | export default Pill 18 | -------------------------------------------------------------------------------- /client/src/pill/index.module.css: -------------------------------------------------------------------------------- 1 | .pill { 2 | border-color: var(--color-text-secondary); 3 | color: var(--color-text-secondary); 4 | border: 1px solid; 5 | border-radius: var(--border-radius); 6 | margin-right: 20px; 7 | text-align: center; 8 | padding: 0.75rem 1.5rem; 9 | cursor: pointer; 10 | } 11 | 12 | .active { 13 | border-color: var(--color-text); 14 | color: var(--color-text); 15 | } 16 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /server/src/ingress/queue.js: -------------------------------------------------------------------------------- 1 | const Queue = require('bull') 2 | const ingestQueue = new Queue('trace:ingest', { redis: { host: 'redis' } }) 3 | const thresholdQueue = new Queue('trace:threshold', { 4 | redis: { host: 'redis' } 5 | }) 6 | 7 | const forwardQueue = new Queue('trace:forward', { 8 | redis: { host: 'redis' } 9 | }) 10 | 11 | module.exports = { 12 | ingestQueue, 13 | thresholdQueue, 14 | forwardQueue 15 | } 16 | -------------------------------------------------------------------------------- /client/src/operation/list.module.css: -------------------------------------------------------------------------------- 1 | .row { 2 | cursor: pointer; 3 | display: flex; 4 | align-items: center; 5 | margin: 20px 0; 6 | padding: 20px 0; 7 | border-bottom: 1px solid var(--color-grey); 8 | justify-content: space-between; 9 | } 10 | 11 | .row > div, .rowRight > div { 12 | padding: 0 20px; 13 | } 14 | 15 | .rowRight { 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | } 20 | -------------------------------------------------------------------------------- /client/src/stats/index.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | justify-content: center; 4 | } 5 | 6 | .stat { 7 | display: flex; 8 | flex-direction: column; 9 | padding: 0 10px; 10 | text-align: center; 11 | justify-items: center; 12 | } 13 | 14 | .statNumber { 15 | font-size: 1.3rem; 16 | font-weight: 800; 17 | color: var(--color-black); 18 | } 19 | 20 | .statTitle { 21 | color: var(--color-text-secondary); 22 | } 23 | -------------------------------------------------------------------------------- /server/src/persistence/redis.js: -------------------------------------------------------------------------------- 1 | const promisify = require('util').promisify 2 | const redis = require('redis').createClient({ host: 'redis' }) 3 | 4 | const get = promisify(redis.get).bind(redis) 5 | const set = promisify(redis.set).bind(redis) 6 | const del = promisify(redis.del).bind(redis) 7 | const flushdb = promisify(redis.flushdb).bind(redis) 8 | 9 | module.exports = { 10 | get, 11 | set, 12 | del, 13 | flushdb, 14 | client: redis 15 | } 16 | -------------------------------------------------------------------------------- /client/src/trace/TracingResponse.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | padding-top: 6px; 3 | padding-left: 100px; 4 | padding-right: 25px; 5 | overflow: auto; 6 | position: relative; 7 | height: 100%; 8 | } 9 | 10 | .rows { 11 | padding-left: 100px; 12 | padding-bottom: 100px; 13 | padding-top: 16px; 14 | position: absolute; 15 | overflow: auto; 16 | top: 0; 17 | left: 0; 18 | width: calc(100% + 150px); 19 | height: calc(100% + 166px); 20 | } 21 | -------------------------------------------------------------------------------- /client/src/trace/source.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { print } from 'graphql/language/printer' 3 | import { gql } from 'apollo-boost' 4 | 5 | export default function Source({ children }) { 6 | return ( 7 |
8 |
 9 |         
10 |           {print(
11 |             gql`
12 |               ${children}
13 |             `
14 |           )}
15 |         
16 |       
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /client/src/nav/index.module.css: -------------------------------------------------------------------------------- 1 | .decorator { 2 | border-bottom: 1px solid var(--color-grey); 3 | margin: 50px 0; 4 | padding: 0; 5 | width: 100%; 6 | } 7 | 8 | .container { 9 | padding: 0; 10 | } 11 | 12 | .wrapper { 13 | display: flex; 14 | align-items: center; 15 | } 16 | 17 | .item { 18 | text-decoration: none; 19 | } 20 | 21 | .link { 22 | flex-grow: 1; 23 | text-decoration: none !important; 24 | text-align: center; 25 | padding: 25px 0; 26 | } 27 | -------------------------------------------------------------------------------- /server/src/graphql/utils.js: -------------------------------------------------------------------------------- 1 | const decodeCursor = cursor => { 2 | // returns [ , field, isAsc ] 3 | if (cursor) { 4 | return Buffer.from(cursor, 'base64') 5 | .toString('utf-8') 6 | .split(':') 7 | } 8 | 9 | return [] 10 | } 11 | 12 | const encodeCursor = (o, field, asc) => { 13 | return Buffer.from(`${o[field]}:${field}:${asc}`).toString('base64') 14 | } 15 | 16 | module.exports = { 17 | Cursor: { 18 | encode: encodeCursor, 19 | decode: decodeCursor 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /server/bin/worker.js: -------------------------------------------------------------------------------- 1 | const logger = require('loglevel') 2 | const { 3 | thresholdQueue, 4 | ingestQueue, 5 | forwardQueue 6 | } = require('../src/ingress/queue') 7 | const { ingest, cull, forward } = require('../src/ingress/consumer') 8 | const fetch = require('node-fetch') 9 | if (process.env.LOG_LEVEL != null) logger.setLevel(process.env.LOG_LEVEL) 10 | 11 | logger.info('Running workers') 12 | 13 | ingestQueue.process(ingest(thresholdQueue)) 14 | thresholdQueue.process(cull) 15 | forwardQueue.process(forward(fetch)) 16 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Populate email smtp settings needed for auth 2 | # required 3 | # You can use sendgrid's free tier if you dont have one. 4 | SMTP=:: 5 | SMTP_EMAIL_FROM= 6 | # publically accessible instance host used for email auth callback + letsEncrypt 7 | DOMAIN=localhost 8 | 9 | # Optional but encouraged to change 10 | POSTGRES_USER=user 11 | POSTGRES_PASSWORD=pass 12 | POSTGRES_DB=db 13 | SESSION_SECRET=super-secret 14 | 15 | # Optial but encouraged for letsEncrypt 16 | LETSENCRYPT_EMAIL= 17 | -------------------------------------------------------------------------------- /client/src/trace/index.module.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | text-align: center; 3 | margin-bottom: 40px; 4 | } 5 | 6 | .wrapper > div { 7 | margin-bottom: 10px; 8 | } 9 | 10 | .stat { 11 | display: flex; 12 | flex-direction: column; 13 | padding: 0 10px; 14 | text-align: center; 15 | justify-items: center; 16 | } 17 | 18 | .statNumber { 19 | font-size: 1.3rem; 20 | font-weight: 800; 21 | } 22 | 23 | .statTitle { 24 | color: var(--color-text-secondary); 25 | } 26 | 27 | .subTitle { 28 | color: var(--color-text-secondary); 29 | } 30 | -------------------------------------------------------------------------------- /nginx/user.conf.d/ssl.conf: -------------------------------------------------------------------------------- 1 | server { 2 | resolver 127.0.0.11 valid=30s; 3 | listen 443 ssl; 4 | server_name ${DOMAIN}; 5 | ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem; 6 | ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem; 7 | 8 | location ^~/api/ { 9 | set $api http://server:3000; 10 | proxy_pass $api; 11 | proxy_redirect off; 12 | } 13 | 14 | location / { 15 | autoindex on; 16 | root /home/www-data/clementine; 17 | try_files $uri /index.html; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /server/bin/migrate.js: -------------------------------------------------------------------------------- 1 | const { runMigration } = require('../src/persistence/migrator') 2 | const logger = require('loglevel') 3 | if (process.env.LOG_LEVEL != null) logger.setLevel(process.env.LOG_LEVEL) 4 | 5 | const [command] = process.argv.slice(2) 6 | 7 | runMigration(command) 8 | .then(() => { 9 | logger.info(`migrations "${command}" successfully ran`) 10 | // eslint-disable-next-line unicorn/no-process-exit 11 | process.exit(0) 12 | }) 13 | .catch(error => { 14 | console.error(error.stack) 15 | // eslint-disable-next-line unicorn/no-process-exit 16 | process.exit(1) 17 | }) 18 | -------------------------------------------------------------------------------- /client/src/trace/TracingRow.module.css: -------------------------------------------------------------------------------- 1 | .row { 2 | position: relative; 3 | font-size: 16px; 4 | display: table; 5 | padding-right: 25px; 6 | } 7 | 8 | .bar { 9 | display: inline-block; 10 | position: relative; 11 | margin: 0 10px; 12 | height: 1.5px; 13 | bottom: 4px; 14 | background: var(--color-black); 15 | } 16 | 17 | .duration { 18 | font-size: 16px; 19 | } 20 | 21 | .wrapper { 22 | position: absolute; 23 | left: 0; 24 | transform: translateX(-100%); 25 | display: inline-flex; 26 | align-items: center; 27 | text-align: right; 28 | } 29 | 30 | .name { 31 | font-size: 16px; 32 | } 33 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/src/operation/utils.js: -------------------------------------------------------------------------------- 1 | import { getOperationName } from 'apollo-utilities' 2 | 3 | export function getOperationTypes(doc) { 4 | let operationTypes = [] 5 | 6 | const definitions = doc.definitions.filter( 7 | definition => definition.kind === 'OperationDefinition' 8 | ) 9 | 10 | const isQuery = definitions.some(def => def.operation === 'query') 11 | const isMutation = definitions.some(def => def.operation === 'mutation') 12 | 13 | if (isQuery) { 14 | operationTypes.push('query') 15 | } 16 | 17 | if (isMutation) { 18 | operationTypes.push('mutation') 19 | } 20 | 21 | return operationTypes 22 | } 23 | 24 | export { 25 | getOperationName 26 | } 27 | -------------------------------------------------------------------------------- /server/src/migrations/1550969025172-authentication.js: -------------------------------------------------------------------------------- 1 | const { db, sql } = require('../persistence') 2 | 3 | module.exports.up = async function(next) { 4 | await db.query(sql` 5 | CREATE TABLE IF NOT EXISTS users ( 6 | id uuid PRIMARY KEY, 7 | email text UNIQUE, 8 | "createdAt" timestamp with time zone default (now() at time zone 'utc') NOT NULL, 9 | "isVerified" boolean default FALSE 10 | ); 11 | CREATE INDEX IF NOT EXISTS "usersEmail" on users (email); 12 | `) 13 | 14 | next() 15 | } 16 | 17 | module.exports.down = async function(next) { 18 | await db.query(sql` 19 | DROP TABLE users; 20 | DROP INDEX IF EXISTS "usersEmail"; 21 | `) 22 | next() 23 | } 24 | -------------------------------------------------------------------------------- /ssl.yaml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml 2 | version: "3" 3 | services: 4 | nginx: 5 | ports: 6 | - 443:443 7 | image: staticfloat/nginx-certbot 8 | environment: 9 | CERTBOT_EMAIL: ${LETSENCRYPT_EMAIL} 10 | DOMAIN: ${DOMAIN} 11 | ENVSUBST_VARS: DOMAIN 12 | IS_STAGING: ${LETSENCRYPT_IS_STAGING:-"1"} 13 | volumes: 14 | # nginx-certbot moves nginx.conf to ./conf.d/ and overwrites our thang 15 | - ./nginx/user.conf.d/ssl.conf:/etc/nginx/user.conf.d/default.conf 16 | - letsencrypt:/etc/letsencrypt 17 | 18 | postgres: 19 | 20 | redis: 21 | 22 | worker: 23 | 24 | server: 25 | environment: 26 | IS_SSL: "1" 27 | 28 | client: 29 | 30 | volumes: 31 | letsencrypt: 32 | -------------------------------------------------------------------------------- /client/src/logout.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { useMutation } from '@apollo/react-hooks' 3 | import { gql } from 'apollo-boost' 4 | import client from './client' 5 | import UserContext from './user' 6 | import { Link } from 'react-router-dom' 7 | 8 | const LOGOUT = gql` 9 | mutation logout { 10 | userLogout 11 | } 12 | ` 13 | 14 | export default function Logout() { 15 | const { setUser } = useContext(UserContext) 16 | const [logout] = useMutation(LOGOUT) 17 | 18 | return ( 19 | { 22 | await logout() 23 | client.resetStore() 24 | setUser(null) 25 | }} 26 | > 27 | Logout 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /server/src/migrations/1585141743246-graphs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { db, sql } = require('../persistence') 4 | 5 | module.exports.up = async function(next) { 6 | await db.query(sql` 7 | CREATE TABLE IF NOT EXISTS graphs ( 8 | id uuid PRIMARY KEY, 9 | "userId" uuid REFERENCES users (id) ON DELETE CASCADE, 10 | name text, 11 | "createdAt" timestamp with time zone default (now() at time zone 'utc') NOT NULL 12 | ); 13 | CREATE INDEX IF NOT EXISTS "graphUser" on graphs ("userId"); 14 | `) 15 | 16 | next() 17 | } 18 | 19 | module.exports.down = async function(next) { 20 | await db.query(sql` 21 | DROP TABLE graphs CASCADE; 22 | DROP INDEX IF EXISTS "graphUser"; 23 | `) 24 | 25 | next() 26 | } 27 | -------------------------------------------------------------------------------- /server/src/migrations/1585147972450-keys.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { db, sql } = require('../persistence') 4 | 5 | module.exports.up = async function(next) { 6 | await db.query(sql` 7 | CREATE TABLE IF NOT EXISTS keys ( 8 | id uuid PRIMARY KEY, 9 | "createdAt" timestamp with time zone default (now() at time zone 'utc') NOT NULL, 10 | "graphId" uuid REFERENCES graphs (id) ON DELETE CASCADE, 11 | hash text UNIQUE, 12 | prefix text 13 | ); 14 | CREATE INDEX IF NOT EXISTS "keyGraph" on keys ("graphId"); 15 | `) 16 | 17 | next() 18 | } 19 | 20 | module.exports.down = async function(next) { 21 | await db.query(sql` 22 | DROP TABLE keys; 23 | DROP INDEX IF EXISTS "keyGraph"; 24 | `) 25 | next() 26 | } 27 | -------------------------------------------------------------------------------- /client/src/nav/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from './index.module.css' 3 | import Link from '../link' 4 | 5 | export default ({ items }) => { 6 | const isActive = false 7 | return ( 8 |
9 |
10 |
11 | {items.map(item => { 12 | return ( 13 | 20 |
{item.title}
21 | 22 | ) 23 | })} 24 |
25 |
26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /server/src/persistence/graphs.js: -------------------------------------------------------------------------------- 1 | const { sql } = require('slonik') 2 | const uuid = require('uuid/v4') 3 | const db = require('./db') 4 | 5 | module.exports = { 6 | async create(name, userId) { 7 | const { rows } = await db.query(sql` 8 | INSERT INTO graphs (id, name, "userId") 9 | VALUES (${uuid()}, ${name}, ${userId}) 10 | RETURNING id, name, "userId"; 11 | `) 12 | 13 | const [graph] = rows 14 | return graph 15 | }, 16 | async findById(id) { 17 | const { rows } = await db.query(sql` 18 | SELECT * FROM graphs WHERE id=${id} LIMIT 1; 19 | `) 20 | 21 | const [graph] = rows 22 | return graph 23 | }, 24 | async findAll({ userId }) { 25 | const { rows } = await db.query(sql` 26 | SELECT * FROM graphs WHERE "userId"=${userId}; 27 | `) 28 | return rows 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | docker-compose run server npm test 3 | 4 | ci: 5 | docker-compose run server npm ci 6 | 7 | build: 8 | docker-compose -f docker-compose.yaml -f dev.yaml build 9 | 10 | dev: build 11 | docker-compose -f docker-compose.yaml -f dev.yaml up --remove-orphans 12 | 13 | migrate: 14 | docker-compose run server npm run migrate up 15 | 16 | migrate-down: 17 | docker-compose run server npm run migrate down 18 | 19 | db-rm: 20 | docker-compose kill postgres && docker-compose rm postgres 21 | 22 | psql: 23 | psql postgres://user:pass@localhost:5432/db 24 | 25 | build-prod: 26 | docker-compose -f docker-compose.yaml -f prod.yaml build 27 | 28 | start: build-prod 29 | docker-compose -f docker-compose.yaml -f prod.yaml -f ssl-less.yaml up -d 30 | 31 | start_with_ssl: build-prod 32 | docker-compose -f docker-compose.yaml -f prod.yaml -f ssl.yaml up -d 33 | -------------------------------------------------------------------------------- /server/src/persistence/migrator.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const migrate = require('migrate') 3 | 4 | const stateStore = require('./postgres-state-storage') 5 | const migrationsDirectory = path.resolve(__dirname, '../migrations') 6 | 7 | const [command] = process.argv.slice(2) 8 | 9 | const runMigration = command => { 10 | return new Promise((resolve, reject) => { 11 | migrate.load( 12 | { 13 | stateStore, 14 | migrationsDirectory 15 | }, 16 | (err, set) => { 17 | if (err) { 18 | reject(err) 19 | } 20 | 21 | if (typeof set[command] !== 'function') { 22 | reject(new Error('Command is not a function')) 23 | } 24 | 25 | set[command](err => { 26 | if (err) reject(err) 27 | resolve() 28 | }) 29 | } 30 | ) 31 | }) 32 | } 33 | 34 | module.exports = { runMigration } 35 | -------------------------------------------------------------------------------- /client/src/orderby/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import Pill from '../pill' 3 | 4 | export default function OrderBy({ 5 | fields, 6 | orderAsc, 7 | setOrderAsc, 8 | setOrderField, 9 | orderField 10 | }) { 11 | const symbol = orderAsc 12 | ? String.fromCharCode(9652) 13 | : String.fromCharCode(9662) 14 | 15 | return ( 16 | 17 | {fields.map(({ field, label }) => { 18 | return ( 19 | { 23 | if (orderField === field) { 24 | setOrderAsc(prev => !prev) 25 | } 26 | setOrderField(field) 27 | }} 28 | > 29 | {label} {orderField === field && symbol} 30 | 31 | ) 32 | })} 33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /client/src/utils.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export function ErrorBanner({ error }) { 4 | return
Error: {error.message}...
5 | } 6 | 7 | export function Loading() { 8 | return
Loading...
9 | } 10 | 11 | export function NotFound() { 12 | return
Not Found
13 | } 14 | 15 | export function printDuration(nanoSeconds) { 16 | const microSeconds = Math.round(nanoSeconds / 1000) 17 | if (microSeconds > 1000) { 18 | const ms = Math.round(microSeconds / 1000) 19 | return `${ms} ms` 20 | } 21 | 22 | return `${microSeconds} µs` 23 | } 24 | 25 | export function printDate(d) { 26 | return ( 27 | d.getFullYear() + 28 | '-' + 29 | (d.getMonth() + 1) + 30 | '-' + 31 | d.getDate() + 32 | ' ' + 33 | d.getHours() + 34 | ':' + 35 | d.getMinutes() + 36 | ':' + 37 | d.getSeconds() 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /client/src/stats/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from './index.module.css' 3 | import { printDuration } from '../utils' 4 | 5 | export default function keyMetrics({ 6 | count, 7 | errorCount, 8 | errorPercent, 9 | duration 10 | }) { 11 | return ( 12 |
13 |
14 |
{count}
15 |
Requests
16 |
17 |
18 |
{errorCount}
19 |
Errors
20 |
21 |
22 |
{errorPercent}%
23 |
Error Rate
24 |
25 |
26 |
{printDuration(duration) || 0}
27 |
95 percentile
28 |
29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | # This workflow contains a single job called "build" 16 | build: 17 | # The type of runner that the job will run on 18 | runs-on: ubuntu-latest 19 | 20 | # Steps represent a sequence of tasks that will be executed as part of the job 21 | steps: 22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 23 | - uses: actions/checkout@v2 24 | 25 | # Runs a single command using the runners shell 26 | - name: build 27 | run: make build 28 | 29 | - name: migrate 30 | run: make migrate 31 | 32 | # Runs a set of commands using the runners shell 33 | - name: ci 34 | run: make ci 35 | -------------------------------------------------------------------------------- /server/src/ingress/utils.js: -------------------------------------------------------------------------------- 1 | const uuidByString = require('uuid-by-string') 2 | 3 | const extractErrors = (node, acc = []) => { 4 | if (node.error.length > 0) { 5 | acc.push(...node.error) 6 | } 7 | 8 | if (node.child) { 9 | node.child.map(n => { 10 | extractErrors(n, acc) 11 | }) 12 | } 13 | 14 | return acc 15 | } 16 | 17 | function parseTS(message) { 18 | return new Date(message.seconds * 1000 + message.nanos / 1000000) 19 | } 20 | 21 | function prepareTraces(report) { 22 | return Object.entries(report.tracesPerQuery).reduce((acc, [key, v]) => { 23 | return [ 24 | ...acc, 25 | ...v.trace.map(trace => { 26 | return { 27 | schemaTag: report.header.schemaTag, 28 | key, 29 | operationId: uuidByString(key), 30 | ...trace, 31 | startTime: parseTS(trace.startTime), 32 | endTime: parseTS(trace.endTime), 33 | hasErrors: extractErrors(trace.root).length > 0 34 | } 35 | }) 36 | ] 37 | }, []) 38 | } 39 | 40 | module.exports = { 41 | extractErrors, 42 | prepareTraces 43 | } 44 | -------------------------------------------------------------------------------- /server/src/persistence/users.js: -------------------------------------------------------------------------------- 1 | const { sql } = require('slonik') 2 | const uuid = require('uuid/v4') 3 | const db = require('./db') 4 | 5 | module.exports = { 6 | async create(email, password) { 7 | try { 8 | const { rows } = await db.query(sql` 9 | INSERT INTO users (id, email) 10 | VALUES (${uuid()}, ${email}) 11 | RETURNING id, email, "isVerified"; 12 | `) 13 | 14 | const [user] = rows 15 | return user 16 | } catch (error) { 17 | throw error 18 | } 19 | }, 20 | async find(email) { 21 | return await db.maybeOne(sql` 22 | SELECT * FROM users WHERE email=${email}; 23 | `) 24 | }, 25 | async findAll() { 26 | return await db.query(sql` 27 | SELECT * FROM users; 28 | `) 29 | }, 30 | async markVerified(id) { 31 | return await db.query(sql` 32 | UPDATE users SET "isVerified" = true WHERE id=${id}; 33 | `) 34 | }, 35 | async findById(id) { 36 | if (!id) { 37 | return null 38 | } 39 | 40 | return db.maybeOne(sql` 41 | SELECT * FROM users WHERE id=${id}; 42 | `) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /prod.yaml: -------------------------------------------------------------------------------- 1 | # docker-compose.yml 2 | version: "3" 3 | services: 4 | nginx: 5 | volumes: 6 | - static:/home/www-data/clementine:ro 7 | 8 | postgres: 9 | 10 | redis: 11 | 12 | worker: 13 | build: 14 | context: ./server 15 | args: 16 | NODE_ENV: production 17 | user: node 18 | environment: 19 | NODE_ENV: production 20 | LOG_LEVEL: info 21 | command: npm run start:worker 22 | restart: unless-stopped 23 | 24 | server: 25 | build: 26 | context: ./server 27 | args: 28 | NODE_ENV: production 29 | user: node 30 | environment: 31 | NODE_ENV: production 32 | LOG_LEVEL: info 33 | command: npm run start 34 | restart: unless-stopped 35 | 36 | client: 37 | build: 38 | context: ./client 39 | dockerfile: Dockerfile.prod 40 | args: 41 | NODE_ENV: production 42 | user: node 43 | volumes: 44 | - static:/app/build 45 | environment: 46 | NODE_ENV: production 47 | LOG_LEVEL: info 48 | command: echo "mounting static!" 49 | 50 | volumes: 51 | static: 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2019 Hugo Di Francesco 3 | 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /client/src/trace/TracingRow.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import styles from './TracingRow.module.css' 3 | import { printDuration } from '../utils' 4 | 5 | export default function TracingRow({ 6 | path, 7 | startOffset, 8 | duration, 9 | totalDuration, 10 | screenWidth 11 | }) { 12 | const offsetLeft = (startOffset / totalDuration) * screenWidth * 0.9 13 | const barWidth = (duration / totalDuration) * screenWidth * 0.9 14 | 15 | return ( 16 |
20 | 21 | 22 | {path.slice(-2).map((p, index) => ( 23 | 29 | {`${index > 0 ? '.' : ''}${p}`} 30 | 31 | ))} 32 | 33 | 34 | 35 | {printDuration(duration)} 36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@apollo/react-hooks": "^3.1.3", 7 | "@data-ui/theme": "0.0.84", 8 | "@data-ui/xy-chart": "0.0.84", 9 | "@testing-library/jest-dom": "^4.2.4", 10 | "@testing-library/react": "^9.3.2", 11 | "@testing-library/user-event": "^7.1.2", 12 | "apollo-boost": "^0.4.7", 13 | "graphql": "^14.6.0", 14 | "loglevel": "^1.6.7", 15 | "react": "^16.13.1", 16 | "react-dom": "^16.13.1", 17 | "react-router-dom": "^5.1.2", 18 | "react-scripts": "3.4.1", 19 | "react-visual-filter": "^1.0.9", 20 | "serve": "^11.3.0" 21 | }, 22 | "scripts": { 23 | "start": "serve build", 24 | "dev": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": "react-app" 31 | }, 32 | "proxy": "http://localhost:3000", 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /dev.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | nginx: 4 | volumes: 5 | - ./nginx/user.conf.d/dev.conf:/etc/nginx/conf.d/default.conf 6 | 7 | 8 | worker: 9 | build: 10 | context: ./server 11 | args: 12 | NODE_ENV: development 13 | environment: 14 | NODE_ENV: development 15 | LOG_LEVEL: debug 16 | command: npm run dev:worker 17 | volumes: 18 | - ./server/src:/app/src 19 | - ./server/bin:/app/bin 20 | networks: 21 | - clementine 22 | 23 | server: 24 | build: 25 | context: ./server 26 | args: 27 | NODE_ENV: development 28 | environment: 29 | LOGLEVEL: debug 30 | NODE_ENV: development 31 | command: npm run dev 32 | volumes: 33 | - ./server/src:/app/src 34 | - ./server/bin:/app/bin 35 | networks: 36 | - clementine 37 | 38 | client: 39 | build: 40 | context: ./client 41 | args: 42 | NODE_ENV: development 43 | environment: 44 | LOG_LEVEL: debug 45 | NODE_ENV: development 46 | stdin_open: true 47 | command: npm run dev 48 | volumes: 49 | - ./client/src:/app/src 50 | networks: 51 | - clementine 52 | 53 | postgres: 54 | ports: 55 | - "5432:5432" 56 | 57 | redis: 58 | ports: 59 | - "6379:6379" 60 | -------------------------------------------------------------------------------- /server/src/magicLink.js: -------------------------------------------------------------------------------- 1 | const uuid = require('uuid/v4') 2 | const promisify = require('util').promisify 3 | const { redis } = require('./persistence') 4 | const email = require('./email') 5 | const sendEmail = promisify(email.send).bind(email) 6 | const crypto = require('crypto') 7 | const prefix = 'magicLink' 8 | 9 | function hash(str) { 10 | return crypto 11 | .createHash('sha256') 12 | .update(str) 13 | .digest('hex') 14 | } 15 | 16 | const EXPIRE = 3600 // 1 hr 17 | 18 | const domain = process.env.DOMAIN 19 | const protocol = process.env.IS_SSL === '1' ? 'https' : 'http' 20 | 21 | async function generate(data) { 22 | const token = uuid() 23 | await redis.set( 24 | `${prefix}:${hash(token)}`, 25 | JSON.stringify(data), 26 | 'EX', 27 | EXPIRE 28 | ) 29 | return [token, `${protocol}://${domain}/api/verify?token=${token}`] 30 | } 31 | 32 | async function verify(token) { 33 | const data = await redis.get(`${prefix}:${hash(token)}`) 34 | return JSON.parse(data) 35 | } 36 | 37 | async function send(user) { 38 | const [token, link] = await generate(user) 39 | 40 | return sendEmail({ 41 | text: `Follow this ${link} to login.`, 42 | subject: 'Clementine Signin', 43 | from: process.env.SMTP_EMAIL_FROM, 44 | to: user.email 45 | }) 46 | } 47 | 48 | module.exports = { 49 | verify, 50 | generate, 51 | send 52 | } 53 | -------------------------------------------------------------------------------- /client/src/menu.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/anchor-is-valid */ 2 | import React, { useContext } from 'react' 3 | import { Link, useLocation, useRouteMatch } from 'react-router-dom' 4 | import Logout from './logout' 5 | import UserContext from './user' 6 | import { FiltersContext } from './trace' 7 | import Label from './label' 8 | 9 | function Menu() { 10 | const { user } = useContext(UserContext) 11 | const { rawFilters: filters } = useContext(FiltersContext) 12 | const location = useLocation() 13 | const match = useRouteMatch('/graph/:graphId') 14 | const search = new URLSearchParams(location.search) 15 | 16 | let path 17 | 18 | // toggle fitlers query 19 | if (!search.get('filters')) { 20 | search.set('filters', '1') 21 | path = location.pathname + '?' + search.toString() 22 | } else { 23 | search.delete('filters') 24 | path = location.pathname + '?' + search.toString() 25 | } 26 | 27 | if (user) { 28 | const label = filters.length > 0 ?