diff --git a/Dockerfile b/Dockerfile index 924f804..0ebdc9b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,7 @@ WORKDIR /app/server ENV MELI_URL_INTERNAL=http://localhost:3001 ENV MELI_UI_DIR=/app/ui +ENV MELI_POSTHOG_ENABLED="true" # Caddy defaults, copied from official Dockerfile # https://github.com/caddyserver/caddy-docker/blob/2093c4a571bfe356447008d229195eb7063232b2/2.3/alpine/Dockerfile diff --git a/package.json b/package.json index 10e875d..67acdfd 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "build:ui": "npm run build --prefix ui", "test": "npm run test:ui", "test:ui": "npm run test --prefix server", - "lint": "npm run lint --prefix server" + "lint": "npm run lint --prefix server", + "lint:fix": "npm run lint:fix --prefix server" }, "keywords": [ "meli", diff --git a/server/package-lock.json b/server/package-lock.json index 3ba7e1f..bb69362 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -2534,6 +2534,14 @@ "follow-redirects": "^1.10.0" } }, + "axios-retry": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-3.1.9.tgz", + "integrity": "sha512-NFCoNIHq8lYkJa6ku4m+V1837TP6lCa7n79Iuf8/AqATAHYB0ISaAS1eyIenDOfHOLtym34W65Sjke2xjg2fsA==", + "requires": { + "is-retry-allowed": "^1.1.0" + } + }, "babel-jest": { "version": "26.6.3", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", @@ -3209,6 +3217,11 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, "check-types": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz", @@ -3474,6 +3487,11 @@ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, + "component-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-type/-/component-type-1.2.1.tgz", + "integrity": "sha1-ikeQFwAjjk/DIml3EjAibyS0Fak=" + }, "compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -3872,6 +3890,11 @@ } } }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -7081,8 +7104,7 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "is-callable": { "version": "1.2.3", @@ -7269,6 +7291,11 @@ "has-symbols": "^1.0.1" } }, + "is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==" + }, "is-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", @@ -7924,6 +7951,11 @@ "@sideway/pinpoint": "^2.0.0" } }, + "join-component": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/join-component/-/join-component-1.1.0.tgz", + "integrity": "sha1-uEF7dQZho5K+4sJTfGiyqdSXfNU=" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8382,6 +8414,16 @@ "supports-hyperlinks": "^2.1.0" } }, + "md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -13563,6 +13605,28 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, + "posthog-node": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-1.0.7.tgz", + "integrity": "sha512-KTCwyU+PU1eAQtjy5ZSJ47mrxv2d/mMkxo+vvV5c+YqfE4mBAY1UPEPMv1nElb5Vq0UnxvyQXaUnOn8d8Xr6Eg==", + "requires": { + "axios": "^0.21.1", + "axios-retry": "^3.1.9", + "component-type": "^1.2.1", + "join-component": "^1.1.0", + "md5": "^2.3.0", + "ms": "^2.1.3", + "remove-trailing-slash": "^0.1.1", + "uuid": "^8.3.2" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -14057,6 +14121,11 @@ "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", "dev": true }, + "remove-trailing-slash": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz", + "integrity": "sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA==" + }, "repeat-element": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", diff --git a/server/package.json b/server/package.json index 015f6b9..1cda939 100644 --- a/server/package.json +++ b/server/package.json @@ -96,6 +96,7 @@ "passport-custom": "^1.1.1", "passport-google-oauth20": "^2.0.0", "passport-oauth2": "^1.5.0", + "posthog-node": "^1.0.7", "prom-client": "^12.0.0", "qs": "^6.9.4", "queue": "^6.0.1", diff --git a/server/src/auth/handlers/get-auth-methods.spec.ts b/server/src/auth/handlers/get-auth-methods.spec.ts index bb8922c..8a73b9f 100644 --- a/server/src/auth/handlers/get-auth-methods.spec.ts +++ b/server/src/auth/handlers/get-auth-methods.spec.ts @@ -1,5 +1,5 @@ import request from 'supertest'; -import { MeliServer } from '../../server'; +import { MeliServer } from '../../createServer'; import { spyOnVerifyToken } from '../../../tests/utils/spyon-verifytoken'; import { testServer } from '../../../tests/test-server'; import { authMethods } from '../passport/auth-methods'; diff --git a/server/src/auth/handlers/sign-out.spec.ts b/server/src/auth/handlers/sign-out.spec.ts index f6100e9..ceab400 100644 --- a/server/src/auth/handlers/sign-out.spec.ts +++ b/server/src/auth/handlers/sign-out.spec.ts @@ -1,5 +1,5 @@ import request from 'supertest'; -import { MeliServer } from '../../server'; +import { MeliServer } from '../../createServer'; import { spyOnVerifyToken } from '../../../tests/utils/spyon-verifytoken'; import { testServer } from '../../../tests/test-server'; diff --git a/server/src/server.ts b/server/src/createServer.ts similarity index 92% rename from server/src/server.ts rename to server/src/createServer.ts index f4ae51d..6c1eb7f 100644 --- a/server/src/server.ts +++ b/server/src/createServer.ts @@ -7,7 +7,7 @@ import cors from 'cors'; import express, { Express } from 'express'; import rateLimit from 'express-rate-limit'; import helmet from 'helmet'; -import { createServer, Server } from 'http'; +import { createServer as createHttpServer, Server } from 'http'; import morgan from 'morgan'; import passport from 'passport'; import { Logger } from './commons/logger/logger'; @@ -24,6 +24,7 @@ import { authorizeApiReq } from './auth/handlers/authorize-api-req'; import './auth/passport'; import { createIoServer } from './socket/create-io-server'; import { initSocketRooms } from './socket/socket-rooms'; +import { initPosthog } from './posthog/init-posthog'; const logger = new Logger('meli.api:server'); @@ -50,8 +51,9 @@ export interface MeliServer { stop: () => void; } -export async function server(): Promise { +export async function createServer(): Promise { await AppDb.init(); + await initPosthog(); await migrate(AppDb.client, AppDb.db); setupDbIndexes().catch(err => logger.error('Could not setup indexes indexes', err)); await configureCaddy(); @@ -89,7 +91,7 @@ export async function server(): Promise { app.use(Sentry.Handlers.errorHandler()); app.use(handleError); - const httpServer = createServer(app); + const httpServer = createHttpServer(app); createIoServer(httpServer); initSocketRooms(); diff --git a/server/src/entities/orgs/handlers/create-org.spec.ts b/server/src/entities/orgs/handlers/create-org.spec.ts index 2ca965b..709f265 100644 --- a/server/src/entities/orgs/handlers/create-org.spec.ts +++ b/server/src/entities/orgs/handlers/create-org.spec.ts @@ -2,7 +2,7 @@ import { testServer } from '../../../../tests/test-server'; import { spyOnCollection } from '../../../../tests/utils/spyon-collection'; import { spyOnVerifyToken } from '../../../../tests/utils/spyon-verifytoken'; import * as _emitEvent from '../../../events/emit-event'; -import { MeliServer } from '../../../server'; +import { MeliServer } from '../../../createServer'; import { User } from '../../users/user'; import request from 'supertest'; @@ -50,7 +50,7 @@ describe('createOrg', () => { .post('/api/v1/orgs') .set('Cookie', ['auth=testToken']) .send({ - name: 'Test Organization' + name: 'Test Organization', }); @@ -75,7 +75,7 @@ describe('createOrg', () => { userId: 'authenticatedUserId', admin: true, name: 'Authenticated User', - email: 'authenticated@test.tst' + email: 'authenticated@test.tst', })); expect(teams.insertOne).toHaveBeenCalled(); @@ -93,7 +93,7 @@ describe('createOrg', () => { .post('/api/v1/orgs') .set('Cookie', ['auth=testToken']) .send({ - name: 'Test Organization' + name: 'Test Organization', }); diff --git a/server/src/entities/orgs/handlers/teams/create-team.spec.ts b/server/src/entities/orgs/handlers/teams/create-team.spec.ts index e40f786..e2a1e08 100644 --- a/server/src/entities/orgs/handlers/teams/create-team.spec.ts +++ b/server/src/entities/orgs/handlers/teams/create-team.spec.ts @@ -5,7 +5,7 @@ import { spyOnCollection } from '../../../../../tests/utils/spyon-collection'; import { spyOnIsAdminOrOwner } from '../../../../../tests/utils/spyon-isadminorowner'; import { AUTHENTICATED_USER_ID, spyOnVerifyToken } from '../../../../../tests/utils/spyon-verifytoken'; import * as _emitEvent from '../../../../events/emit-event'; -import { MeliServer } from '../../../../server'; +import { MeliServer } from '../../../../createServer'; // jest.mock('../../../../env/env', () => ({ env: testEnv })); diff --git a/server/src/entities/sites/handlers/branches/add-branch.spec.ts b/server/src/entities/sites/handlers/branches/add-branch.spec.ts index e981319..c65446a 100644 --- a/server/src/entities/sites/handlers/branches/add-branch.spec.ts +++ b/server/src/entities/sites/handlers/branches/add-branch.spec.ts @@ -4,7 +4,7 @@ import { spyOnCollection } from '../../../../../tests/utils/spyon-collection'; import { spyOnVerifyToken } from '../../../../../tests/utils/spyon-verifytoken'; import * as _configureSiteBranchInCaddy from '../../../../caddy/configuration'; import * as _emitEvent from '../../../../events/emit-event'; -import { MeliServer } from '../../../../server'; +import { MeliServer } from '../../../../createServer'; import * as _linkBranchToRelease from '../../link-branch-to-release'; import { linkBranchToRelease } from '../../link-branch-to-release'; import { EventType } from '../../../../events/event-type'; diff --git a/server/src/entities/sites/handlers/branches/delete-branch.spec.ts b/server/src/entities/sites/handlers/branches/delete-branch.spec.ts index ef1bd83..0da3be0 100644 --- a/server/src/entities/sites/handlers/branches/delete-branch.spec.ts +++ b/server/src/entities/sites/handlers/branches/delete-branch.spec.ts @@ -5,7 +5,7 @@ import { spyOnVerifyToken } from '../../../../../tests/utils/spyon-verifytoken'; import * as _removeSiteBranchFromCaddy from '../../../../caddy/configuration'; import { removeSiteBranchFromCaddy } from '../../../../caddy/configuration'; import * as _emitEvent from '../../../../events/emit-event'; -import { MeliServer } from '../../../../server'; +import { MeliServer } from '../../../../createServer'; import { EventType } from '../../../../events/event-type'; import { canAdminSiteGuard } from '../../guards/can-admin-site-guard'; import { branchExistsGuard } from '../../guards/branch-exists-guard'; diff --git a/server/src/entities/teams/handlers/sites/create-site.spec.ts b/server/src/entities/teams/handlers/sites/create-site.spec.ts index fccc512..3495410 100644 --- a/server/src/entities/teams/handlers/sites/create-site.spec.ts +++ b/server/src/entities/teams/handlers/sites/create-site.spec.ts @@ -5,7 +5,7 @@ import { spyOnIsAdminOrOwner } from '../../../../../tests/utils/spyon-isadminoro import { AUTHENTICATED_USER_ID, spyOnVerifyToken } from '../../../../../tests/utils/spyon-verifytoken'; import * as _configureSiteInCaddy from '../../../../caddy/configuration'; import * as _emitEvent from '../../../../events/emit-event'; -import { MeliServer } from '../../../../server'; +import { MeliServer } from '../../../../createServer'; // jest.mock('../../../../env/env', () => ({ env: testEnv })); @@ -27,7 +27,7 @@ describe('createSite', () => { it('should create a site', async () => { const teams = spyOnCollection('Teams', { countDocuments: jest.fn().mockReturnValue(Promise.resolve(1)), - findOne: jest.fn().mockReturnValue(Promise.resolve({orgId: 'organization-id'})), + findOne: jest.fn().mockReturnValue(Promise.resolve({ orgId: 'organization-id' })), }); const members = spyOnCollection('Members', { countDocuments: jest.fn().mockReturnValue(Promise.resolve(1)), @@ -43,7 +43,7 @@ describe('createSite', () => { .post('/api/v1/teams/team-id/sites') .set('Cookie', ['auth=testToken']) .send({ - name: 'test-site' + name: 'test-site', }); @@ -79,19 +79,19 @@ describe('createSite', () => { .post('/api/v1/teams/team-id/sites') .set('Cookie', ['auth=testToken']) .send({ - name: 'test-site' + name: 'test-site', }); expect(response.status).toEqual(404); - expect(teams.countDocuments).toHaveBeenCalledWith({_id: 'team-id'}, expect.anything()); + expect(teams.countDocuments).toHaveBeenCalledWith({ _id: 'team-id' }, expect.anything()); }); it('should check if the user can administrate the team', async () => { const teams = spyOnCollection('Teams', { countDocuments: jest.fn().mockReturnValue(Promise.resolve(1)), - findOne: jest.fn().mockReturnValue(Promise.resolve({orgId: 'organization-id'})), + findOne: jest.fn().mockReturnValue(Promise.resolve({ orgId: 'organization-id' })), }); const isAdminOrOwner = spyOnIsAdminOrOwner(false); @@ -100,7 +100,7 @@ describe('createSite', () => { .post('/api/v1/teams/team-id/sites') .set('Cookie', ['auth=testToken']) .send({ - name: 'test-site' + name: 'test-site', }); @@ -112,7 +112,7 @@ describe('createSite', () => { it('should validate the site', async () => { const teams = spyOnCollection('Teams', { countDocuments: jest.fn().mockReturnValue(Promise.resolve(1)), - findOne: jest.fn().mockReturnValue(Promise.resolve({orgId: 'organization-id'})), + findOne: jest.fn().mockReturnValue(Promise.resolve({ orgId: 'organization-id' })), }); const isAdminOrOwner = spyOnIsAdminOrOwner(true); @@ -121,7 +121,7 @@ describe('createSite', () => { .post('/api/v1/teams/team-id/sites') .set('Cookie', ['auth=testToken']) .send({ - name: 'invalid site name' + name: 'invalid site name', }); expect(response.status).toEqual(400); diff --git a/server/src/entities/teams/handlers/sites/list-team-sites.spec.ts b/server/src/entities/teams/handlers/sites/list-team-sites.spec.ts index 0d9644b..37a2af5 100644 --- a/server/src/entities/teams/handlers/sites/list-team-sites.spec.ts +++ b/server/src/entities/teams/handlers/sites/list-team-sites.spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; import { testServer } from '../../../../../tests/test-server'; import { spyOnCollection } from '../../../../../tests/utils/spyon-collection'; import { spyOnVerifyToken } from '../../../../../tests/utils/spyon-verifytoken'; -import { MeliServer } from '../../../../server'; +import { MeliServer } from '../../../../createServer'; import * as _teamExistsGuard from '../../guards/team-exists-guard'; import * as _canReadTeamGuard from '../../guards/can-read-team-guard'; import * as _serializeSite from '../../../sites/serialize-site'; diff --git a/server/src/env/env-spec.ts b/server/src/env/env-spec.ts index 037bde7..b109fff 100644 --- a/server/src/env/env-spec.ts +++ b/server/src/env/env-spec.ts @@ -272,4 +272,7 @@ export const envSpec: EnvSpec = { MELI_GOOGLE_RECAPTCHA_SECRET_KEY: { schema: string().optional(), }, + MELI_POSTHOG_ENABLED: { + schema: boolean().optional().default(false), + }, }; diff --git a/server/src/env/env.ts b/server/src/env/env.ts index 8b1aed7..a161be2 100644 --- a/server/src/env/env.ts +++ b/server/src/env/env.ts @@ -73,6 +73,7 @@ export interface Env { MELI_MULTER_FORM_LIMITS: MulterLimitOptions; MELI_GOOGLE_RECAPTCHA_SITE_KEY: string; MELI_GOOGLE_RECAPTCHA_SECRET_KEY: string; + MELI_POSTHOG_ENABLED: boolean; } export const env: Env = parseEnv(envSpec); diff --git a/server/src/index.ts b/server/src/index.ts index b8f8623..d974401 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -11,7 +11,7 @@ require('source-map-support/register'); require('dotenv/config'); require('./commons/force-chalk-colors'); -const { server } = require('./server'); +const { createServer } = require('./createServer'); // eslint-disable-next-line no-console -server().catch(console.error); +createServer().catch(console.error); diff --git a/server/src/posthog/init-posthog.ts b/server/src/posthog/init-posthog.ts new file mode 100644 index 0000000..fb2c7c7 --- /dev/null +++ b/server/src/posthog/init-posthog.ts @@ -0,0 +1,31 @@ +import { v4 as uuid } from 'uuid'; +import { postHogId } from './posthog'; +import { sendHeartbeat } from './send-heartbeat'; +import { AppDb } from '../db/db'; + +export interface AppInfo { + _id: string; + value: string; +} + +export const AppInfos = () => AppDb.db.collection('app-info'); + +const appInfoKey = 'install_id'; + +export async function initPosthog() { + // id + const appInfo = await AppInfos().findOne({ _id: appInfoKey }); + if (appInfo) { + postHogId.id = appInfo.value; + } else { + postHogId.id = uuid(); + await AppInfos().insertOne({ + _id: appInfoKey, + value: postHogId.id, + }); + } + + // heartbeat + sendHeartbeat(); + setInterval(sendHeartbeat, 86400000); // every day +} diff --git a/server/src/posthog/posthog.ts b/server/src/posthog/posthog.ts new file mode 100644 index 0000000..f773462 --- /dev/null +++ b/server/src/posthog/posthog.ts @@ -0,0 +1,14 @@ +import PostHog from 'posthog-node'; +import { env } from '../env/env'; + +export const postHog = new PostHog( + '-BcCDFlG6nIchkTWROH5C3iplPWRjdEwrb6wpQKKwDg', + { + host: 'https://posthog.meli.sh', + enable: env.MELI_POSTHOG_ENABLED, + }, +); + +export const postHogId = { + id: undefined, +}; diff --git a/server/src/posthog/send-heartbeat.ts b/server/src/posthog/send-heartbeat.ts new file mode 100644 index 0000000..f2a404c --- /dev/null +++ b/server/src/posthog/send-heartbeat.ts @@ -0,0 +1,15 @@ +import { postHog, postHogId } from './posthog'; +import { Logger } from '../commons/logger/logger'; + +const logger = new Logger('app.posthog:sendHeartbeat'); + +export function sendHeartbeat() { + logger.debug('sending heartbeat'); + postHog.capture({ + event: 'heartbeat', + distinctId: postHogId.id, + properties: { + version: BUILD_INFO.version, + }, + }); +} diff --git a/server/tests/test-server.ts b/server/tests/test-server.ts index afc7907..3293e9c 100644 --- a/server/tests/test-server.ts +++ b/server/tests/test-server.ts @@ -12,7 +12,7 @@ import { Logger } from '../src/commons/logger/logger'; import { handleError } from '../src/commons/utils/handle-error'; import { env } from '../src/env/env'; import routes from '../src/routes'; -import { MeliServer } from '../src/server'; +import { MeliServer } from '../src/createServer'; import { authorizeReq } from '../src/auth/handlers/authorize-req'; import { authorizeApiReq } from '../src/auth/handlers/authorize-api-req'; import '../src/auth/passport'; diff --git a/ui/src/App.module.scss b/ui/src/App.module.scss index 7462a1f..d5c9fc9 100644 --- a/ui/src/App.module.scss +++ b/ui/src/App.module.scss @@ -30,3 +30,10 @@ $header-height: 70px; display: flex; flex-grow: 1; } + +.banner { + position: absolute; + top: 0; + left: 0; + z-index: 10; +} diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 46d5cd7..c901deb 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -26,6 +26,7 @@ import { UserView } from './components/user/UserView'; import { PrivateRoute } from './commons/components/PrivateRoute'; import { FullPageCentered } from './commons/components/FullPageCentered'; import { Loader } from './commons/components/Loader'; +import { PosthogWarning } from './posthog/PosthogWarning'; function Header() { const { user } = useAuth(); @@ -37,15 +38,15 @@ function Header() {
{user && ( <> - + {currentOrg && (
- + - +
)} @@ -65,14 +66,15 @@ export function App() {

Initializing - +

) : (
-
+ +
{currentOrg && ( - + )}
@@ -142,12 +144,12 @@ export function App() { redirectTo="/orgs" /> - + - +
-
+
); } diff --git a/ui/src/commons/components/modals/AppModal.module.scss b/ui/src/commons/components/modals/AppModal.module.scss index b9a5172..9cf0f44 100644 --- a/ui/src/commons/components/modals/AppModal.module.scss +++ b/ui/src/commons/components/modals/AppModal.module.scss @@ -7,7 +7,7 @@ bottom: 0; left: 0; right: 0; - background: transparentize($_primary, 0.8); + background: transparentize($_primary, 0.7); } .container { diff --git a/ui/src/commons/components/modals/AppModal.tsx b/ui/src/commons/components/modals/AppModal.tsx index 857fa65..4b242f0 100644 --- a/ui/src/commons/components/modals/AppModal.tsx +++ b/ui/src/commons/components/modals/AppModal.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react'; import Modal from 'react-modal'; import classNames from 'classnames'; import styles from './AppModal.module.scss'; +import { useBlur } from '../../../providers/BlurProvider'; export type AppModalProps = { isOpen: boolean; @@ -13,16 +14,6 @@ export type AppModalProps = { [key: string]: any; }; -const blurElId = 'blur-overlay'; - -function blurBackground() { - document.getElementById(blurElId).setAttribute('data-blur', 'true'); -} - -function unblurBackground() { - document.getElementById(blurElId).removeAttribute('data-blur'); -} - export function AppModal({ title, children, @@ -32,20 +23,24 @@ export function AppModal({ footer, ...otherProps }: AppModalProps) { + const { blur, unblur } = useBlur(); + const close = () => { - unblurBackground(); if (closeModal) { closeModal(); + unblur(); } }; + // TODO for some strange reason, isOpen goes to false and unblur is called + // this is why modal blur is broken useEffect(() => { if (isOpen) { - blurBackground(); + blur(); } else { - unblurBackground(); + unblur(); } - }, [isOpen]); + }, [isOpen, blur, unblur]); return ( void; className?: string; + cardClassName?: string; [key: string]: any; }; @@ -20,6 +21,7 @@ export function CardModal({ isOpen, closeModal, className, + cardClassName, footer, ...otherProps }: AppModalProps) { @@ -30,10 +32,10 @@ export function CardModal({ className={className} {...otherProps} > -
+
{title} - +
{children}
diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 28addcd..408c7b6 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -13,6 +13,7 @@ import { routerHistory } from './providers/history'; import { SocketProvider } from './websockets/SocketProvider'; import { isSentryEnabled, SENTRY_CONFIGURED, SentryProvider } from './commons/sentry/SentryProvider'; import { OrgProvider } from './providers/OrgProvider'; +import { BlurProvider } from './providers/BlurProvider'; if (SENTRY_CONFIGURED) { if (isSentryEnabled()) { @@ -73,7 +74,7 @@ const app = ( ReactDOM.render( // <> -
+ {SENTRY_CONFIGURED ? ( {app} @@ -81,7 +82,7 @@ ReactDOM.render( ) : ( app )} -
+ , //
, diff --git a/ui/src/posthog/PosthogWarning.module.scss b/ui/src/posthog/PosthogWarning.module.scss new file mode 100644 index 0000000..795c47b --- /dev/null +++ b/ui/src/posthog/PosthogWarning.module.scss @@ -0,0 +1,30 @@ +@import "src/styles/variables"; + +.container { + padding: 3rem; + background: linear-gradient(180deg, #200c39, #220f3f); + color: $light; +} + +.logos { + display: flex; + align-items: center; + justify-content: center; +} + +.message { + margin-top: 3rem; +} + +.posthogLogo { + height: 35px; +} + +.plus { + margin: 0 2rem; + font-size: 2rem; +} + +.meliLogo { + height: 35px; +} diff --git a/ui/src/posthog/PosthogWarning.tsx b/ui/src/posthog/PosthogWarning.tsx new file mode 100644 index 0000000..da5d0ac --- /dev/null +++ b/ui/src/posthog/PosthogWarning.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { ExternalLink } from '../commons/components/ExternalLink'; +import { useLocalStorage } from '../utils/use-local-storage'; +import styles from './PosthogWarning.module.scss'; +import classNames from 'classnames'; +import { AppModal } from '../commons/components/modals/AppModal'; +import postHogLogo from './posthog.svg'; +import meliLogo from './meli.svg'; +import { faPlus } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +export function PosthogWarning({ className }: { + className?: string; +}) { + const [show, setShow] = useLocalStorage('posthog.warning.show', true); + return ( + +
+
+ posthog + + meli +
+
+ We've added PostHog to Meli. + It helps us know which versions are being used in production and how many active installations are being deployed across the world. + You may opt-out of this feature. Please review this thread for more info. +
+
+ +
+
+
+ ) +} diff --git a/ui/src/posthog/meli.svg b/ui/src/posthog/meli.svg new file mode 100644 index 0000000..98bc496 --- /dev/null +++ b/ui/src/posthog/meli.svg @@ -0,0 +1,5 @@ + + + diff --git a/ui/src/posthog/posthog.svg b/ui/src/posthog/posthog.svg new file mode 100644 index 0000000..460e830 --- /dev/null +++ b/ui/src/posthog/posthog.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ui/src/providers/BlurProvider.module.scss b/ui/src/providers/BlurProvider.module.scss new file mode 100644 index 0000000..e6cee6f --- /dev/null +++ b/ui/src/providers/BlurProvider.module.scss @@ -0,0 +1,3 @@ +.blur { + filter: blur(8px); +} diff --git a/ui/src/providers/BlurProvider.tsx b/ui/src/providers/BlurProvider.tsx new file mode 100644 index 0000000..19b59df --- /dev/null +++ b/ui/src/providers/BlurProvider.tsx @@ -0,0 +1,32 @@ +import React, { createContext, useContext, useState } from "react"; +import styles from './BlurProvider.module.scss'; +import classNames from 'classnames'; + +interface Context { + isBlurred: boolean; + blur: () => void; + unblur: () => void; +} + +const context = createContext(undefined); +export const useBlur = () => useContext(context); + +export function BlurProvider({ children, ...props }) { + const [enabled, setEnabled] = useState(false); + return ( + setEnabled(true), + unblur: () => setEnabled(false), + }} + {...props} + > +
+ {children} +
+
+ ) +} diff --git a/ui/src/styles/variables.scss b/ui/src/styles/variables.scss index 734bee2..772f290 100644 --- a/ui/src/styles/variables.scss +++ b/ui/src/styles/variables.scss @@ -111,7 +111,7 @@ $input-height: 50px; $input-focus-border-color: $primary; $input-focus-box-shadow: none; -$btn-border-radius: $border-radius; +$btn-border-radius: 0; $btn-font-weight: bold; $btn-line-height: 100%; $btn-border-width: 2px; diff --git a/ui/src/utils/use-local-storage.ts b/ui/src/utils/use-local-storage.ts new file mode 100644 index 0000000..fbcc0d5 --- /dev/null +++ b/ui/src/utils/use-local-storage.ts @@ -0,0 +1,43 @@ +import { useState } from 'react'; + +function readFromStorage(key: string): any { + try { + const item = localStorage.getItem(key); + return JSON.parse(item); + } catch (e) { + console.log(`Could not read ${key} from local storage`, e); + return undefined; + } +} + +function writeToStorage(key: string, value: any): void { + try { + const val = JSON.stringify(value); + localStorage.setItem(key, val); + } catch (e) { + console.log(`Could not write ${key} to local storage`, value, e); + } +} + +function storageHasKey(key: string): boolean { + return !!localStorage.getItem(key); +} + +function init(key: string, value: any): any { + if (storageHasKey(key)) { + return readFromStorage(key); + } + writeToStorage(key, value); + return value; +} + +export function useLocalStorage(key: string, initialValue?: T): [T, (value: T) => void] { + const [value, setValue] = useState(init(key, initialValue)); + + const setValueProxy = (newValue: T) => { + setValue(newValue); + writeToStorage(key, newValue); + }; + + return [value, setValueProxy]; +}