diff --git a/api/src/app.module.ts b/api/src/app.module.ts index e938cc903..fbc83d5c4 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -10,6 +10,7 @@ import { CachingService } from './caching/caching.service'; import { ComponentModule } from './component/component.module'; import { EnvironmentModule } from './environment/environment.module'; import { ErrorsModule } from './errors/errors.module'; +import { GithubApiModule } from './github-api/github-api.module'; import { AppLoggerMiddleware } from './middleware/logger.middle'; import { OperationsModule } from './operations/operations.module'; import { OrganizationModule } from './organization/organization.module'; @@ -45,6 +46,7 @@ import { UsersModule } from './users/users.module'; StreamModule, CachingModule, ErrorsModule, + GithubApiModule, ], controllers: [], providers: [CachingService], diff --git a/api/src/config.ts b/api/src/config.ts index e1f2a165f..18e81eaa0 100644 --- a/api/src/config.ts +++ b/api/src/config.ts @@ -9,6 +9,10 @@ export type ApiConfig = { database: string; }; port: number; + github: { + owner: string, + repo: string, + } AWS: { accessKeyId: string; secretAccessKey: string; @@ -62,6 +66,10 @@ export function init() { database: getEnvVarOrFail('TYPEORM_DATABASE') }, port: parseInt(process.env.APP_PORT) || 3000, + github: { + owner: getEnvVarOrDefault('GIT_OWNER', 'zlab-tech'), + repo: getEnvVarOrDefault('GIT_REPO', 'hooli-config'), + }, AWS: { accessKeyId: getEnvVarOrFail('AWS_ACCESS_KEY_ID'), secretAccessKey: getEnvVarOrFail('AWS_SECRET_ACCESS_KEY'), diff --git a/api/src/environment/environment.module.ts b/api/src/environment/environment.module.ts index bd4d3ea3d..da5b065fe 100644 --- a/api/src/environment/environment.module.ts +++ b/api/src/environment/environment.module.ts @@ -36,8 +36,9 @@ import { SystemService } from 'src/system/system.service'; EnvironmentService, TeamService, ReconciliationService, - SystemService + SystemService, ], + exports: [EnvironmentService], }) export class EnvironmentModule implements NestModule { configure(consumer: MiddlewareConsumer) { diff --git a/api/src/github-api/github-api.controller.spec.ts b/api/src/github-api/github-api.controller.spec.ts new file mode 100644 index 000000000..8ffcfcea9 --- /dev/null +++ b/api/src/github-api/github-api.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { GithubApiController } from './github-api.controller'; + +describe('GithubApiController', () => { + let controller: GithubApiController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [GithubApiController], + }).compile(); + + controller = module.get(GithubApiController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/api/src/github-api/github-api.controller.ts b/api/src/github-api/github-api.controller.ts new file mode 100644 index 000000000..badfe65fb --- /dev/null +++ b/api/src/github-api/github-api.controller.ts @@ -0,0 +1,27 @@ +import { Controller, Post, Request } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { EnvironmentService } from 'src/environment/environment.service'; +import { APIRequest, EnvironmentApiParam } from 'src/types'; +import { GithubApiService } from './github-api.service'; +import { get } from 'src/config'; + +@Controller({ + version: '1', +}) +@ApiTags('github-api') +export class GithubApiController { + constructor( + private readonly envSvc: EnvironmentService, + private readonly gitSvc: GithubApiService + ) {} + + @Post('/:environmentId') + @EnvironmentApiParam() + async gitCommit(@Request() req: APIRequest) { + const { org, team, env } = req; + const environment = await this.envSvc.findById(org, env.id); + if (environment) { + return this.gitSvc.gitCommit(org, get().github.owner, get().github.repo, `environments/demo/env.yaml`); + } + } +} diff --git a/api/src/github-api/github-api.module.ts b/api/src/github-api/github-api.module.ts new file mode 100644 index 000000000..637ce92b3 --- /dev/null +++ b/api/src/github-api/github-api.module.ts @@ -0,0 +1,22 @@ +import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common'; +import { GithubApiController } from './github-api.controller'; +import { EnvironmentService } from 'src/environment/environment.service'; +import { EnvironmentMiddleware } from 'src/middleware/environment.middle'; +import { GithubApiService } from './github-api.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Environment } from 'src/typeorm/environment.entity'; +import { SecretsService } from 'src/secrets/secrets.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([Environment])], + controllers: [GithubApiController], + providers: [EnvironmentService, GithubApiService, SecretsService], +}) +export class GithubApiModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(EnvironmentMiddleware).forRoutes({ + path: '*/github/:environmentId*', + method: RequestMethod.ALL, + }); + } +} diff --git a/api/src/github-api/github-api.service.ts b/api/src/github-api/github-api.service.ts new file mode 100644 index 000000000..41f8951b5 --- /dev/null +++ b/api/src/github-api/github-api.service.ts @@ -0,0 +1,77 @@ +import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import axios from 'axios'; +import { get } from 'src/config'; +import { SecretsService } from 'src/secrets/secrets.service'; +import { Organization } from 'src/typeorm'; + +@Injectable() +export class GithubApiService { + private readonly baseUri: string = 'https://api.github.com/repos'; + private readonly headers = async (org: Organization) => ({ + Accept: 'application/vnd.github+json', + Authorization: `Bearer ${await this.getGITPAT(org)}`, + 'X-GitHub-Api-Version': '2022-11-28', + }); + + constructor(private readonly secretSvc: SecretsService){} + + private getURL(owner: string, repo: string, filePath: string) { + return `${this.baseUri}/${owner}/${repo}/contents/${filePath}`; + } + + private async getFileSHA(org: Organization, owner: string, repo: string, filePath: string) { + const url = this.getURL(owner, repo, filePath); + const { data } = await axios.get<{ sha: string; content: string }>(url, { + headers: await this.headers(org), + }); + return data; + } + + public setAlternateFlag(yamlString: string) { + const decodedYaml = Buffer.from(yamlString, 'base64').toString(); + console.log(decodedYaml); + if (decodedYaml.includes('teardown: true')) { + return Buffer.from( + decodedYaml.replace('teardown: true', 'teardown: false') + ).toString('base64'); + } + return Buffer.from( + decodedYaml.replace('teardown: false', 'teardown: true') + ).toString('base64'); + } + + public async gitCommit(org: Organization, owner: string, repo: string, filePath: string) { + const { sha, content } = await this.getFileSHA(org, owner, repo, filePath); + const payload = { + message: 'api testing for playground ', + committer: { name: 'playground', email: 'playground@cloudknit.io' }, + content: this.setAlternateFlag(content), + sha, + }; + + try { + const { data } = await axios.put( + this.getURL(owner, repo, filePath), + payload, + { + headers: await this.headers(org), + } + ); + + const { commit } = data; + const { html_url } = commit; + return { + status: 'success', + html_url, + }; + } catch (err) { + throw new InternalServerErrorException( + 'There was an error while pushing the commit to git' + ); + } + } + + private async getGITPAT(org: Organization) { + return await this.secretSvc.getSsmSecret(org, 'playground-git-token'); + } +} diff --git a/api/src/organization/organization.module.ts b/api/src/organization/organization.module.ts index d667295e3..fba577ed3 100644 --- a/api/src/organization/organization.module.ts +++ b/api/src/organization/organization.module.ts @@ -15,6 +15,7 @@ import { OrganizationService } from './organization.service'; imports: [TypeOrmModule.forFeature([Organization, User])], controllers: [OrganizationController], providers: [OrganizationService, UsersService], + exports: [OrganizationService], }) export class OrganizationModule implements NestModule { configure(consumer: MiddlewareConsumer) { diff --git a/api/src/routes.ts b/api/src/routes.ts index 134606ed4..6a28ea8b8 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -11,6 +11,7 @@ import { EnvironmentModule } from './environment/environment.module'; import { ComponentModule } from './component/component.module'; import { StreamModule } from './stream/stream.module'; import { ErrorsModule } from './errors/errors.module'; +import { GithubApiModule } from './github-api/github-api.module'; export const appRoutes: Routes = [ { @@ -58,10 +59,15 @@ export const appRoutes: Routes = [ module: ComponentModule, }, ], - }, { + }, + { + path: '/:teamId/github', + module: GithubApiModule, + }, + { path: '/:teamId/errors', - module: ErrorsModule - } + module: ErrorsModule, + }, ], }, ], diff --git a/api/src/secrets/secrets.module.ts b/api/src/secrets/secrets.module.ts index d2ef46dfd..1afbc9a1a 100644 --- a/api/src/secrets/secrets.module.ts +++ b/api/src/secrets/secrets.module.ts @@ -5,5 +5,6 @@ import { SecretsService } from './secrets.service'; @Module({ controllers: [SecretsController], providers: [SecretsService], + exports: [SecretsService] }) export class SecretsModule {} diff --git a/api/src/typeorm/User.entity.ts b/api/src/typeorm/User.entity.ts index def3de8e1..b0be4500a 100644 --- a/api/src/typeorm/User.entity.ts +++ b/api/src/typeorm/User.entity.ts @@ -8,6 +8,7 @@ import { UpdateDateColumn, } from 'typeorm'; import { Organization } from './Organization.entity'; +import { UserRole } from 'src/types'; @Entity({ name: 'users' }) export class User { @@ -35,13 +36,19 @@ export class User { @Column({ default: 'User', }) - role: string; + role: UserRole; @Column({ default: false, }) archived: boolean; + @Column({ + default: null, + unique: true + }) + ipv4: string; + @ManyToMany(() => Organization, (org) => org.users) organizations: Organization[]; diff --git a/api/src/typeorm/migrations/1683899743907-UserIpForPlayground.js b/api/src/typeorm/migrations/1683899743907-UserIpForPlayground.js new file mode 100644 index 000000000..d24c3b76b --- /dev/null +++ b/api/src/typeorm/migrations/1683899743907-UserIpForPlayground.js @@ -0,0 +1,9 @@ +module.exports = class UserIpForPlayground1683899743907 { + async up(queryRunner) { + await queryRunner.query( + 'ALTER TABLE `USERS` ADD COLUMN `ipv4` varchar(255) UNIQUE default null' + ); + } + + async down(queryRunner) {} +}; diff --git a/api/src/types.ts b/api/src/types.ts index 89c1379d9..1149079d4 100644 --- a/api/src/types.ts +++ b/api/src/types.ts @@ -5,6 +5,12 @@ import { Request } from 'express'; import { ComponentReconcile, Environment, EnvironmentReconcile, Team } from './typeorm'; import { Organization } from './typeorm/Organization.entity'; +export enum UserRole { + ADMIN = 'Admin', + USER = 'User', + GUEST = 'Guest' +} + export type APIRequest = Request & { org: Organization; team: Team; diff --git a/api/src/users/User.dto.ts b/api/src/users/User.dto.ts index 3367539d9..6725777e0 100644 --- a/api/src/users/User.dto.ts +++ b/api/src/users/User.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; +import { UserRole } from 'src/types'; export class CreateUserDto { @ApiProperty() @@ -8,14 +9,19 @@ export class CreateUserDto { email: string; @ApiProperty({ - default: 'User', + default: UserRole.USER, }) - role: string; + role: UserRole; @ApiProperty() name: string; } +export class CreatePlaygroundUserDto { + @ApiProperty() + ipv4: string; +} + export class PatchUserDto { @ApiProperty({ default: null, diff --git a/api/src/users/users.controller.ts b/api/src/users/users.controller.ts index 4b377d4e2..fcd591014 100644 --- a/api/src/users/users.controller.ts +++ b/api/src/users/users.controller.ts @@ -2,13 +2,11 @@ import { Body, Controller, Get, - Logger, NotFoundException, Param, - Post, + Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { AuthController } from 'src/auth/auth.controller'; import { User } from 'src/typeorm/User.entity'; import { CreateUserDto } from './User.dto'; import { UsersService } from './users.service'; @@ -18,12 +16,12 @@ import { UsersService } from './users.service'; }) @ApiTags('users') export class UsersController { - private readonly logger = new Logger(AuthController.name); constructor(private readonly userService: UsersService) {} @Get('/:username') public async getUser(@Param('username') username: string): Promise { + console.log(username); const user = await this.userService.getUser(username); if (!user) { @@ -35,6 +33,7 @@ export class UsersController { @Post() public async createUser(@Body() body: CreateUserDto): Promise { + console.log(body); const user = await this.userService.create(body); return user; diff --git a/api/src/users/users.module.ts b/api/src/users/users.module.ts index f05e099bc..e30886a18 100644 --- a/api/src/users/users.module.ts +++ b/api/src/users/users.module.ts @@ -3,9 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from 'src/typeorm/User.entity'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; +import { Organization } from 'src/typeorm'; @Module({ - imports: [TypeOrmModule.forFeature([User])], + imports: [TypeOrmModule.forFeature([User, Organization])], controllers: [UsersController], providers: [UsersService], exports: [UsersService], diff --git a/api/src/users/users.service.ts b/api/src/users/users.service.ts index 0e29037af..1e484829a 100644 --- a/api/src/users/users.service.ts +++ b/api/src/users/users.service.ts @@ -1,6 +1,12 @@ -import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { User } from 'src/typeorm'; +import { Organization, User } from 'src/typeorm'; +import { UserRole } from 'src/types'; import { Repository } from 'typeorm'; import { CreateUserDto } from './User.dto'; @@ -8,15 +14,27 @@ import { CreateUserDto } from './User.dto'; export class UsersService { private readonly logger = new Logger(UsersService.name); - constructor(@InjectRepository(User) private userRepo: Repository) {} + constructor( + @InjectRepository(User) private userRepo: Repository, + @InjectRepository(Organization) private orgRepo: Repository + ) {} async getUser(username: string): Promise { - return this.userRepo.findOne({ + const user = await this.userRepo.findOne({ where: { username }, relations: { organizations: true, }, }); + + if (!user) { + return null; + } + + if (user.role === UserRole.GUEST) { + return this.associateOrganization(user); + } + return user; } async getUserById(userId: number): Promise { @@ -47,8 +65,41 @@ export class UsersService { const user = await this.userRepo.save(newUser); + console.log(user); + + if (user.role === UserRole.GUEST) { + return this.associateOrganization(user); + } + this.logger.log('created user', { user: userDto }); return user; } + + private async associateOrganization(user: User) { + if (user.organizations.length === 0) { + const org = await this.getOrganizationWithoutUserAssociation(); + const updatedUser = this.userRepo.merge(user, { + organizations: [org] + }); + user = await this.userRepo.save(updatedUser); + } + return user; + } + + private async getOrganizationWithoutUserAssociation() { + const orgs = await this.orgRepo.find({ + relations: { + users: true, + }, + }); + + const org = orgs.find((org) => !org.users.some(user => user.role === UserRole.GUEST)); + + if (!org) { + throw new NotFoundException('No Organization is present at the moment.'); + } + + return org; + } } diff --git a/bff/src/auth/auth.ts b/bff/src/auth/auth.ts index 077c76327..88b58e1c0 100644 --- a/bff/src/auth/auth.ts +++ b/bff/src/auth/auth.ts @@ -13,7 +13,7 @@ import * as express from "express"; import { auth, requiresAuth } from "express-openid-connect"; import * as session from "express-session"; import { ExpressOIDC } from "@okta/oidc-middleware"; -import * as crypto from 'crypto'; +import * as crypto from "crypto"; export async function getUser(username: string): Promise { try { @@ -41,8 +41,9 @@ async function createUser( name: string ): Promise { try { + console.log(role); const user = await axios.post( - `${process.env.ZLIFECYCLE_API_URL}/v1/users/`, + `${process.env.ZLIFECYCLE_API_URL}/v1/users`, { username, email, @@ -118,7 +119,7 @@ export function getAuth0Config() { // @ts-ignore claims.nickname, claims.email, - "Admin", + getNewUserRole(), claims.name ); @@ -131,7 +132,7 @@ export function getAuth0Config() { return { ...session, user, - organizations: [], + organizations: user.organizations || [], }; } catch (err) { logger.error(`could not create user ${claims.nickname}`, { @@ -210,7 +211,7 @@ function getOktaAuthMW() { // @ts-ignore userInfo.preferred_username, userInfo.email, - "Admin", + getNewUserRole(), userInfo.name ); @@ -284,3 +285,20 @@ export function setUpAuth(app: express.Express, authRouter: express.Router) { authRouter.use(requiresAuth()); } } + +function getClientIP(req) { + if (!req.headers["x-forwarded-for"]) { + return null; + } + + // x-forwarded-for header returns the list of ips that our request has been forwarded by, + // first being clients and then it depends on the no. of proxies that have forwarded it. + const addresses = req.headers["x-forwarded-for"]; + + // Getting the first ip since that is where the request has originated from. + return addresses.split(",")[0]; +} + +function getNewUserRole() { + return helper.isGuestAuth() ? 'Guest' : 'Admin' +} diff --git a/bff/src/config.ts b/bff/src/config.ts index f2ee6aff6..ab9d2919b 100644 --- a/bff/src/config.ts +++ b/bff/src/config.ts @@ -10,6 +10,7 @@ const config = { WEB_URL: process.env.SITE_URL, API_URL: `${process.env.ZLIFECYCLE_API_URL}/v1`, ARGOCD_URL: process.env.ARGO_CD_API_URL, + PLAYGROUND_APP: true, //process.env.CK_PLAYGROUND == 'true', argoWFUrl: (orgName: string) => process.env.ARGO_WORKFLOW_API_URL.replaceAll(":org", orgName), stateMgrUrl: (orgName: string) => diff --git a/bff/src/index.ts b/bff/src/index.ts index 5f95dd32b..096c74bf6 100644 --- a/bff/src/index.ts +++ b/bff/src/index.ts @@ -4,11 +4,19 @@ import * as cookieParser from "cookie-parser"; import * as cors from "cors"; import * as express from "express"; import * as correlator from "express-correlation-id"; -import { apiAuthMw, getUser, organizationMW, setUpAuth } from "./auth/auth"; +import { + apiAuthMw, + getUser, + organizationMW, + setUpAuth +} from "./auth/auth"; import zlConfig from "./config"; import AuthRoutes from "./controllers/auth.controller"; import { - externalApiRoutes, handlePublicRoutes, noOrgRoutes, orgRoutes + externalApiRoutes, + handlePublicRoutes, + noOrgRoutes, + orgRoutes } from "./proxy/proxy"; import helper, { oidcUser } from "./utils/helper"; import logger, { AuthRequestLogger, ErrorLogger } from "./utils/logger"; @@ -70,7 +78,7 @@ authRouter.use(AuthRequestLogger); authRouter.use(organizationMW); // checks for selectedOrg cookie, throws 401 if not present app.use("/auth", AuthRoutes(authRouter)); -app.use("/", orgRoutes(authRouter)); +app.use("/", orgRoutes(authRouter)) // replaces expresses default error handler app.use(ErrorLogger); diff --git a/bff/src/proxy/pathMappings.ts b/bff/src/proxy/pathMappings.ts index 4728bd865..bd3070ce7 100644 --- a/bff/src/proxy/pathMappings.ts +++ b/bff/src/proxy/pathMappings.ts @@ -196,6 +196,10 @@ const API_PATH_MAPPINGS = [ path: "/api/teams/:teamId/environments/:envId", newPath: (params: any) => `v1/orgs/${params.orgId}/teams/${params.teamId}/environments/${params.envId}`, }, + { + path: "/api/teams/:teamId/gitCommit/:envId", + newPath: (params: any) => `v1/orgs/${params.orgId}/teams/${params.teamId}/github/${params.envId}`, + }, { path: "/api/teams/:teamId/environments/:envId/components", newPath: (params: any) => `v1/orgs/${params.orgId}/teams/${params.teamId}/environments/${params.envId}/components`, diff --git a/bff/src/proxy/proxy.ts b/bff/src/proxy/proxy.ts index 73b8fe54e..f925cd3b7 100644 --- a/bff/src/proxy/proxy.ts +++ b/bff/src/proxy/proxy.ts @@ -352,7 +352,7 @@ export function noOrgRoutes(router: express.Router) { export function orgRoutes(router: express.Router) { router.use("/wf", async (req: BFFRequest, res, next) => { - const org = await helper.orgFromReq(req); + const org = await helper.orgFromReq(req, true); if (!org) { helper.handleNoOrg(res); @@ -370,8 +370,7 @@ export function orgRoutes(router: express.Router) { )(req, res, next); }); - router.use( - "/cd", + router.use("/cd", async (req: BFFRequest, res, next) => { /* Since http-proxy-middleware's are cached we need a way to inject ArgoCD tokens @@ -380,7 +379,7 @@ export function orgRoutes(router: express.Router) { Here, we set the `authorization` header and get a valid ArgoCD token on each call. */ - const org = await helper.orgFromReq(req); + const org = await helper.orgFromReq(req, true); if (!org) { helper.handleNoOrg(res); @@ -417,7 +416,7 @@ export function orgRoutes(router: express.Router) { ); router.use("/reconciliation", async (req: BFFRequest, res, next) => { - const org = await helper.orgFromReq(req); + const org = await helper.orgFromReq(req, true); if (!org) { helper.handleNoOrg(res); @@ -461,7 +460,7 @@ export function orgRoutes(router: express.Router) { }); router.use("/api", async (req: BFFRequest, res, next) => { - const org = await helper.orgFromReq(req); + const org = await helper.orgFromReq(req, true); if (!org) { helper.handleNoOrg(res); diff --git a/bff/src/utils/helper.ts b/bff/src/utils/helper.ts index 9df7b9ebf..05c72253b 100644 --- a/bff/src/utils/helper.ts +++ b/bff/src/utils/helper.ts @@ -8,11 +8,17 @@ import logger from "../utils/logger"; import { getArgoCDAuthHeader } from "../auth/argo"; import ckConfig from "../config"; -const orgFromReq = async (req: BFFRequest): Promise => { +const orgFromReq = async (req: BFFRequest, forGuestUser = false): Promise => { if (!req.cookies[config.SELECTED_ORG_HEADER]) { return; } + const currentUser = userFromReq(req); + + if (currentUser?.role === 'Guest' && !forGuestUser) { + return null; + } + const orgName = req.cookies[config.SELECTED_ORG_HEADER]; const session = appSession(req); @@ -152,6 +158,8 @@ const isOktaAuth = () => ckConfig.AUTH0_ISSUER_BASE_URL.includes("oktapreview.com") || ckConfig.AUTH0_ISSUER_BASE_URL.includes("okta.com"); +const isGuestAuth = () => config.PLAYGROUND_APP; //ckConfig.PLAYGROUND_APP === "true"; + export const appSession = (req: BFFRequest): any => { if (isOktaAuth()) { return req.session.appSession; @@ -175,5 +183,6 @@ export default { getOrg, syncWatcher, appSessionFromReq, - isOktaAuth + isOktaAuth, + isGuestAuth }; diff --git a/helm-charts/api/templates/deployment.yaml b/helm-charts/api/templates/deployment.yaml index 4a2b8ed38..7d2330a79 100644 --- a/helm-charts/api/templates/deployment.yaml +++ b/helm-charts/api/templates/deployment.yaml @@ -48,6 +48,12 @@ spec: value: {{ .Values.awsCredentials.sessionToken}} - name: AWS_REGION value: {{ .Values.awsCredentials.region}} + - name: GIT_PERSONAL_ACCESS_TOKEN + value: {{ .Values.github.personalAccessToken}} + - name: GIT_OWNER + value: {{ .Values.github.owner}} + - name: GIT_REPO + value: {{ .Values.github.repo}} image: {{ .Values.image.repository }}/{{ .Values.image.name }}:{{ .Values.image.tag }} ports: - containerPort: 3000 diff --git a/helm-charts/api/values.yaml b/helm-charts/api/values.yaml index ae0d114be..b7099fd63 100644 --- a/helm-charts/api/values.yaml +++ b/helm-charts/api/values.yaml @@ -3,6 +3,10 @@ image: tag: latest repository: replicas: 1 +github: + personalAccessToken: "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" + owner: + repo: database: host: username: diff --git a/helm-charts/bff/Chart.yaml b/helm-charts/bff/Chart.yaml index e593a6b30..be399ba0f 100644 --- a/helm-charts/bff/Chart.yaml +++ b/helm-charts/bff/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v1 name: zlifecycle-web-bff description: A Helm chart for Kubernetes type: application -version: 0.35.0 +version: 0.36.0 appVersion: "1.16.0" diff --git a/helm-charts/bff/templates/deployment.yaml b/helm-charts/bff/templates/deployment.yaml index f11b67fce..88e6e38a2 100644 --- a/helm-charts/bff/templates/deployment.yaml +++ b/helm-charts/bff/templates/deployment.yaml @@ -27,6 +27,8 @@ spec: value: {{ .Values.domain | quote }} - name: COOKIE_SECRET value: "test" + - name: CK_PLAYGROUND + value: {{ .Values.playground }} - name: ARGO_WORKFLOW_API_URL value: {{ .Values.argoWorkflowApiUrl | quote }} - name: ARGO_CD_API_URL diff --git a/helm-charts/bff/values.yaml b/helm-charts/bff/values.yaml index 965bb7aa3..72543b4c2 100644 --- a/helm-charts/bff/values.yaml +++ b/helm-charts/bff/values.yaml @@ -29,6 +29,7 @@ ingressPaths: - "/api" - "/ext" protocol: +playground: true auth0: issuerBaseUrl: web: diff --git a/helm-charts/web/Chart.yaml b/helm-charts/web/Chart.yaml index fd1b723f2..661698d69 100644 --- a/helm-charts/web/Chart.yaml +++ b/helm-charts/web/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v1 name: zlifecycle-web description: A Helm chart for Kubernetes type: application -version: 0.8.0 +version: 0.9.0 appVersion: "1.16.0" diff --git a/helm-charts/web/templates/deployment.yaml b/helm-charts/web/templates/deployment.yaml index 9903cbb1d..b52557b78 100644 --- a/helm-charts/web/templates/deployment.yaml +++ b/helm-charts/web/templates/deployment.yaml @@ -21,6 +21,8 @@ spec: value: "3000" - name: HOST value: {{ .Values.domain }} + - name: PLAYGROUND_APP + value: {{ .Values.playground }} - name: REACT_APP_STREAM_URL value: {{ .Values.bff.urlWithProtocol }} - name: REACT_APP_BASE_URL diff --git a/helm-charts/web/values.yaml b/helm-charts/web/values.yaml index aeb3f2948..1d47a3dd5 100644 --- a/helm-charts/web/values.yaml +++ b/helm-charts/web/values.yaml @@ -8,6 +8,7 @@ domain: zcustomer.zlifecycle.com environment: enabledFeatureFlags: adminDomain: zcustomer-admin.zlifecycle.com +playground: true replicas: 1 bff: urlWithProtocol: diff --git a/web/src/components/argo-core/top-bar/top-bar.tsx b/web/src/components/argo-core/top-bar/top-bar.tsx index 04abd7ef5..0969a0265 100644 --- a/web/src/components/argo-core/top-bar/top-bar.tsx +++ b/web/src/components/argo-core/top-bar/top-bar.tsx @@ -1,11 +1,10 @@ import { ReactComponent as Logo } from 'assets/images/icons/logo.svg'; -import { ZText } from 'components/atoms/text/Text'; -import React, { useState } from 'react'; import AuthStore from 'auth/AuthStore'; import { ZDropdownMenuJSX } from 'components/molecules/dropdown-menu/DropdownMenu'; -import { NavItem } from 'models/nav-item.models'; import { TopNav } from 'components/organisms/top-nav/TopNav'; -import { BradAdarshFeatureVisible, FeatureKeys, FeatureRoutes } from 'pages/authorized/feature_toggle'; +import { NavItem } from 'models/nav-item.models'; +import { BradAdarshFeatureVisible, FeatureKeys, FeatureRoutes, playgroundFeatureVisible } from 'pages/authorized/feature_toggle'; +import React, { useState } from 'react'; import { useHistory } from 'react-router-dom'; require('./top-bar.scss'); @@ -56,12 +55,20 @@ const navItems: NavItem[] = [ }, ], }, - { title: 'Infra Components', path: '/all/all' }, - { title: 'Overview', path: '/overview', visible: () => BradAdarshFeatureVisible()}, + { + title: 'Infra Components', + path: '/all/all', + visible: () => playgroundFeatureVisible(), + }, + { title: 'Overview', path: '/overview', visible: () => BradAdarshFeatureVisible() }, { title: 'Dashboard', path: '/demo-dashboard', visible: () => BradAdarshFeatureVisible() }, { title: 'Builder', path: '/builder', visible: () => BradAdarshFeatureVisible() }, - { title: 'Settings', path: '/settings', visible: () => AuthStore.getUser()?.role === 'Admin' }, - { title: 'Quick Start', path: '/quick-start'}, + { + title: 'Settings', + path: '/settings', + visible: () => BradAdarshFeatureVisible(), + }, + { title: 'Quick Start', path: '/quick-start', visible: () => playgroundFeatureVisible() }, ]; export const TopBar = ({ title }: TopBarProps) => { @@ -73,9 +80,13 @@ export const TopBar = ({ title }: TopBarProps) => {
- { - history.push('/'); - }} style={{ width: '80px', marginRight: '30px', cursor: 'pointer' }} className="top-bar__logo" /> + { + history.push('/'); + }} + style={{ width: '80px', marginRight: '30px', cursor: 'pointer' }} + className="top-bar__logo" + />
@@ -84,28 +95,30 @@ export const TopBar = ({ title }: TopBarProps) => {
-
- setShowDropDown(!showDropdown)} - /> - ({ - text: org.name || '', - action: async () => { - await AuthStore.selectOrganization(org.name); - }, - selected: AuthStore.getUser()?.selectedOrg.name === org.name - })), - { text: '', jsx: Log Out, action: () => true }, - ]} - /> -
+ {playgroundFeatureVisible() && ( +
+ setShowDropDown(!showDropdown)} + /> + ({ + text: org.name || '', + action: async () => { + await AuthStore.selectOrganization(org.name); + }, + selected: AuthStore.getUser()?.selectedOrg.name === org.name, + })), + { text: '', jsx: Log Out, action: () => true }, + ]} + /> +
+ )}
); diff --git a/web/src/components/molecules/modal/ZModalPopup.tsx b/web/src/components/molecules/modal/ZModalPopup.tsx new file mode 100644 index 000000000..9334878bf --- /dev/null +++ b/web/src/components/molecules/modal/ZModalPopup.tsx @@ -0,0 +1,22 @@ +import './style.scss'; + +import classNames from 'classnames'; +import { FC, PropsWithChildren } from 'react'; + +interface Props extends PropsWithChildren { + className?: string; + isShown: boolean; + header: JSX.Element | string; + onClose: () => void; +} + +export const ZModalPopup: FC = ({ className = '', isShown, header, onClose, children }: Props) => { + return ( +
+
+
{header}
+
{children}
+
+
+ ); +}; diff --git a/web/src/components/molecules/modal/style.scss b/web/src/components/molecules/modal/style.scss new file mode 100644 index 000000000..238517176 --- /dev/null +++ b/web/src/components/molecules/modal/style.scss @@ -0,0 +1,46 @@ +@import '../../../assets/styles/colors'; + +.zlifecycle-modal-popup-overlay { + height: 100vh; + width: 100vw; + background-color: rgba(255, 255, 255, 0.3); + backdrop-filter: blur(10px); + position: fixed; + top: 0px; + left: 0px; + z-index: 2; + display: none; + .zlifecycle-modal-popup { + display: flex; + opacity: 0; + position: fixed; + height: fit-content; + top: -75vh; + width: 500px; + left: calc((100vw - 500px)/2); + z-index: 2; + box-shadow: 0 0 10px #aaa; + background-color: white; + border-radius: 5px; + transition: all 0.5s ease-in-out; + overflow-y: scroll; + flex-direction: column; + &--header { + background-color: $zlifecycle-navy; + color: white; + padding: 5px 10px; + font-size: 1.5em; + font-family: 'DM Sans'; + } + &--content { + padding: 5px 10px; + } + } + &--active { + display: block; + .zlifecycle-modal-popup { + top: 25vh; + opacity: 1; + } + } +} diff --git a/web/src/models/entity.store.ts b/web/src/models/entity.store.ts index 6abdb98ad..dd7e4c052 100644 --- a/web/src/models/entity.store.ts +++ b/web/src/models/entity.store.ts @@ -1,6 +1,5 @@ import { BehaviorSubject, Subject, Subscription } from 'rxjs'; import { EntityService } from 'services/entity/entity.service'; -import { ErrorStateService } from 'services/error/error-state.service'; import { CompAuditData, Component, EnvAuditData, Environment, StreamTypeEnum, Team, Update } from './entity.type'; export class EntityStore { @@ -39,7 +38,6 @@ export class EntityStore { } private constructor() { - ErrorStateService.getInstance(); this.generateEmitterMap(); Promise.resolve(this.getTeams()); this.startStreaming(); diff --git a/web/src/pages/authorized/authorizedRouteNames.ts b/web/src/pages/authorized/authorizedRouteNames.ts index 180383aa1..f068ba35d 100644 --- a/web/src/pages/authorized/authorizedRouteNames.ts +++ b/web/src/pages/authorized/authorizedRouteNames.ts @@ -6,11 +6,12 @@ import { Dashboard } from './dashboard/Dashboard'; import { EnvironmentBuilder } from './environment-builder/EnvironmentBuilder'; import { EnvironmentComponents } from './environment-components/EnvironmentComponents'; import { Environments } from './environments/Environments'; -import { FeatureRoutes } from './feature_toggle'; +import { BradAdarshFeatureVisible, FeatureRoutes, playgroundFeatureVisible } from './feature_toggle'; import { Overview } from './overview/Overview'; import { Profile } from './profile/Profile'; import { Teams } from './teams/Teams'; import { TermsAndConditions } from './terms-and-conditions/TermsAndConditons'; +import { ENVIRONMENT_VARIABLES } from 'utils/environmentVariables'; export const PROJECTS_URL = '/dashboard'; const DASHBOARD_URL = '/demo-dashboard'; @@ -38,6 +39,12 @@ const urls = [ { key: 'RESOURCE_VIEW_URL', value: RESOURCE_VIEW_URL }, ]; +if (ENVIRONMENT_VARIABLES.PLAYGROUND_APP && !BradAdarshFeatureVisible()) { + ['ORG_REGISTRATION', 'OVERVIEW_URL', 'QUICK_START_URL', 'ENVIRONMENT_BUILDER_URL', 'PROFILE_URL', 'RESOURCE_VIEW_URL'].forEach(e => { + urls.splice(urls.findIndex(u => e === u.key), 1); + }) +} + Reflect.ownKeys(FeatureRoutes).forEach(key => { if (Reflect.get(FeatureRoutes, key) === false) { switch (key) { diff --git a/web/src/pages/authorized/environments/Environments.tsx b/web/src/pages/authorized/environments/Environments.tsx index 1586c989d..84981a925 100644 --- a/web/src/pages/authorized/environments/Environments.tsx +++ b/web/src/pages/authorized/environments/Environments.tsx @@ -1,7 +1,8 @@ import { DiffEditor } from '@monaco-editor/react'; -import { NotificationType } from 'components/argo-core'; +import { NotificationType, NotificationsApi } from 'components/argo-core'; import { ZLoaderCover } from 'components/atoms/loader/LoaderCover'; import { EnvironmentCards } from 'components/molecules/cards/EnvironmentCards'; +import { ZModalPopup } from 'components/molecules/modal/ZModalPopup'; import { Context } from 'context/argo/ArgoUi'; import { ZEnvSyncStatus } from 'models/argo.models'; import { EntityStore } from 'models/entity.store'; @@ -12,12 +13,14 @@ import { getCheckBoxFilters, mockModifiedYaml, mockOriginalYaml, - renderSyncStatusItems + renderSyncStatusItems, } from 'pages/authorized/environments/helpers'; import React, { useEffect, useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; import { Subscription } from 'rxjs'; +import { EntityService } from 'services/entity/entity.service'; import { usePageHeader } from '../contexts/EnvironmentHeaderContext'; +import { playgroundFeatureVisible } from '../feature_toggle'; type CompareEnv = { env: EnvironmentItem | null; @@ -37,6 +40,8 @@ export const Environments: React.FC = () => { const [loading, setLoading] = useState(true); const [environments, setEnvironments] = useState([]); const [viewType, setViewType] = useState(''); + const [pushingCommit, setPushingCommit] = useState(null); + const [commitInfo, setCommitInfo] = useState(null); const [checkBoxFilters, setCheckBoxFilters] = useState(<>); const [filterItems, setFilterItems] = useState JSX.Element>>([]); const { pageHeaderObservable, breadcrumbObservable } = usePageHeader(); @@ -46,6 +51,7 @@ export const Environments: React.FC = () => { a: null, b: null, }); + const nm = React.useContext(Context)?.notifications as NotificationsApi; const breadcrumbItems = [ { @@ -205,7 +211,7 @@ export const Environments: React.FC = () => { return (
- +
{viewType === 'list' ? (
@@ -222,6 +228,98 @@ export const Environments: React.FC = () => { )} {compareEnvs.a?.env && compareEnvs.b?.env ? compareMode && renderDiffEditor() : null}
+ {!playgroundFeatureVisible() && ( + Provison an Environment
} + isShown={ + !loading && + environments?.length > 0 && + environments[0].status === ZEnvSyncStatus.Destroyed && + pushingCommit !== false + } + onClose={() => {}}> +
+
+ + Clicking on this button will push a commit to our repository and cloudknit will + start provisioning your environment. + +
+
+
+ + This is the repository where you will commit. + + + + https://github.com/zlab-tech/hooli-config + + +
+ +
+
+ + )} + {!playgroundFeatureVisible() && ( + Your commit was successful.} + isShown={commitInfo !== null} + onClose={() => {}}> +
+
+ You can see your commit by clicking on the below link. +
+
+ + + + {commitInfo} + + + +
+
+ +
+
+
+ )} ); diff --git a/web/src/pages/authorized/feature_toggle.tsx b/web/src/pages/authorized/feature_toggle.tsx index 0295b76c3..3341d90b2 100644 --- a/web/src/pages/authorized/feature_toggle.tsx +++ b/web/src/pages/authorized/feature_toggle.tsx @@ -1,4 +1,5 @@ -import AuthStore from "auth/AuthStore"; +import AuthStore from 'auth/AuthStore'; +import { ENVIRONMENT_VARIABLES } from 'utils/environmentVariables'; const showFeatures = (process.env.REACT_APP_ENABLED_FEATURE_FLAGS || '') .toString() @@ -49,11 +50,15 @@ export const featureToggled = (featureKey: string, userBased: boolean = false) = return BradAdarshFeatureVisible() && VisibleFeatures[featureKey]; } return VisibleFeatures[featureKey]; -} +}; -export function BradAdarshFeatureVisible() : boolean { +export function BradAdarshFeatureVisible(): boolean { const user = AuthStore.getUser(); // sometimes life hands you lemons... - return ['shahadarsh', 'bradj', 'shashank-cloudknit-io'].includes(user?.username || ''); + return ['shahadarsh', 'bradj', 'shashank-cloudknit-io', 'shashank-compuzest'].includes(user?.username || ''); +} + +export function playgroundFeatureVisible() { + return ENVIRONMENT_VARIABLES.PLAYGROUND_APP ? BradAdarshFeatureVisible() : true; } diff --git a/web/src/router/PrivateRoute.tsx b/web/src/router/PrivateRoute.tsx index 231a27ba3..a154cb261 100644 --- a/web/src/router/PrivateRoute.tsx +++ b/web/src/router/PrivateRoute.tsx @@ -2,9 +2,10 @@ import AuthStore from 'auth/AuthStore'; import { LOGIN_URL } from 'pages/anonymous/anonymousRouteNames'; import { NotFound } from 'pages/anonymous/not-found/NotFound'; import { QUICK_START_URL } from 'pages/authorized/authorizedRouteNames'; +import { playgroundFeatureVisible } from 'pages/authorized/feature_toggle'; import { QuickStart } from 'pages/authorized/quick-start/QuickStart'; import { TermsAndConditions } from 'pages/authorized/terms-and-conditions/TermsAndConditons'; -import React, { ElementType, FC, ReactNode, useEffect } from 'react'; +import { ElementType, FC, ReactNode } from 'react'; import { Redirect, Route, RouteComponentProps, RouteProps } from 'react-router-dom'; interface PrivateRouteProps extends Omit { @@ -20,21 +21,25 @@ const PrivateRoute: FC = ({ component: Component, ...rest }: render={(props: RouteComponentProps): ReactNode => { const user = AuthStore.getUser(); if (user) { - if (user.role !== 'Admin' && rest.location?.pathname?.includes('settings')) { - return - } + if (playgroundFeatureVisible()) { + if (user.role !== 'Admin' && rest.location?.pathname?.includes('settings')) { + return ; + } - if ((user.selectedOrg && !user.selectedOrg.githubRepo) || rest.location?.pathname === QUICK_START_URL) { - return ; - } + if ( + (user.selectedOrg && !user.selectedOrg.githubRepo) || + rest.location?.pathname === QUICK_START_URL + ) { + return ; + } - if (user.organizations.length === 0 || user.selectedOrg?.provisioned !== true) { - return ; + if (user.organizations.length === 0 || user.selectedOrg?.provisioned !== true) { + return ; + } } - + return ; } - return ; }} /> diff --git a/web/src/router/Routes.tsx b/web/src/router/Routes.tsx index bd0a92949..120de32af 100644 --- a/web/src/router/Routes.tsx +++ b/web/src/router/Routes.tsx @@ -1,11 +1,10 @@ -import AuthStore from 'auth/AuthStore'; import Anonymous from 'pages/anonymous'; import { LOGIN_URL } from 'pages/anonymous/anonymousRouteNames'; import { Login } from 'pages/anonymous/login/Login'; import { NotFound } from 'pages/anonymous/not-found/NotFound'; import Authorized from 'pages/authorized'; -import { privateRouteMap, PROJECTS_URL, routes } from 'pages/authorized/authorizedRouteNames'; -import React, { FC } from 'react'; +import { PROJECTS_URL, privateRouteMap, routes } from 'pages/authorized/authorizedRouteNames'; +import { FC } from 'react'; import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'; import PrivateRoute from 'router/PrivateRoute'; import PublicRoute from 'router/PublicRoute'; diff --git a/web/src/services/entity/entity.service.tsx b/web/src/services/entity/entity.service.tsx index 4265d741a..954b7ed09 100644 --- a/web/src/services/entity/entity.service.tsx +++ b/web/src/services/entity/entity.service.tsx @@ -44,7 +44,7 @@ export class EntityService extends BaseService { const url = this.constructUri(EntitytUriType.environment(teamId, envId)); try { const { data } = await ApiClient.patch(url, { - isReconcile: true + isReconcile: true, }); return data; } catch (err) { @@ -64,6 +64,19 @@ export class EntityService extends BaseService { } } + async gitCommit(teamId: number, envId: number) { + const url = this.constructUri(EntitytUriType.gitCommit(teamId, envId)); + try { + const { data } = await ApiClient.post(url); + return data; + } catch (err) { + console.error(err); + return { + status: 'error', + }; + } + } + stream(eventList: string[]) { const ec = new EventClient(this.constructUri(EntitytUriType.stream()), eventList); return ec.listen(); @@ -74,6 +87,7 @@ class EntitytUriType { static teams = (withComps: boolean) => `teams?withCost=true&withEnvironments=true&withComponents=${withComps}`; static environments = (teamId: number) => `teams/${teamId}/environments`; static environment = (teamId: number, envId: number) => `teams/${teamId}/environments/${envId}`; + static gitCommit = (teamId: number, envId: number) => `teams/${teamId}/gitCommit/${envId}`; static components = (teamId: number, envId: number, withLastAuditStatus: boolean) => `teams/${teamId}/environments/${envId}/components?withLastAuditStatus=${withLastAuditStatus}`; static stream = () => `stream`; diff --git a/web/src/services/error/error-state.service.tsx b/web/src/services/error/error-state.service.tsx deleted file mode 100644 index c1569a7bc..000000000 --- a/web/src/services/error/error-state.service.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { EnvironmentStatus, ErrorEvent, ErrorStatus, EventMessage } from 'models/error.model'; -import { ErrorService } from './error.service'; -import { Subject } from 'rxjs'; - -export class ErrorStateService { - private static instance: ErrorStateService | null = null; - private errorStateEnvironment: Map = new Map(); - public updates: Subject = new Subject(); - - private constructor() { - const errorInstance = ErrorService.getInstance(); - errorInstance.subscribeToErrorStream().subscribe((data: EnvironmentStatus) => this.errorStateData(data)); - errorInstance.getEventData(); - } - - static getInstance() { - if (!ErrorStateService.instance) { - ErrorStateService.instance = new ErrorStateService(); - } - return ErrorStateService.instance; - } - - private getTimestamp(e: EnvironmentStatus, err: string) { - const errorEvent = e.events.find(ev => (ev.payload || []).includes(err)); - return errorEvent?.createdAt ? new Date(errorEvent?.createdAt).toUTCString() : null; - } - - private errorMapper(e: EnvironmentStatus, err: string): EventMessage { - // @ts-ignore - return { - company: e.company, - environment: e.environment, - team: e.team, - message: err, - timestamp: e.events.length > 0 ? e.events[0].createdAt.toLocaleString() : 'N/A' - }; - } - - errorStateData(e: EnvironmentStatus) { - this.errorStateEnvironment.set(e.environment, e); - this.updates.next(); - } - - errorsInEnvironment(environmentId: string) { - const field = this.errorStateEnvironment.get(environmentId); - if (!field) { - return []; - } - return (field.errors || []).map(e => this.errorMapper(field, e)); - } - - public get Errors(): EventMessage[] { - const msgs: EventMessage[] = []; - - const errors = [...this.errorStateEnvironment.values()]; - const filtered = errors.filter(e => e.errors?.length > 0); - - filtered.forEach(error => { - msgs.push(...error.errors.map(er => this.errorMapper(error, er))); - }); - - return msgs; - } - - public get ErrorsEnvs(): EnvironmentStatus[] { - return [...this.errorStateEnvironment.values()].filter(e => e.errors?.length > 0); - } -} diff --git a/web/src/services/error/error.service.tsx b/web/src/services/error/error.service.tsx deleted file mode 100644 index e2608b8fb..000000000 --- a/web/src/services/error/error.service.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { EnvironmentStatus, ErrorEvent } from 'models/error.model'; -import { EventClient } from 'utils/apiClient/EventClient'; -import { Subject } from 'rxjs'; -import ApiClient from 'utils/apiClient'; - -export class ErrorService { - private static instance: ErrorService | null = null; - private constructUri = (path: string) => `/events/${path}`; - private constructApiUri = () => `/error-api`; - private errorModelStream: Subject | null = null; - private streamMap = new Map, EventClient>(); - - private constructor() { - } - - static getInstance() { - if (!ErrorService.instance) { - ErrorService.instance = new ErrorService(); - } - return ErrorService.instance; - } - - subscribeToErrorStream() { - if (!this.errorModelStream) { - const url = this.constructUri(ErrorUriType.errorStream); - const eventClient = new EventClient(url); - this.errorModelStream = eventClient.listen(); - this.streamMap.set(this.errorModelStream, eventClient); - } - - return this.errorModelStream; - } - - disposeStreams(...streams: Subject[]) { - streams.forEach(stream => { - const client = this.streamMap.get(stream); - - if (client) { - client.close(); - } - }); - } - - async getEventData() { - const url = this.constructApiUri(); - try { - const res = await ApiClient.get(url); - - if (!res.data || !res.data.status) { - return null; - } - - // status.environmentStatus.environment.team - const status = res.data.status - const environmentStatus = status.environmentStatus; - - for (const teamKey of Object.keys(environmentStatus)) { - const team = environmentStatus[teamKey]; - for (const envKey of Object.keys(environmentStatus[teamKey])) { - const env = team[envKey]; - const envEvents : ErrorEvent[] = []; - - if (env.status.status === "ok") { - continue; - } - - for (let event of env.status.events) { - envEvents.push({ - company: event.meta.company, - createdAt: new Date(event.createdAt), - environment: event.meta.environment, - eventType: event.eventType, - id: event.id, - team: event.meta.team - }); - } - - this.errorModelStream?.next({ - company: env.company, - environment: env.environment, - errors: env.status.status.validation?.errors, - events: envEvents, - status: env.status.status, - team: env.team, - }); - } - } - - return true; // super useful - } catch (err) { - console.log("getEventData error:", err); - return null; - } - } -} - -class ErrorUriType { - static errorStream = `stream`; -} diff --git a/web/src/utils/environmentVariables.ts b/web/src/utils/environmentVariables.ts index c14908503..2584df076 100644 --- a/web/src/utils/environmentVariables.ts +++ b/web/src/utils/environmentVariables.ts @@ -1,4 +1,5 @@ export const ENVIRONMENT_VARIABLES = { REACT_APP_CUSTOMER_NAME: `${process.env.REACT_APP_CUSTOMER_NAME}`, - REACT_APP_BASE_URL: `${process.env.REACT_APP_BASE_URL}` + REACT_APP_BASE_URL: `${process.env.REACT_APP_BASE_URL}`, + PLAYGROUND_APP: true, //`${process.env.PLAYGROUND_APP}` == 'true' } \ No newline at end of file