From 875fb7734cd982ea5017b9eae04d4cd0470b05fc Mon Sep 17 00:00:00 2001 From: Youssef Date: Mon, 8 Nov 2021 16:31:38 +0100 Subject: [PATCH 01/21] create splash migration store and service file --- src/lib/db/user-splash-store.ts | 111 ++++++++++++++++++ src/lib/services/user-splash-service.ts | 47 ++++++++ src/lib/types/stores.ts | 3 + src/lib/types/stores/user-splash-store.ts | 19 +++ ...20211108130333-create-user-splash-table.js | 25 ++++ src/server-dev.ts | 2 +- 6 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 src/lib/db/user-splash-store.ts create mode 100644 src/lib/services/user-splash-service.ts create mode 100644 src/lib/types/stores/user-splash-store.ts create mode 100644 src/migrations/20211108130333-create-user-splash-table.js diff --git a/src/lib/db/user-splash-store.ts b/src/lib/db/user-splash-store.ts new file mode 100644 index 0000000000..77ddbc1c05 --- /dev/null +++ b/src/lib/db/user-splash-store.ts @@ -0,0 +1,111 @@ +import { Knex } from 'knex'; +import { EventEmitter } from 'events'; +import { LogProvider, Logger } from '../logger'; +import { + IUserSplash, + IUserSplashKey, + IUserSplashStore, +} from '../types/stores/user-splash-store'; + +const COLUMNS = ['user_id', 'splash_id', 'seen']; +const TABLE = 'user_splash'; + +interface IUserSplashTable { + seen?: boolean; + splash_id: string; + user_id: number; +} + +const fieldToRow = (fields: IUserSplash): IUserSplashTable => ({ + seen: fields.seen, + splash_id: fields.splashId, + user_id: fields.userId, +}); + +const rowToField = (row: IUserSplashTable): IUserSplash => ({ + seen: row.seen, + splashId: row.splash_id, + userId: row.user_id, +}); + +export default class UserSplashStore implements IUserSplashStore { + private db: Knex; + + private logger: Logger; + + constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { + this.db = db; + this.logger = getLogger('user-splash-store.ts'); + } + + async getAllUserSplashs(userId: number): Promise { + const userSplash = await this.db + .table(TABLE) + .select() + .where({ user_id: userId }); + + return userSplash.map(rowToField); + } + + async getSplash( + userId: number, + splashId: string, + ): Promise { + const userSplash = await this.db + .table(TABLE) + .select() + .where({ user_id: userId, splash_id: splashId }) + .first(); + + return rowToField(userSplash); + } + + async updateSplash(splash: IUserSplash): Promise { + const insertedSplash = await this.db + .table(TABLE) + .insert(fieldToRow(splash)) + .onConflict(['user_id', 'splash_id']) + .merge() + .returning(COLUMNS); + + return rowToField(insertedSplash[0]); + } + + async delete({ userId, splashId }: IUserSplashKey): Promise { + await this.db(TABLE) + .where({ user_id: userId, splash_id: splashId }) + .del(); + } + + async deleteAll(): Promise { + await this.db(TABLE).del(); + } + + destroy(): void {} + + async exists({ userId, splashId }: IUserSplashKey): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE user_id = ? AND splash_id = ?) AS present`, + [userId, splashId], + ); + const { present } = result.rows[0]; + return present; + } + + async get({ + userId, + splashId, + }: IUserSplashKey): Promise { + return this.getSplash(userId, splashId); + } + + async getAll(): Promise { + const userSplashs = await this.db + .table(TABLE) + .select(); + + return userSplashs.map(rowToField); + } +} + +module.exports = UserSplashStore; diff --git a/src/lib/services/user-splash-service.ts b/src/lib/services/user-splash-service.ts new file mode 100644 index 0000000000..a86b44f009 --- /dev/null +++ b/src/lib/services/user-splash-service.ts @@ -0,0 +1,47 @@ +import { Logger } from '../logger'; +import { IUnleashStores } from '../types/stores'; +import { IUnleashConfig } from '../types/option'; +import User from '../types/user'; +import { + IUserSplash, + IUserSplashStore, +} from '../types/stores/user-splash-store'; + +export default class UserSplashService { + private userSplashStore: IUserSplashStore; + + private logger: Logger; + + constructor( + { userSplashStore }: Pick, + { getLogger }: Pick, + ) { + this.userSplashStore = userSplashStore; + this.logger = getLogger('services/user-splash-service.js'); + } + + async getAllUserSplash(user: User): Promise { + if (user.isAPI) { + return []; + } + try { + return await this.userSplashStore.getAllUserSplashs(user.id); + } catch (err) { + this.logger.error(err); + return []; + } + } + + async getSplash( + user_id: number, + splash_id: string, + ): Promise { + return this.userSplashStore.getSplash(user_id, splash_id); + } + + async updateSplash(splash: IUserSplash): Promise { + return this.userSplashStore.updateSplash(splash); + } +} + +module.exports = UserSplashService; diff --git a/src/lib/types/stores.ts b/src/lib/types/stores.ts index ff22f24b3f..ff09b606a5 100644 --- a/src/lib/types/stores.ts +++ b/src/lib/types/stores.ts @@ -23,6 +23,8 @@ import { IFeatureStrategiesStore } from './stores/feature-strategies-store'; import { IEnvironmentStore } from './stores/environment-store'; import { IFeatureToggleClientStore } from './stores/feature-toggle-client-store'; import { IClientMetricsStoreV2 } from './stores/client-metrics-store-v2'; +import { IUserSplashStore } from './stores/user-splash-store'; + export interface IUnleashStores { accessStore: IAccessStore; @@ -50,4 +52,5 @@ export interface IUnleashStores { tagTypeStore: ITagTypeStore; userFeedbackStore: IUserFeedbackStore; userStore: IUserStore; + userSplashStore: IUserSplashStore; } diff --git a/src/lib/types/stores/user-splash-store.ts b/src/lib/types/stores/user-splash-store.ts new file mode 100644 index 0000000000..ebe833e823 --- /dev/null +++ b/src/lib/types/stores/user-splash-store.ts @@ -0,0 +1,19 @@ +import { Store } from './store'; + +export interface IUserSplash { + seen?: boolean; + splashId: string; + userId: number; +} + +export interface IUserSplashKey { + userId: number; + splashId: string; +} + +export interface IUserSplashStore + extends Store { + getAllUserSplashs(userId: number): Promise; + getSplash(userId: number, splashId: string): Promise; + updateSplash(splash: IUserSplash): Promise; +} diff --git a/src/migrations/20211108130333-create-user-splash-table.js b/src/migrations/20211108130333-create-user-splash-table.js new file mode 100644 index 0000000000..067ec76bd3 --- /dev/null +++ b/src/migrations/20211108130333-create-user-splash-table.js @@ -0,0 +1,25 @@ +'use strict'; + +exports.up = function (db, cb) { + db.runSql( + ` + CREATE TABLE IF NOT EXISTS user_splash + (user_id INTEGER NOT NULL references users (id), + splash_id TEXT, + seen BOOLEAN NOT NULL DEFAULT false, + PRIMARY KEY (user_id, splash_id)); + CREATE INDEX user_splash_user_id_idx ON user_splash (user_id); + );`, + cb, + ); +}; + +exports.down = function (db, cb) { + db.runSql( + ` + DROP INDEX user_splash_user_id_idx; + DROP TABLE user_splash; +`, + cb, +); +}; diff --git a/src/server-dev.ts b/src/server-dev.ts index bf1cea9c95..34d7b1aec2 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -8,7 +8,7 @@ process.nextTick(async () => { createConfig({ db: { user: 'unleash_user', - password: 'passord', + password: 'some_password', host: 'localhost', port: 5432, database: 'unleash', From d73293c5762d9667571b3819efef79fa1e013629 Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Tue, 9 Nov 2021 11:52:02 +0100 Subject: [PATCH 02/21] fix: add migration --- src/lib/db/index.ts | 2 + src/lib/db/user-splash-store.ts | 10 +-- src/lib/routes/admin-api/user.ts | 6 ++ src/lib/services/index.ts | 3 + src/lib/services/user-splash-service.ts | 6 +- src/lib/types/services.ts | 2 + src/lib/types/stores/user-splash-store.ts | 5 +- ...20211108130333-create-user-splash-table.js | 11 ++-- ...211109103930-add-splash-entry-for-users.js | 15 +++++ src/server-dev.ts | 2 +- src/test/fixtures/fake-user-splash-store.ts | 65 +++++++++++++++++++ src/test/fixtures/store.ts | 2 + 12 files changed, 107 insertions(+), 22 deletions(-) create mode 100644 src/migrations/20211109103930-add-splash-entry-for-users.js create mode 100644 src/test/fixtures/fake-user-splash-store.ts diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index a7d0c07d04..fbe9cec78f 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -29,6 +29,7 @@ import EnvironmentStore from './environment-store'; import FeatureTagStore from './feature-tag-store'; import { FeatureEnvironmentStore } from './feature-environment-store'; import { ClientMetricsStoreV2 } from './client-metrics-store-v2'; +import UserSplashStore from './user-splash-store'; export const createStores = ( config: IUnleashConfig, @@ -85,6 +86,7 @@ export const createStores = ( eventBus, getLogger, ), + userSplashStore: new UserSplashStore(db, eventBus, getLogger), }; }; diff --git a/src/lib/db/user-splash-store.ts b/src/lib/db/user-splash-store.ts index 77ddbc1c05..6b867ee4d7 100644 --- a/src/lib/db/user-splash-store.ts +++ b/src/lib/db/user-splash-store.ts @@ -47,10 +47,7 @@ export default class UserSplashStore implements IUserSplashStore { return userSplash.map(rowToField); } - async getSplash( - userId: number, - splashId: string, - ): Promise { + async getSplash(userId: number, splashId: string): Promise { const userSplash = await this.db .table(TABLE) .select() @@ -92,10 +89,7 @@ export default class UserSplashStore implements IUserSplashStore { return present; } - async get({ - userId, - splashId, - }: IUserSplashKey): Promise { + async get({ userId, splashId }: IUserSplashKey): Promise { return this.getSplash(userId, splashId); } diff --git a/src/lib/routes/admin-api/user.ts b/src/lib/routes/admin-api/user.ts index 6b5299311f..1d2815a9c6 100644 --- a/src/lib/routes/admin-api/user.ts +++ b/src/lib/routes/admin-api/user.ts @@ -7,6 +7,7 @@ import { IUnleashServices } from '../../types/services'; import UserService from '../../services/user-service'; import SessionService from '../../services/session-service'; import UserFeedbackService from '../../services/user-feedback-service'; +import UserSplashService from 'lib/services/user-splash-service'; interface IChangeUserRequest { password: string; @@ -22,6 +23,8 @@ class UserController extends Controller { private sessionService: SessionService; + private userSplashService: UserSplashService; + constructor( config: IUnleashConfig, { @@ -29,12 +32,14 @@ class UserController extends Controller { userService, sessionService, userFeedbackService, + userSplashService, }: Pick< IUnleashServices, | 'accessService' | 'userService' | 'sessionService' | 'userFeedbackService' + | 'userSplashService' >, ) { super(config); @@ -57,6 +62,7 @@ class UserController extends Controller { const feedback = await this.userFeedbackService.getAllUserFeedback( user, ); + //const splash = await this.userSplashService.getAllUserSplash(user); // TODO: remove this line after we remove it from db. delete user.permissions; diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 8e39e913cb..5d3ddc3e95 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -27,6 +27,7 @@ import FeatureToggleService from './feature-toggle-service'; import EnvironmentService from './environment-service'; import FeatureTagService from './feature-tag-service'; import ProjectHealthService from './project-health-service'; +import UserSplashService from './user-splash-service'; export const createServices = ( stores: IUnleashStores, @@ -72,6 +73,7 @@ export const createServices = ( accessService, featureToggleServiceV2, ); + const userSplashService = new UserSplashService(stores, config); return { accessService, @@ -100,6 +102,7 @@ export const createServices = ( userFeedbackService, featureTagService, projectHealthService, + userSplashService, }; }; diff --git a/src/lib/services/user-splash-service.ts b/src/lib/services/user-splash-service.ts index a86b44f009..9dfb163dd2 100644 --- a/src/lib/services/user-splash-service.ts +++ b/src/lib/services/user-splash-service.ts @@ -28,14 +28,12 @@ export default class UserSplashService { return await this.userSplashStore.getAllUserSplashs(user.id); } catch (err) { this.logger.error(err); + return []; } } - async getSplash( - user_id: number, - splash_id: string, - ): Promise { + async getSplash(user_id: number, splash_id: string): Promise { return this.userSplashStore.getSplash(user_id, splash_id); } diff --git a/src/lib/types/services.ts b/src/lib/types/services.ts index b2528df9f8..52dab846bb 100644 --- a/src/lib/types/services.ts +++ b/src/lib/types/services.ts @@ -23,6 +23,7 @@ import EnvironmentService from '../services/environment-service'; import FeatureTagService from '../services/feature-tag-service'; import ProjectHealthService from '../services/project-health-service'; import ClientMetricsServiceV2 from '../services/client-metrics/client-metrics-service-v2'; +import UserSplashService from '../services/user-splash-service'; export interface IUnleashServices { accessService: AccessService; @@ -51,4 +52,5 @@ export interface IUnleashServices { userFeedbackService: UserFeedbackService; userService: UserService; versionService: VersionService; + userSplashService: UserSplashService; } diff --git a/src/lib/types/stores/user-splash-store.ts b/src/lib/types/stores/user-splash-store.ts index ebe833e823..75938981ef 100644 --- a/src/lib/types/stores/user-splash-store.ts +++ b/src/lib/types/stores/user-splash-store.ts @@ -1,7 +1,7 @@ import { Store } from './store'; export interface IUserSplash { - seen?: boolean; + seen: boolean; splashId: string; userId: number; } @@ -11,8 +11,7 @@ export interface IUserSplashKey { splashId: string; } -export interface IUserSplashStore - extends Store { +export interface IUserSplashStore extends Store { getAllUserSplashs(userId: number): Promise; getSplash(userId: number, splashId: string): Promise; updateSplash(splash: IUserSplash): Promise; diff --git a/src/migrations/20211108130333-create-user-splash-table.js b/src/migrations/20211108130333-create-user-splash-table.js index 067ec76bd3..21953415b3 100644 --- a/src/migrations/20211108130333-create-user-splash-table.js +++ b/src/migrations/20211108130333-create-user-splash-table.js @@ -8,18 +8,17 @@ exports.up = function (db, cb) { splash_id TEXT, seen BOOLEAN NOT NULL DEFAULT false, PRIMARY KEY (user_id, splash_id)); - CREATE INDEX user_splash_user_id_idx ON user_splash (user_id); - );`, + CREATE INDEX user_splash_user_id_idx ON user_splash (user_id);`, cb, ); }; exports.down = function (db, cb) { - db.runSql( - ` + db.runSql( + ` DROP INDEX user_splash_user_id_idx; DROP TABLE user_splash; `, - cb, -); + cb, + ); }; diff --git a/src/migrations/20211109103930-add-splash-entry-for-users.js b/src/migrations/20211109103930-add-splash-entry-for-users.js new file mode 100644 index 0000000000..8a34421415 --- /dev/null +++ b/src/migrations/20211109103930-add-splash-entry-for-users.js @@ -0,0 +1,15 @@ +exports.up = function (db, cb) { + db.runSql(`SELECT * FROM users`, (err, results) => { + results.rows.forEach((user) => { + db.runSql( + `INSERT INTO user_splash(splash_id, user_id, seen) VALUES (?, ?, ?)`, + ['environments', user.id, false], + ); + }); + cb(); + }); +}; + +exports.down = function (db, cb) { + db.runSql('DELETE FROM user_splash', cb); +}; diff --git a/src/server-dev.ts b/src/server-dev.ts index 34d7b1aec2..bf1cea9c95 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -8,7 +8,7 @@ process.nextTick(async () => { createConfig({ db: { user: 'unleash_user', - password: 'some_password', + password: 'passord', host: 'localhost', port: 5432, database: 'unleash', diff --git a/src/test/fixtures/fake-user-splash-store.ts b/src/test/fixtures/fake-user-splash-store.ts new file mode 100644 index 0000000000..2fb6af87b8 --- /dev/null +++ b/src/test/fixtures/fake-user-splash-store.ts @@ -0,0 +1,65 @@ +import { + IUserSplashKey, + IUserSplash, + IUserSplashStore, +} from 'lib/types/stores/user-splash-store'; + +export default class FakeUserSplashStore implements IUserSplashStore { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getAllUserSplashs(userId: number): Promise { + return Promise.resolve([]); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getSplash(userId: number, splashId: string): Promise { + return Promise.resolve({ seen: false, userId: 123, splashId: 'env' }); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + updateSplash(splash: IUserSplash): Promise { + return Promise.resolve({ seen: false, userId: 123, splashId: 'env' }); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + exists(key: IUserSplashKey): Promise { + return Promise.resolve(false); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + get(key: IUserSplashKey): Promise { + return Promise.resolve(undefined); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getAll(): Promise { + return Promise.resolve([]); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getAllUserFeedback(userId: number): Promise { + return Promise.resolve([]); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getFeedback(userId: number, feedbackId: string): Promise { + return Promise.resolve(undefined); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + updateFeedback(feedback: IUserSplash): Promise { + return Promise.resolve(undefined); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + delete(key: IUserSplashKey): Promise { + return Promise.resolve(undefined); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + deleteAll(): Promise { + return Promise.resolve(undefined); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + destroy(): void {} +} diff --git a/src/test/fixtures/store.ts b/src/test/fixtures/store.ts index 5e36e35ff0..6b6bf25d04 100644 --- a/src/test/fixtures/store.ts +++ b/src/test/fixtures/store.ts @@ -24,6 +24,7 @@ import FakeFeatureTypeStore from './fake-feature-type-store'; import FakeResetTokenStore from './fake-reset-token-store'; import FakeFeatureToggleClientStore from './fake-feature-toggle-client-store'; import FakeClientMetricsStoreV2 from './fake-client-metrics-store-v2'; +import FakeUserSplashStore from './fake-user-splash-store'; const createStores: () => IUnleashStores = () => { const db = { @@ -59,6 +60,7 @@ const createStores: () => IUnleashStores = () => { featureTypeStore: new FakeFeatureTypeStore(), resetTokenStore: new FakeResetTokenStore(), sessionStore: new FakeSessionStore(), + userSplashStore: new FakeUserSplashStore(), }; }; From cc516618a1ff1021817033232307bf352e414341 Mon Sep 17 00:00:00 2001 From: Youssef Date: Tue, 9 Nov 2021 14:13:30 +0100 Subject: [PATCH 03/21] add splash-controller and cascade migration --- .../admin-api/user-splash-controller.ts | 76 +++++++++++++++++++ ...11109123505-add-cascade-for-user-splash.js | 16 ++++ 2 files changed, 92 insertions(+) create mode 100644 src/lib/routes/admin-api/user-splash-controller.ts create mode 100644 src/migrations/20211109123505-add-cascade-for-user-splash.js diff --git a/src/lib/routes/admin-api/user-splash-controller.ts b/src/lib/routes/admin-api/user-splash-controller.ts new file mode 100644 index 0000000000..c2e9ad7b14 --- /dev/null +++ b/src/lib/routes/admin-api/user-splash-controller.ts @@ -0,0 +1,76 @@ +import { Response } from 'express'; + +import Controller from '../controller'; +import { Logger } from '../../logger'; +import { IUnleashConfig } from '../../types/option'; +import { IUnleashServices } from '../../types/services'; +import UserSplashService from '../../services/user-splash-service'; +import { IAuthRequest } from '../unleash-types'; + +interface ISplashBody { + seen: boolean; + splashId: string; +} + +class UserSplashController extends Controller { + private logger: Logger; + private userSplashService: UserSplashService; + + constructor( + config: IUnleashConfig, + { userSplashService }: Pick, + ) { + super(config); + this.logger = config.getLogger('splash-controller.ts'); + this.userSplashService = userSplashService; + + this.post('/', this.recordSplash); + this.put('/:id', this.updateSplashSettings); + } + + private async recordSplash( + req: IAuthRequest, + res: Response, + ): Promise { + const BAD_REQUEST = 400; + const { user } = req; + + const { splashId } = req.body; + + if (!splashId) { + res.status(BAD_REQUEST).json({ + error: 'splashId must be present.', + }); + return; + } + + const splash = { + ...req.body, + userId: user.id, + seen: req.body.seen || false, + }; + + const updated = await this.userSplashService.updateSplash(splash); + res.json(updated); + } + + private async updateSplashSettings( + req: IAuthRequest, + res: Response, + ): Promise { + const { user } = req; + const { id } = req.params; + + const splash = { + ...req.body, + splashId: id, + userId: user.id, + seen: req.body.seen || false, + }; + + const updated = await this.userSplashService.updateSplash(splash); + res.json(updated); + } +} + +module.exports = UserSplashController; diff --git a/src/migrations/20211109123505-add-cascade-for-user-splash.js b/src/migrations/20211109123505-add-cascade-for-user-splash.js new file mode 100644 index 0000000000..9a62b4e4e8 --- /dev/null +++ b/src/migrations/20211109123505-add-cascade-for-user-splash.js @@ -0,0 +1,16 @@ +exports.up = function (db, cb) { + db.runSql( + ` + ALTER TABLE user_splash DROP CONSTRAINT user_splash_user_id_fkey; + ALTER TABLE user_splash + ADD CONSTRAINT user_splash_user_id_fkey + FOREIGN KEY (user_id) + REFERENCES users(id) ON DELETE CASCADE; +`, + cb, + ); +}; + +exports.down = function (db, cb) { + db.runSql('', cb); +}; From 96f2514fc5619d7d33105c1c3b44e72095e3cf96 Mon Sep 17 00:00:00 2001 From: Youssef Date: Tue, 9 Nov 2021 20:39:13 +0100 Subject: [PATCH 04/21] send splash data in the user object --- src/lib/routes/admin-api/index.ts | 5 ++++ .../admin-api/user-splash-controller.ts | 1 + src/lib/routes/admin-api/user.ts | 9 ++++--- src/lib/services/user-splash-service.ts | 13 ++++++++-- src/test/fixtures/fake-user-splash-store.ts | 25 ++++--------------- 5 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/lib/routes/admin-api/index.ts b/src/lib/routes/admin-api/index.ts index 3e0edf6114..f1aa75c7fa 100644 --- a/src/lib/routes/admin-api/index.ts +++ b/src/lib/routes/admin-api/index.ts @@ -21,6 +21,7 @@ import ApiTokenController from './api-token-controller'; import UserAdminController from './user-admin'; import EmailController from './email'; import UserFeedbackController from './user-feedback-controller'; +import UserSplashController from './user-splash-controller'; import ProjectApi from './project'; import { EnvironmentsController } from './environments-controller'; @@ -92,6 +93,10 @@ class AdminApi extends Controller { '/environments', new EnvironmentsController(config, services).router, ); + this.app.use( + '/splash', + new UserSplashController(config, services).router, + ); } index(req, res) { diff --git a/src/lib/routes/admin-api/user-splash-controller.ts b/src/lib/routes/admin-api/user-splash-controller.ts index c2e9ad7b14..87815ebd3a 100644 --- a/src/lib/routes/admin-api/user-splash-controller.ts +++ b/src/lib/routes/admin-api/user-splash-controller.ts @@ -74,3 +74,4 @@ class UserSplashController extends Controller { } module.exports = UserSplashController; +export default UserSplashController; \ No newline at end of file diff --git a/src/lib/routes/admin-api/user.ts b/src/lib/routes/admin-api/user.ts index 1d2815a9c6..3e13b4db19 100644 --- a/src/lib/routes/admin-api/user.ts +++ b/src/lib/routes/admin-api/user.ts @@ -7,7 +7,7 @@ import { IUnleashServices } from '../../types/services'; import UserService from '../../services/user-service'; import SessionService from '../../services/session-service'; import UserFeedbackService from '../../services/user-feedback-service'; -import UserSplashService from 'lib/services/user-splash-service'; +import UserSplashService from '../../services/user-splash-service'; interface IChangeUserRequest { password: string; @@ -47,6 +47,7 @@ class UserController extends Controller { this.userService = userService; this.sessionService = sessionService; this.userFeedbackService = userFeedbackService; + this.userSplashService = userSplashService; this.get('/', this.getUser); this.post('/change-password', this.updateUserPass); @@ -62,12 +63,14 @@ class UserController extends Controller { const feedback = await this.userFeedbackService.getAllUserFeedback( user, ); - //const splash = await this.userSplashService.getAllUserSplash(user); + const splash = await this.userSplashService.getAllUserSplashs( + user, + ); // TODO: remove this line after we remove it from db. delete user.permissions; - return res.status(200).json({ user, permissions, feedback }).end(); + return res.status(200).json({ user, permissions, feedback, splash }).end(); } async updateUserPass( diff --git a/src/lib/services/user-splash-service.ts b/src/lib/services/user-splash-service.ts index 9dfb163dd2..65d604ef85 100644 --- a/src/lib/services/user-splash-service.ts +++ b/src/lib/services/user-splash-service.ts @@ -20,12 +20,21 @@ export default class UserSplashService { this.logger = getLogger('services/user-splash-service.js'); } - async getAllUserSplash(user: User): Promise { + async getAllUserSplashs(user: User): Promise { if (user.isAPI) { return []; } try { - return await this.userSplashStore.getAllUserSplashs(user.id); + const splashs = await ( + await this.userSplashStore.getAllUserSplashs(user.id) + ).reduce( + (splashObject, splash) => ({ + ...splashObject, + [splash.splashId]: splash.seen, + }), + {}, + ); + return splashs; } catch (err) { this.logger.error(err); diff --git a/src/test/fixtures/fake-user-splash-store.ts b/src/test/fixtures/fake-user-splash-store.ts index 2fb6af87b8..adc0775c7b 100644 --- a/src/test/fixtures/fake-user-splash-store.ts +++ b/src/test/fixtures/fake-user-splash-store.ts @@ -2,7 +2,7 @@ import { IUserSplashKey, IUserSplash, IUserSplashStore, -} from 'lib/types/stores/user-splash-store'; +} from '../../lib/types/stores/user-splash-store' export default class FakeUserSplashStore implements IUserSplashStore { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -11,13 +11,13 @@ export default class FakeUserSplashStore implements IUserSplashStore { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - getSplash(userId: number, splashId: string): Promise { - return Promise.resolve({ seen: false, userId: 123, splashId: 'env' }); + getSplash(userId: number, splashId: string): Promise { + return Promise.resolve(undefined); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - updateSplash(splash: IUserSplash): Promise { - return Promise.resolve({ seen: false, userId: 123, splashId: 'env' }); + updateSplash(splash: IUserSplash): Promise { + return Promise.resolve(undefined); } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -35,21 +35,6 @@ export default class FakeUserSplashStore implements IUserSplashStore { return Promise.resolve([]); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getAllUserFeedback(userId: number): Promise { - return Promise.resolve([]); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getFeedback(userId: number, feedbackId: string): Promise { - return Promise.resolve(undefined); - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - updateFeedback(feedback: IUserSplash): Promise { - return Promise.resolve(undefined); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars delete(key: IUserSplashKey): Promise { return Promise.resolve(undefined); From c4da3e89c5a02364e77e66dbe635a501bdb25e9a Mon Sep 17 00:00:00 2001 From: Youssef Date: Tue, 9 Nov 2021 20:55:23 +0100 Subject: [PATCH 05/21] fix: styling --- src/lib/routes/admin-api/user-splash-controller.ts | 3 ++- src/lib/routes/admin-api/user.ts | 9 +++++---- src/lib/types/stores.ts | 1 - .../20211109123505-add-cascade-for-user-splash.js | 10 +++++----- src/test/fixtures/fake-user-splash-store.ts | 6 +++--- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/lib/routes/admin-api/user-splash-controller.ts b/src/lib/routes/admin-api/user-splash-controller.ts index 87815ebd3a..68872fad54 100644 --- a/src/lib/routes/admin-api/user-splash-controller.ts +++ b/src/lib/routes/admin-api/user-splash-controller.ts @@ -14,6 +14,7 @@ interface ISplashBody { class UserSplashController extends Controller { private logger: Logger; + private userSplashService: UserSplashService; constructor( @@ -74,4 +75,4 @@ class UserSplashController extends Controller { } module.exports = UserSplashController; -export default UserSplashController; \ No newline at end of file +export default UserSplashController; diff --git a/src/lib/routes/admin-api/user.ts b/src/lib/routes/admin-api/user.ts index 3e13b4db19..a0449d1b8d 100644 --- a/src/lib/routes/admin-api/user.ts +++ b/src/lib/routes/admin-api/user.ts @@ -63,14 +63,15 @@ class UserController extends Controller { const feedback = await this.userFeedbackService.getAllUserFeedback( user, ); - const splash = await this.userSplashService.getAllUserSplashs( - user, - ); + const splash = await this.userSplashService.getAllUserSplashs(user); // TODO: remove this line after we remove it from db. delete user.permissions; - return res.status(200).json({ user, permissions, feedback, splash }).end(); + return res + .status(200) + .json({ user, permissions, feedback, splash }) + .end(); } async updateUserPass( diff --git a/src/lib/types/stores.ts b/src/lib/types/stores.ts index ff09b606a5..cb92c0c44a 100644 --- a/src/lib/types/stores.ts +++ b/src/lib/types/stores.ts @@ -25,7 +25,6 @@ import { IFeatureToggleClientStore } from './stores/feature-toggle-client-store' import { IClientMetricsStoreV2 } from './stores/client-metrics-store-v2'; import { IUserSplashStore } from './stores/user-splash-store'; - export interface IUnleashStores { accessStore: IAccessStore; addonStore: IAddonStore; diff --git a/src/migrations/20211109123505-add-cascade-for-user-splash.js b/src/migrations/20211109123505-add-cascade-for-user-splash.js index 9a62b4e4e8..5f9754c440 100644 --- a/src/migrations/20211109123505-add-cascade-for-user-splash.js +++ b/src/migrations/20211109123505-add-cascade-for-user-splash.js @@ -1,16 +1,16 @@ exports.up = function (db, cb) { - db.runSql( - ` + db.runSql( + ` ALTER TABLE user_splash DROP CONSTRAINT user_splash_user_id_fkey; ALTER TABLE user_splash ADD CONSTRAINT user_splash_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; `, - cb, - ); + cb, + ); }; exports.down = function (db, cb) { - db.runSql('', cb); + db.runSql('', cb); }; diff --git a/src/test/fixtures/fake-user-splash-store.ts b/src/test/fixtures/fake-user-splash-store.ts index adc0775c7b..5b3c6e8331 100644 --- a/src/test/fixtures/fake-user-splash-store.ts +++ b/src/test/fixtures/fake-user-splash-store.ts @@ -2,7 +2,7 @@ import { IUserSplashKey, IUserSplash, IUserSplashStore, -} from '../../lib/types/stores/user-splash-store' +} from '../../lib/types/stores/user-splash-store'; export default class FakeUserSplashStore implements IUserSplashStore { // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -11,12 +11,12 @@ export default class FakeUserSplashStore implements IUserSplashStore { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - getSplash(userId: number, splashId: string): Promise { + getSplash(userId: number, splashId: string): Promise { return Promise.resolve(undefined); } // eslint-disable-next-line @typescript-eslint/no-unused-vars - updateSplash(splash: IUserSplash): Promise { + updateSplash(splash: IUserSplash): Promise { return Promise.resolve(undefined); } From db9722dccc22262da692be7e164d8401c11bd821 Mon Sep 17 00:00:00 2001 From: Youssef Date: Wed, 10 Nov 2021 11:24:01 +0100 Subject: [PATCH 06/21] add splash e2e + api tests --- src/test/e2e/api/admin/splash.e2e.test.ts | 97 +++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/test/e2e/api/admin/splash.e2e.test.ts diff --git a/src/test/e2e/api/admin/splash.e2e.test.ts b/src/test/e2e/api/admin/splash.e2e.test.ts new file mode 100644 index 0000000000..263d7319dd --- /dev/null +++ b/src/test/e2e/api/admin/splash.e2e.test.ts @@ -0,0 +1,97 @@ +import { Application, NextFunction, Request, Response } from 'express'; +import { setupAppWithCustomAuth } from '../../helpers/test-helper'; +import dbInit from '../../helpers/database-init'; +import getLogger from '../../../fixtures/no-logger'; +import { IUnleashConfig } from '../../../../lib/types/option'; +import { IUnleashServices } from '../../../../lib/types/services'; + +let stores; +let db; +let app; + +beforeAll(async () => { + db = await dbInit('splash_api_serial', getLogger); + stores = db.stores; + + const email = 'custom-user@mail.com'; + + const preHook = ( + application: Application, + config: IUnleashConfig, + { userService }: IUnleashServices, + ) => { + application.use( + '/api/admin/', + async (req: Request, res: Response, next: NextFunction) => { + // @ts-ignore + req.user = await userService.loginUserWithoutPassword( + email, + true, + ); + next(); + }, + ); + }; + + app = await setupAppWithCustomAuth(stores, preHook); +}); + +afterAll(async () => { + await app.destroy(); + await db.destroy(); +}); + +test('it creates splash for user', async () => { + expect.assertions(1); + + return app.request + .post('/api/admin/splash') + .send({ splashId: 'environment' }) + .set('Content-Type', 'application/json') + .expect('Content-Type', /json/) + .expect(200) + .expect((res) => { + expect(res.body.splashId).toBe('environment'); + }); +}); + +test('it gives 400 when splash is not present', async () => { + expect.assertions(1); + + return app.request + .post('/api/admin/splash') + .send({}) + .set('Content-Type', 'application/json') + .expect('Content-Type', /json/) + .expect(400) + .expect((res) => { + expect(res.body.error).toBeTruthy(); + }); +}); + +test('it updates splash for user', async () => { + expect.assertions(1); + + return app.request + .put('/api/admin/splash/environment') + .send({ seen: true }) + .set('Content-Type', 'application/json') + .expect('Content-Type', /json/) + .expect(200) + .expect((res) => { + expect(res.body.seen).toBe(true); + }); +}); + +test('it retrieves splash for user', async () => { + expect.assertions(1); + + return app.request + .get('/api/admin/user') + .set('Content-Type', 'application/json') + .expect('Content-Type', /json/) + .expect(200) + .expect((res) => { + expect(res.body.splash).toStrictEqual({ environment: true }); + }); +}); From cd38c5a01dca6a5813332149037fa05add0bd642 Mon Sep 17 00:00:00 2001 From: Youssef Date: Wed, 10 Nov 2021 11:24:44 +0100 Subject: [PATCH 07/21] add e2e splash test --- .../e2e/stores/user-splash-store.e2e.test.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/test/e2e/stores/user-splash-store.e2e.test.ts diff --git a/src/test/e2e/stores/user-splash-store.e2e.test.ts b/src/test/e2e/stores/user-splash-store.e2e.test.ts new file mode 100644 index 0000000000..73050f2c0b --- /dev/null +++ b/src/test/e2e/stores/user-splash-store.e2e.test.ts @@ -0,0 +1,82 @@ +import { IUserSplashStore } from 'lib/types/stores/user-splash-store'; +import { IUserStore } from 'lib/types/stores/user-store'; +import dbInit from '../helpers/database-init'; +import getLogger from '../../fixtures/no-logger'; + +let stores; +let db; +let userSplashStore: IUserSplashStore; +let userStore: IUserStore; +let currentUser; + +beforeAll(async () => { + db = await dbInit('user_splash_store', getLogger); + stores = db.stores; + userSplashStore = stores.userSplashStore; + userStore = stores.userStore; + currentUser = await userStore.upsert({ email: 'me.feedback@mail.com' }); +}); + +afterAll(async () => { + await db.destroy(); +}); + +afterEach(async () => { + await userSplashStore.deleteAll(); +}); + +test('should create userSplash', async () => { + await userSplashStore.updateSplash({ + splashId: 'some-id', + userId: currentUser.id, + seen: false, + }); + const userSplashs = await userSplashStore.getAllUserSplashs(currentUser.id); + expect(userSplashs).toHaveLength(1); + expect(userSplashs[0].splashId).toBe('some-id'); +}); + +test('should get userSplash', async () => { + await userSplashStore.updateSplash({ + splashId: 'some-id', + userId: currentUser.id, + seen: false, + }); + const userSplash = await userSplashStore.getSplash( + currentUser.id, + 'some-id', + ); + expect(userSplash.splashId).toBe('some-id'); +}); + +test('should exists', async () => { + await userSplashStore.updateSplash({ + splashId: 'some-id-3', + userId: currentUser.id, + seen: false, + }); + const exists = await userSplashStore.exists({ + userId: currentUser.id, + splashId: 'some-id-3', + }); + expect(exists).toBe(true); +}); + +test('should not exists', async () => { + const exists = await userSplashStore.exists({ + userId: currentUser.id, + splashId: 'some-id-not-here', + }); + expect(exists).toBe(false); +}); + +test('should get all userSplashs', async () => { + await userSplashStore.updateSplash({ + splashId: 'some-id-2', + userId: currentUser.id, + seen: false, + }); + const userSplashs = await userSplashStore.getAll(); + expect(userSplashs).toHaveLength(1); + expect(userSplashs[0].splashId).toBe('some-id-2'); +}); From c4e56aae09da6689cd92744260f1d58d6eddfbfa Mon Sep 17 00:00:00 2001 From: Youssef Date: Fri, 12 Nov 2021 10:49:09 +0100 Subject: [PATCH 08/21] fix: remove record splash and update sql query in add-splash-entry --- .../admin-api/user-splash-controller.ts | 32 ++----------------- src/lib/services/user-splash-service.ts | 2 +- ...211109103930-add-splash-entry-for-users.js | 3 +- 3 files changed, 4 insertions(+), 33 deletions(-) diff --git a/src/lib/routes/admin-api/user-splash-controller.ts b/src/lib/routes/admin-api/user-splash-controller.ts index 68872fad54..8f01b2172e 100644 --- a/src/lib/routes/admin-api/user-splash-controller.ts +++ b/src/lib/routes/admin-api/user-splash-controller.ts @@ -25,34 +25,7 @@ class UserSplashController extends Controller { this.logger = config.getLogger('splash-controller.ts'); this.userSplashService = userSplashService; - this.post('/', this.recordSplash); - this.put('/:id', this.updateSplashSettings); - } - - private async recordSplash( - req: IAuthRequest, - res: Response, - ): Promise { - const BAD_REQUEST = 400; - const { user } = req; - - const { splashId } = req.body; - - if (!splashId) { - res.status(BAD_REQUEST).json({ - error: 'splashId must be present.', - }); - return; - } - - const splash = { - ...req.body, - userId: user.id, - seen: req.body.seen || false, - }; - - const updated = await this.userSplashService.updateSplash(splash); - res.json(updated); + this.post('/:id', this.updateSplashSettings); } private async updateSplashSettings( @@ -66,9 +39,8 @@ class UserSplashController extends Controller { ...req.body, splashId: id, userId: user.id, - seen: req.body.seen || false, + seen: true, }; - const updated = await this.userSplashService.updateSplash(splash); res.json(updated); } diff --git a/src/lib/services/user-splash-service.ts b/src/lib/services/user-splash-service.ts index 65d604ef85..0f609499d5 100644 --- a/src/lib/services/user-splash-service.ts +++ b/src/lib/services/user-splash-service.ts @@ -25,7 +25,7 @@ export default class UserSplashService { return []; } try { - const splashs = await ( + const splashs = ( await this.userSplashStore.getAllUserSplashs(user.id) ).reduce( (splashObject, splash) => ({ diff --git a/src/migrations/20211109103930-add-splash-entry-for-users.js b/src/migrations/20211109103930-add-splash-entry-for-users.js index 8a34421415..455f743e0a 100644 --- a/src/migrations/20211109103930-add-splash-entry-for-users.js +++ b/src/migrations/20211109103930-add-splash-entry-for-users.js @@ -2,8 +2,7 @@ exports.up = function (db, cb) { db.runSql(`SELECT * FROM users`, (err, results) => { results.rows.forEach((user) => { db.runSql( - `INSERT INTO user_splash(splash_id, user_id, seen) VALUES (?, ?, ?)`, - ['environments', user.id, false], + `INSERT INTO user_splash(splash_id, user_id, seen) SELECT 'environment', u.id, false FROM users u`, ); }); cb(); From fc275a6183f6fa88da1b73f0d0c8eea29c69138b Mon Sep 17 00:00:00 2001 From: Youssef Date: Fri, 12 Nov 2021 11:02:12 +0100 Subject: [PATCH 09/21] fix: update sql query in add-splash-entry --- .../20211109103930-add-splash-entry-for-users.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/migrations/20211109103930-add-splash-entry-for-users.js b/src/migrations/20211109103930-add-splash-entry-for-users.js index 455f743e0a..65d70586ea 100644 --- a/src/migrations/20211109103930-add-splash-entry-for-users.js +++ b/src/migrations/20211109103930-add-splash-entry-for-users.js @@ -1,12 +1,8 @@ exports.up = function (db, cb) { - db.runSql(`SELECT * FROM users`, (err, results) => { - results.rows.forEach((user) => { - db.runSql( - `INSERT INTO user_splash(splash_id, user_id, seen) SELECT 'environment', u.id, false FROM users u`, - ); - }); - cb(); - }); + db.runSql( + `INSERT INTO user_splash(splash_id, user_id, seen) SELECT 'environment', u.id, false FROM users u`, + cb, + ); }; exports.down = function (db, cb) { From e0356eff6dde9839060eb276550dd106118f0c9b Mon Sep 17 00:00:00 2001 From: Youssef Date: Fri, 12 Nov 2021 11:18:09 +0100 Subject: [PATCH 10/21] fix: update e2e test for splash --- src/test/e2e/api/admin/splash.e2e.test.ts | 31 +---------------------- 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/src/test/e2e/api/admin/splash.e2e.test.ts b/src/test/e2e/api/admin/splash.e2e.test.ts index 263d7319dd..61d0c6db26 100644 --- a/src/test/e2e/api/admin/splash.e2e.test.ts +++ b/src/test/e2e/api/admin/splash.e2e.test.ts @@ -41,40 +41,11 @@ afterAll(async () => { await db.destroy(); }); -test('it creates splash for user', async () => { - expect.assertions(1); - - return app.request - .post('/api/admin/splash') - .send({ splashId: 'environment' }) - .set('Content-Type', 'application/json') - .expect('Content-Type', /json/) - .expect(200) - .expect((res) => { - expect(res.body.splashId).toBe('environment'); - }); -}); - -test('it gives 400 when splash is not present', async () => { - expect.assertions(1); - - return app.request - .post('/api/admin/splash') - .send({}) - .set('Content-Type', 'application/json') - .expect('Content-Type', /json/) - .expect(400) - .expect((res) => { - expect(res.body.error).toBeTruthy(); - }); -}); - test('it updates splash for user', async () => { expect.assertions(1); return app.request - .put('/api/admin/splash/environment') - .send({ seen: true }) + .post('/api/admin/splash/environment') .set('Content-Type', 'application/json') .expect('Content-Type', /json/) .expect(200) From 0199d1f21154db4b58e1c14105b3605c2a06e76b Mon Sep 17 00:00:00 2001 From: Youssef Date: Fri, 12 Nov 2021 12:44:17 +0100 Subject: [PATCH 11/21] fix: remove req.body from the splash object when update --- src/lib/routes/admin-api/user-splash-controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/routes/admin-api/user-splash-controller.ts b/src/lib/routes/admin-api/user-splash-controller.ts index 8f01b2172e..07f3b7a049 100644 --- a/src/lib/routes/admin-api/user-splash-controller.ts +++ b/src/lib/routes/admin-api/user-splash-controller.ts @@ -36,7 +36,6 @@ class UserSplashController extends Controller { const { id } = req.params; const splash = { - ...req.body, splashId: id, userId: user.id, seen: true, From 16429fcf9a91f18291b3839fb838faf91856fb50 Mon Sep 17 00:00:00 2001 From: Youssef Date: Fri, 12 Nov 2021 13:01:56 +0100 Subject: [PATCH 12/21] fix: add cascade query inside create table for splash --- .../20211108130333-create-user-splash-table.js | 5 +++-- ...20211109123505-add-cascade-for-user-splash.js | 16 ---------------- 2 files changed, 3 insertions(+), 18 deletions(-) delete mode 100644 src/migrations/20211109123505-add-cascade-for-user-splash.js diff --git a/src/migrations/20211108130333-create-user-splash-table.js b/src/migrations/20211108130333-create-user-splash-table.js index 21953415b3..38ea54e3d9 100644 --- a/src/migrations/20211108130333-create-user-splash-table.js +++ b/src/migrations/20211108130333-create-user-splash-table.js @@ -4,11 +4,12 @@ exports.up = function (db, cb) { db.runSql( ` CREATE TABLE IF NOT EXISTS user_splash - (user_id INTEGER NOT NULL references users (id), + (user_id INTEGER NOT NULL references users (id) ON DELETE CASCADE, splash_id TEXT, seen BOOLEAN NOT NULL DEFAULT false, PRIMARY KEY (user_id, splash_id)); - CREATE INDEX user_splash_user_id_idx ON user_splash (user_id);`, + CREATE INDEX user_splash_user_id_idx ON user_splash (user_id); + `, cb, ); }; diff --git a/src/migrations/20211109123505-add-cascade-for-user-splash.js b/src/migrations/20211109123505-add-cascade-for-user-splash.js deleted file mode 100644 index 5f9754c440..0000000000 --- a/src/migrations/20211109123505-add-cascade-for-user-splash.js +++ /dev/null @@ -1,16 +0,0 @@ -exports.up = function (db, cb) { - db.runSql( - ` - ALTER TABLE user_splash DROP CONSTRAINT user_splash_user_id_fkey; - ALTER TABLE user_splash - ADD CONSTRAINT user_splash_user_id_fkey - FOREIGN KEY (user_id) - REFERENCES users(id) ON DELETE CASCADE; -`, - cb, - ); -}; - -exports.down = function (db, cb) { - db.runSql('', cb); -}; From 2d03b8c61e94cad41ac5ff6aa14e735c7d14fe56 Mon Sep 17 00:00:00 2001 From: Youssef Date: Fri, 12 Nov 2021 13:19:06 +0100 Subject: [PATCH 13/21] fix: return be object instead of array --- src/lib/services/user-splash-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/services/user-splash-service.ts b/src/lib/services/user-splash-service.ts index 0f609499d5..7d919c7dce 100644 --- a/src/lib/services/user-splash-service.ts +++ b/src/lib/services/user-splash-service.ts @@ -38,7 +38,7 @@ export default class UserSplashService { } catch (err) { this.logger.error(err); - return []; + return {}; } } From 98cdc1674f160a577b2bea5201c6d7c2a8b45e81 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Mon, 22 Nov 2021 16:16:38 +0100 Subject: [PATCH 14/21] docs: add initial (untested) steps on how to run the proxy locally. I haven't yet tested connecting to the `unleash:4242` address. --- website/docs/user_guide/quickstart.md | 77 ++++++++++++++++++- website/static/img/api_access_navigation.png | Bin 0 -> 83828 bytes 2 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 website/static/img/api_access_navigation.png diff --git a/website/docs/user_guide/quickstart.md b/website/docs/user_guide/quickstart.md index 9fdab31479..9c1d809709 100644 --- a/website/docs/user_guide/quickstart.md +++ b/website/docs/user_guide/quickstart.md @@ -198,7 +198,7 @@ unleash.on('synchronized', () => { ## I want to run Unleash locally -### Run Unleash with Docker +### Run Unleash with Docker {#run-unleash-with-docker} The easiest way to run unleash locally is using [docker](https://www.docker.com/). @@ -214,11 +214,13 @@ docker run -e POSTGRES_PASSWORD=some_password \ 3. Start Unleash via docker: ```sh -docker run -p 4242:4242 \ +docker run --name unleash \ + -p 4242:4242 \ -e DATABASE_HOST=postgres -e DATABASE_NAME=unleash \ -e DATABASE_USERNAME=unleash_user -e DATABASE_PASSWORD=some_password \ -e DATABASE_SSL=false \ - --network unleash unleashorg/unleash-server + --network unleash unleashorg/unleash-server \ + ``` [Click here to see all options to get started locally.](deploy/getting-started.md) @@ -232,6 +234,75 @@ username: admin password: unleash4all ``` +### Run Unleash and the Unleash proxy with Docker + +Follow steps outlined in the [Run Unleash with +Docker](#run-unleash-with-docker) section to get the Unleash instance +up and running. Once you have done that you need to first get an API +key from your Unleash instance and then use that API key when starting +the Unleash proxy. + +1. Get an API key. + + To get an API key, access your Unleash instance in a web browser. + First, navigate to the API access screen. + + ![The Unleash UI showing a dropdown menu under the "Configure" menu + entry. The dropdown menu's "API Access" option is highlighted and + you're told to navigate there.](/img/api_access_navigation.png + "Navigate to the API access page.") + + Next, create an API key with these details + + - **name:** proxy-key (this can be whatever you want) + - **token type:** client + - **project:** all + - **environment:** select your preferred environment (this option is + only available in Unleash 4.3 and later) + + Copy the API key to your clipboard. You'll need it in the next step. + + :::note + + Depending on whether you have the environments feature + enabled or not, the API key will look a little different. If you + don't have environments enabled, it'll just be a 64 character long + hexadecimal string (for instance + `943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0`). + If you do have environments enabled, the key will be prefixed with + the project and the environment that the key is valid for. It'll use the + format `:.`, e.g. + `demo-app:production.614a75cf68bef8703aa1bd8304938a81ec871f86ea40c975468eabd6`. + + Regardless of which format your string uses, do not modify it. + + ::: + + +2. Start the Unleash proxy + + Start a container with the Unleash proxy by running the following + command. Replace `${API_KEY}` with the key you created in the + following step. + + ```sh + docker run --name unleash-proxy \ + -e UNLEASH_PROXY_SECRETS=some-secret \ + -e UNLEASH_URL='http://unleash:4242/api/' \ + -e UNLEASH_API_TOKEN='${API_KEY}' \ + -p 3000:3000 \ + unleashorg/unleash-proxy + ``` + +3. Test the proxy + + To make sure the proxy is running successfully, you can test it by + running the following command: + + ```curl + curl http://localhost:3000/proxy -H "Authorization: some-secret" + ``` + ### Create your first toggle In order to create a toggle through the UI, [you can follow this guide](create-feature-toggle.md). Once you have created your feature toggle, you are ready to connect your application using an SDK. diff --git a/website/static/img/api_access_navigation.png b/website/static/img/api_access_navigation.png new file mode 100644 index 0000000000000000000000000000000000000000..9f83317ebb1e76318db1cb0d36a4093ec7045d6c GIT binary patch literal 83828 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 P6X!X7%$Ynrhr9m+7ex&Q literal 0 HcmV?d00001 From c1e225ea9bc86b6fa4c7ac995a3ea3600cc8a4b7 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 23 Nov 2021 08:08:23 +0100 Subject: [PATCH 15/21] docs: fix up docker commands to have consistent formatting. Moved each option onto its own line, the last two options are always `--network` and then `--name`. --- website/docs/user_guide/quickstart.md | 41 ++++++++++++++++----------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/website/docs/user_guide/quickstart.md b/website/docs/user_guide/quickstart.md index 9c1d809709..29afb2de98 100644 --- a/website/docs/user_guide/quickstart.md +++ b/website/docs/user_guide/quickstart.md @@ -206,21 +206,28 @@ The easiest way to run unleash locally is using [docker](https://www.docker.com/ 2. Start a postgres database: ```sh -docker run -e POSTGRES_PASSWORD=some_password \ - -e POSTGRES_USER=unleash_user -e POSTGRES_DB=unleash \ - --network unleash --name postgres postgres +docker run \ + -e POSTGRES_USER=unleash_user \ + -e POSTGRES_PASSWORD=some_password \ + -e POSTGRES_DB=unleash \ + --network unleash \ + --name postgres \ + postgres ``` 3. Start Unleash via docker: ```sh -docker run --name unleash \ +docker run \ -p 4242:4242 \ - -e DATABASE_HOST=postgres -e DATABASE_NAME=unleash \ - -e DATABASE_USERNAME=unleash_user -e DATABASE_PASSWORD=some_password \ + -e DATABASE_HOST=postgres \ + -e DATABASE_NAME=unleash \ + -e DATABASE_USERNAME=unleash_user \ + -e DATABASE_PASSWORD=some_password \ -e DATABASE_SSL=false \ - --network unleash unleashorg/unleash-server \ - + --network unleash \ + --name unleash \ + unleashorg/unleash-server ``` [Click here to see all options to get started locally.](deploy/getting-started.md) @@ -268,11 +275,11 @@ the Unleash proxy. enabled or not, the API key will look a little different. If you don't have environments enabled, it'll just be a 64 character long hexadecimal string (for instance - `943ca9171e2c884c545c5d82417a655fb77cec970cc3b78a8ff87f4406b495d0`). + `be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178`). If you do have environments enabled, the key will be prefixed with the project and the environment that the key is valid for. It'll use the format `:.`, e.g. - `demo-app:production.614a75cf68bef8703aa1bd8304938a81ec871f86ea40c975468eabd6`. + `demo-app:production.be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178`. Regardless of which format your string uses, do not modify it. @@ -286,12 +293,14 @@ the Unleash proxy. following step. ```sh - docker run --name unleash-proxy \ - -e UNLEASH_PROXY_SECRETS=some-secret \ - -e UNLEASH_URL='http://unleash:4242/api/' \ - -e UNLEASH_API_TOKEN='${API_KEY}' \ - -p 3000:3000 \ - unleashorg/unleash-proxy + docker run \ + -e UNLEASH_PROXY_SECRETS=some-secret \ + -e UNLEASH_URL='http://unleash:4242/api/' \ + -e UNLEASH_API_TOKEN='${API_KEY}' \ + -p 3000:3000 \ + --network unleash \ + --name unleash-proxy \ + unleashorg/unleash-proxy ``` 3. Test the proxy From 7fdb53f6647f8c6b0f7b3e83db6b643b2371c10c Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 23 Nov 2021 09:07:00 +0100 Subject: [PATCH 16/21] Docs: add a tip about docker names having to be unique. --- website/docs/user_guide/quickstart.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/website/docs/user_guide/quickstart.md b/website/docs/user_guide/quickstart.md index 29afb2de98..1d8e08de8a 100644 --- a/website/docs/user_guide/quickstart.md +++ b/website/docs/user_guide/quickstart.md @@ -202,6 +202,15 @@ unleash.on('synchronized', () => { The easiest way to run unleash locally is using [docker](https://www.docker.com/). +:::tip + +Each container that runs in your local Docker instance must have a +unique name. If you've run these commands before, you can either start +the containers again (`docker start ${CONTAINER_NAME}`) or remove them +(`docker rm ${CONTAINER_NAME}`) and run the commands again. + +::: + 1. Create a network by running `docker network create unleash` 2. Start a postgres database: From 6f062e4d66add01f3239184b0db6d807a2ef5ae6 Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Tue, 23 Nov 2021 09:40:05 +0100 Subject: [PATCH 17/21] docs: add always pull to docker commands --- website/docs/user_guide/quickstart.md | 80 ++++++++++----------------- 1 file changed, 29 insertions(+), 51 deletions(-) diff --git a/website/docs/user_guide/quickstart.md b/website/docs/user_guide/quickstart.md index 1d8e08de8a..7c7e8c432d 100644 --- a/website/docs/user_guide/quickstart.md +++ b/website/docs/user_guide/quickstart.md @@ -204,10 +204,7 @@ The easiest way to run unleash locally is using [docker](https://www.docker.com/ :::tip -Each container that runs in your local Docker instance must have a -unique name. If you've run these commands before, you can either start -the containers again (`docker start ${CONTAINER_NAME}`) or remove them -(`docker rm ${CONTAINER_NAME}`) and run the commands again. +Each container that runs in your local Docker instance must have a unique name. If you've run these commands before, you can either start the containers again (`docker start ${CONTAINER_NAME}`) or remove them (`docker rm ${CONTAINER_NAME}`) and run the commands again. ::: @@ -236,7 +233,7 @@ docker run \ -e DATABASE_SSL=false \ --network unleash \ --name unleash \ - unleashorg/unleash-server + --pull=always unleashorg/unleash-server ``` [Click here to see all options to get started locally.](deploy/getting-started.md) @@ -252,74 +249,55 @@ password: unleash4all ### Run Unleash and the Unleash proxy with Docker -Follow steps outlined in the [Run Unleash with -Docker](#run-unleash-with-docker) section to get the Unleash instance -up and running. Once you have done that you need to first get an API -key from your Unleash instance and then use that API key when starting -the Unleash proxy. +Follow steps outlined in the [Run Unleash with Docker](#run-unleash-with-docker) section to get the Unleash instance up and running. Once you have done that you need to first get an API key from your Unleash instance and then use that API key when starting the Unleash proxy. 1. Get an API key. - To get an API key, access your Unleash instance in a web browser. - First, navigate to the API access screen. + To get an API key, access your Unleash instance in a web browser. First, navigate to the API access screen. ![The Unleash UI showing a dropdown menu under the "Configure" menu - entry. The dropdown menu's "API Access" option is highlighted and - you're told to navigate there.](/img/api_access_navigation.png - "Navigate to the API access page.") +entry. The dropdown menu's "API Access" option is highlighted and +you're told to navigate there.](/img/api_access_navigation.png 'Navigate to the API access page.') Next, create an API key with these details - - **name:** proxy-key (this can be whatever you want) - - **token type:** client - - **project:** all - - **environment:** select your preferred environment (this option is - only available in Unleash 4.3 and later) + - **name:** proxy-key (this can be whatever you want) + - **token type:** client + - **project:** all + - **environment:** select your preferred environment (this option is only available in Unleash 4.3 and later) - Copy the API key to your clipboard. You'll need it in the next step. + Copy the API key to your clipboard. You'll need it in the next step. :::note - Depending on whether you have the environments feature - enabled or not, the API key will look a little different. If you - don't have environments enabled, it'll just be a 64 character long - hexadecimal string (for instance - `be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178`). - If you do have environments enabled, the key will be prefixed with - the project and the environment that the key is valid for. It'll use the - format `:.`, e.g. - `demo-app:production.be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178`. + Depending on whether you have the environments feature enabled or not, the API key will look a little different. If you don't have environments enabled, it'll just be a 64 character long hexadecimal string (for instance `be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178`). If you do have environments enabled, the key will be prefixed with the project and the environment that the key is valid for. It'll use the format `:.`, e.g. `demo-app:production.be44368985f7fb3237c584ef86f3d6bdada42ddbd63a019d26955178`. - Regardless of which format your string uses, do not modify it. - - ::: + Regardless of which format your string uses, do not modify it. + ::: 2. Start the Unleash proxy - Start a container with the Unleash proxy by running the following - command. Replace `${API_KEY}` with the key you created in the - following step. + Start a container with the Unleash proxy by running the following command. Replace `${API_KEY}` with the key you created in the following step. - ```sh - docker run \ - -e UNLEASH_PROXY_SECRETS=some-secret \ - -e UNLEASH_URL='http://unleash:4242/api/' \ - -e UNLEASH_API_TOKEN='${API_KEY}' \ - -p 3000:3000 \ - --network unleash \ - --name unleash-proxy \ - unleashorg/unleash-proxy - ``` + ```sh + docker run \ + -e UNLEASH_PROXY_SECRETS=some-secret \ + -e UNLEASH_URL='http://unleash:4242/api/' \ + -e UNLEASH_API_TOKEN='${API_KEY}' \ + -p 3000:3000 \ + --network unleash \ + --name unleash-proxy \ + --pull=always unleashorg/unleash-proxy + ``` 3. Test the proxy - To make sure the proxy is running successfully, you can test it by - running the following command: + To make sure the proxy is running successfully, you can test it by running the following command: - ```curl - curl http://localhost:3000/proxy -H "Authorization: some-secret" - ``` + ```curl + curl http://localhost:3000/proxy -H "Authorization: some-secret" + ``` ### Create your first toggle From a12250aa0a9035aa09fb0f5c21d4800649639fde Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Tue, 23 Nov 2021 09:43:35 +0100 Subject: [PATCH 18/21] docs: add always pull to getting started --- website/docs/deploy/getting-started.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/website/docs/deploy/getting-started.md b/website/docs/deploy/getting-started.md index f369b547b8..6a13a1e2ee 100644 --- a/website/docs/deploy/getting-started.md +++ b/website/docs/deploy/getting-started.md @@ -57,11 +57,13 @@ docker run -p 4242:4242 \ -e DATABASE_HOST=postgres -e DATABASE_NAME=unleash \ -e DATABASE_USERNAME=unleash_user -e DATABASE_PASSWORD=some_password \ -e DATABASE_SSL=false \ - --network unleash unleashorg/unleash-server + --network unleash --pull=always unleashorg/unleash-server ``` ### Option 2 - use Docker-compose {#option-two---use-docker-compose} + **Steps:** + 1. Clone the [unleash-docker](https://github.com/Unleash/unleash-docker) repository. 2. Run `docker-compose build` in repository root folder. 3. Run `docker-compose up` in repository root folder. @@ -95,7 +97,7 @@ docker run -p 4242:4242 \ port: 4242, }, }) - .then(unleash => { + .then((unleash) => { console.log( `Unleash started on http://localhost:${unleash.app.get('port')}`, ); From 99a2bae0f9067629f0fffe9b0547c2a1245eb102 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 23 Nov 2021 13:02:54 +0100 Subject: [PATCH 19/21] chore(deps): update dependency eslint-config-airbnb-typescript to v16 (#1109) Co-authored-by: Renovate Bot --- package.json | 2 +- yarn.lock | 59 ++++++++-------------------------------------------- 2 files changed, 10 insertions(+), 51 deletions(-) diff --git a/package.json b/package.json index 2fa0cbcb71..c794de6dad 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "del-cli": "4.0.1", "eslint": "8.3.0", "eslint-config-airbnb-base": "15.0.0", - "eslint-config-airbnb-typescript": "15.0.0", + "eslint-config-airbnb-typescript": "16.0.0", "eslint-config-prettier": "8.3.0", "eslint-plugin-import": "2.25.3", "eslint-plugin-prettier": "4.0.0", diff --git a/yarn.lock b/yarn.lock index 31430e8333..5c7323f75f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2335,29 +2335,6 @@ errorhandler@^1.5.1: accepts "~1.3.7" escape-html "~1.0.3" -es-abstract@^1.18.2: - version "1.18.5" - resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz" - integrity sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA== - dependencies: - call-bind "^1.0.2" - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - get-intrinsic "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.2" - internal-slot "^1.0.3" - is-callable "^1.2.3" - is-negative-zero "^2.0.1" - is-regex "^1.1.3" - is-string "^1.0.6" - object-inspect "^1.11.0" - object-keys "^1.1.1" - object.assign "^4.1.2" - string.prototype.trimend "^1.0.4" - string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.1" - es-abstract@^1.19.0, es-abstract@^1.19.1: version "1.19.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" @@ -2466,7 +2443,7 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -eslint-config-airbnb-base@15.0.0: +eslint-config-airbnb-base@15.0.0, eslint-config-airbnb-base@^15.0.0: version "15.0.0" resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz#6b09add90ac79c2f8d723a2580e07f3925afd236" integrity sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig== @@ -2476,21 +2453,12 @@ eslint-config-airbnb-base@15.0.0: object.entries "^1.1.5" semver "^6.3.0" -eslint-config-airbnb-base@^14.2.1: - version "14.2.1" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz#8a2eb38455dc5a312550193b319cdaeef042cd1e" - integrity sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA== +eslint-config-airbnb-typescript@16.0.0: + version "16.0.0" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-16.0.0.tgz#75007e27d5a7fb75530721f48197817c1d2ad4d1" + integrity sha512-qDOyD0YYZo5Us1YvOnWig2Ly/+IlQKmMZpnqKnJgVtHdK8SkjaSyVBHKbD41dEaQxk8vRVGBC94PuR2ceSwbLQ== dependencies: - confusing-browser-globals "^1.0.10" - object.assign "^4.1.2" - object.entries "^1.1.2" - -eslint-config-airbnb-typescript@15.0.0: - version "15.0.0" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-15.0.0.tgz#c88007b3cca5dd0f47125420ca5e8f6efac418fd" - integrity sha512-DTWGwqytbTnB8kSKtmkrGkRf3xwTs2l15shSH0w/3Img47AQwCCrIA/ON/Uj0XXBxP31LHyEItPXeuH3mqCNLA== - dependencies: - eslint-config-airbnb-base "^14.2.1" + eslint-config-airbnb-base "^15.0.0" eslint-config-prettier@8.3.0: version "8.3.0" @@ -3635,7 +3603,7 @@ is-buffer@^1.1.5: resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-callable@^1.1.4, is-callable@^1.2.3, is-callable@^1.2.4: +is-callable@^1.1.4, is-callable@^1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz" integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== @@ -3824,7 +3792,7 @@ is-promise@^2.2.2: resolved "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz" integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== -is-regex@^1.1.3, is-regex@^1.1.4: +is-regex@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== @@ -3854,7 +3822,7 @@ is-stream@^2.0.0: resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== -is-string@^1.0.5, is-string@^1.0.6, is-string@^1.0.7: +is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz" integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== @@ -5274,15 +5242,6 @@ object.defaults@^1.1.0: for-own "^1.0.0" isobject "^3.0.0" -object.entries@^1.1.2: - version "1.1.4" - resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.4.tgz" - integrity sha512-h4LWKWE+wKQGhtMjZEBud7uLGhqyLwj8fpHOarZhD2uY3C9cRtk57VQ89ke3moByLXMedqs3XCHzyb4AmA2DjA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.18.2" - object.entries@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861" From 7056d516b492078eb94473544b423b7c07d74798 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 23 Nov 2021 14:18:35 +0100 Subject: [PATCH 20/21] fix: image inclusion and alt text syntax was wrong. Turns out the image description needed to be wrapped in an extra pair of square brackets and that quotes must be escaped or removed. --- website/docs/user_guide/quickstart.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/docs/user_guide/quickstart.md b/website/docs/user_guide/quickstart.md index 7c7e8c432d..a975312a3b 100644 --- a/website/docs/user_guide/quickstart.md +++ b/website/docs/user_guide/quickstart.md @@ -255,9 +255,9 @@ Follow steps outlined in the [Run Unleash with Docker](#run-unleash-with-docker) To get an API key, access your Unleash instance in a web browser. First, navigate to the API access screen. - ![The Unleash UI showing a dropdown menu under the "Configure" menu -entry. The dropdown menu's "API Access" option is highlighted and -you're told to navigate there.](/img/api_access_navigation.png 'Navigate to the API access page.') + [![The Unleash UI showing a dropdown menu under the Configure menu +entry. The dropdown menu's API Access option is highlighted and +you're told to navigate there.]](/img/api_access_navigation.png 'Navigate to the API access page.') Next, create an API key with these details From 1848d8d4ebaa4400c2ac638923b3351562f7379c Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Tue, 23 Nov 2021 15:19:59 +0100 Subject: [PATCH 21/21] task: add a workflow that validates docs for PRs (#1123) --- .github/workflows/build_doc_prs.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/build_doc_prs.yaml diff --git a/.github/workflows/build_doc_prs.yaml b/.github/workflows/build_doc_prs.yaml new file mode 100644 index 0000000000..c332f67003 --- /dev/null +++ b/.github/workflows/build_doc_prs.yaml @@ -0,0 +1,17 @@ +name: PR -> Build Docs + +on: + pull_request: + paths: + - website/** + +jobs: + validate: + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: setup git config + run: | + # Build the site + cd website && yarn && yarn build