From e8eb72dc9142e547d2ade5f1d28d1bbc0dc5e6cf Mon Sep 17 00:00:00 2001 From: Tyler Aldrich Date: Mon, 15 May 2023 10:52:33 -0400 Subject: [PATCH 1/2] local UI draft --- src/commands/dev/index.ts | 2 +- src/common/docker-compose/index.ts | 29 ++++-- src/common/overlay/overlay-server.ts | 133 +++++++++++++++++++++++++-- src/static/favicon.ico | Bin 0 -> 4334 bytes src/static/overlay.js | 24 +++-- 5 files changed, 160 insertions(+), 28 deletions(-) create mode 100644 src/static/favicon.ico diff --git a/src/commands/dev/index.ts b/src/commands/dev/index.ts index 85eac7c1c..e1f6758a9 100644 --- a/src/commands/dev/index.ts +++ b/src/commands/dev/index.ts @@ -451,7 +451,7 @@ export default class Dev extends BaseCommand { }); if (overlay_port) { - new OverlayServer().listen(overlay_port); + new OverlayServer(this.app, this.config, default_project_name, DockerComposeUtils.getLocalServiceNames(compose_file)).listen(overlay_port); } try { diff --git a/src/common/docker-compose/index.ts b/src/common/docker-compose/index.ts index 65daf2bb8..e525c0add 100644 --- a/src/common/docker-compose/index.ts +++ b/src/common/docker-compose/index.ts @@ -28,6 +28,16 @@ type GenerateOptions = { getImage?: (ref: string) => string; }; +type ServiceValue = { + name: string; + display_name: string; +} + +export type ServiceKey = { + name: string; + value: ServiceValue; +}; + export class DockerComposeUtils { // used to namespace docker-compose projects so multiple deployments can happen to local public static DEFAULT_PROJECT = 'architect'; @@ -432,7 +442,7 @@ export class DockerComposeUtils { service_to.labels.push( `traefik.http.middlewares.${traefik_service}-rewritebody.plugin.rewritebody.lastModified=true`, `traefik.http.middlewares.${traefik_service}-rewritebody.plugin.rewritebody.rewrites.regex=`, - `traefik.http.middlewares.${traefik_service}-rewritebody.plugin.rewritebody.rewrites.replacement=`, + `traefik.http.middlewares.${traefik_service}-rewritebody.plugin.rewritebody.rewrites.replacement=`, `traefik.http.routers.${traefik_service}.middlewares=${traefik_service}-rewritebody@docker`, ); } @@ -632,18 +642,23 @@ export class DockerComposeUtils { return answers.environment; } - public static async getLocalServiceForEnvironment(compose_file: string, service_name?: string): Promise<{ display_name: string, name: string }> { - // docker-compose -f and -p don't work in tandem - const compose = yaml.load(fs.readFileSync(compose_file).toString()) as DockerComposeTemplate; + public static getLocalServiceNames(compose_file: string): ServiceKey[] { + // docker-compose -f and -p don't work in tandem + const compose = yaml.load(fs.readFileSync(compose_file).toString()) as DockerComposeTemplate; - const services: { name: string, value: { display_name: string, name: string } }[] = []; + const services: ServiceKey[] = []; for (const [service_name, service] of Object.entries(compose.services)) { const display_name = service.labels?.find((label) => label.startsWith('architect.ref='))?.split('=')[1]; if (!display_name) continue; services.push({ name: display_name, value: { name: service_name, display_name } }); } - const answers: { service: { display_name: string, name: string } } = await inquirer.prompt([ + return services; + } + + public static async getLocalServiceForEnvironment(compose_file: string, service_name?: string): Promise { + const services = DockerComposeUtils.getLocalServiceNames(compose_file); + const answers: { service: ServiceValue } = await inquirer.prompt([ { when: !service_name, type: 'autocomplete', @@ -655,7 +670,7 @@ export class DockerComposeUtils { }, ]); - let selected_service; + let selected_service: ServiceValue | undefined; if (service_name) { selected_service = services.find((service) => service.name === service_name)?.value; if (!selected_service) { diff --git a/src/common/overlay/overlay-server.ts b/src/common/overlay/overlay-server.ts index ebbf1ca08..dafa15c02 100644 --- a/src/common/overlay/overlay-server.ts +++ b/src/common/overlay/overlay-server.ts @@ -1,28 +1,141 @@ import fs from 'fs-extra'; import http from 'http'; import path from 'path'; +import DevRestart from '../../commands/dev/restart'; +import { Config } from '@oclif/core'; +import AppService from '../../app-config/service'; +import Logs from '../../commands/logs'; +import { ServiceKey } from '../docker-compose'; export class OverlayServer { + private app: AppService; + private config: Config; + private environment: string; + private services: ServiceKey[]; + + constructor(app: AppService, config: Config, environment: string, services: ServiceKey[]) { + this.app = app; + this.config = config; + this.environment = environment; + this.services = services; + } + listen(port: number): void { - const server = http.createServer(function (req, res) { + const server = http.createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - try { - // eslint-disable-next-line unicorn/prefer-module - const file_path = path.join(__dirname, '../../static/overlay.js'); - const file = fs.readFileSync(file_path); - res.writeHead(200, { 'Content-Type': 'text/javascript' }); - res.end(file); - } catch (err) { - res.writeHead(500, { 'Content-Type': 'text/plain' }); - res.end('Server Error'); + if (req.url === '/overlay.js') { + this.handleOverlay(res); + } else if (req.url === '/favicon.ico') { + this.handleFavicon(res); + } else if (req.url?.startsWith('/restart/')) { + const [_, __, service_name] = req.url.split('/'); + const restart_cmd = new DevRestart([service_name, '-e', this.environment], this.config); + restart_cmd.app = this.app; + + await restart_cmd.run(); + + res.writeHead(200); + res.end(`Restarted ${service_name}.`); + } else if (req.url?.startsWith('/logs/')) { + const [_, __, service_name] = req.url.split('/'); + const logs_cmd = new Logs([service_name, '-e', this.environment, '--raw'], this.config); + logs_cmd.app = this.app; + + res.writeHead(200); + res.write(`Logs can be viewed in the CLI with the command: 'architect logs -e ${this.environment} ${service_name}'`); + res.write('\n----------------\n\n'); + + // Overwrite log function to instead send data to our response. + logs_cmd.log = (...message: string[]) => { + for (const m of message) { + // log writes include ansi characters because we're using chalk to color text. + res.write(stripAnsi(m)); + } + res.write('\n'); + }; + + await logs_cmd.run(); + + res.end(); + } else { + res.writeHead(200); + let service_rows = ''; + for (const service_key of this.services) { + service_rows += ''; + + service_rows += ` + ${service_key.name} + Restart + View Logs + `; + + service_rows += ''; + } + + res.write(` + + + Architect Control + + + + + + + + + ${service_rows} +
Service
+ + + `); + res.end(); } }); server.on('error', err => console.log(err)); + console.log(`Starting overlay server on port: ${port}`); server.listen(port); } + + private handleOverlay(res: http.ServerResponse): void { + try { + // eslint-disable-next-line unicorn/prefer-module + const file_path = path.join(__dirname, '../../static/overlay.js'); + const file = fs.readFileSync(file_path); + res.writeHead(200, { 'Content-Type': 'text/javascript' }); + res.end(file); + } catch (err) { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Server Error'); + } + } + + private handleFavicon(res: http.ServerResponse): void { + try { + // eslint-disable-next-line unicorn/prefer-module + const file_path = path.join(__dirname, '../../static/favicon.ico'); + const file = fs.readFileSync(file_path); + res.writeHead(200, { 'Content-Type': 'image/x-icon' }); + res.end(file); + } catch { + // No favicon for you :( + } + } +} + +/** + * Remove ansi characters from a string. + * Pulled from https://github.com/chalk/strip-ansi + */ +function stripAnsi(string: string) { + const pattern = [ + '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', + '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))' + ].join('|'); + return string.replace(new RegExp(pattern, 'g'), ''); } diff --git a/src/static/favicon.ico b/src/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..5bced214fed3c5e2316be7ea446dbc5d115319b2 GIT binary patch literal 4334 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 literal 0 HcmV?d00001 diff --git a/src/static/overlay.js b/src/static/overlay.js index 5ac60f38e..cd1dd13ff 100644 --- a/src/static/overlay.js +++ b/src/static/overlay.js @@ -12,6 +12,10 @@ Architect.copyToClipboard = function (element, text) { tooltipText.innerHTML = 'Copied to clipboard!'; }; +Architect.run = function (url) { + fetch(url).then(res => console.log('Success')).catch(err => console.log('Failed')); +}; + Architect.outFunc = function (element) { const tooltipText = element.querySelector('.tooltiptext'); if (tooltipText.dataset.text) { @@ -29,6 +33,7 @@ Architect.appendHTML = function () { const script = document.querySelector('#architect-script'); const environment = script.dataset.environment; const service = script.dataset.service; + const overlay_url = script.dataset.overlayUrl; var styles = document.createElement('style'); styles.innerHTML = ` @@ -129,24 +134,23 @@ Architect.appendHTML = function () { wrapper.innerHTML = `