From f547c1f7c7483e8d17874cb420208ebe09c1a1b4 Mon Sep 17 00:00:00 2001 From: belopash Date: Tue, 11 Mar 2025 00:51:45 +0300 Subject: [PATCH 1/3] feat: output cloud link --- src/commands/view.ts | 260 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 src/commands/view.ts diff --git a/src/commands/view.ts b/src/commands/view.ts new file mode 100644 index 0000000..ef5d2d6 --- /dev/null +++ b/src/commands/view.ts @@ -0,0 +1,260 @@ +import { json } from 'stream/consumers'; + +import { ux as CliUx, Flags } from '@oclif/core'; +import { Manifest, ManifestValue } from '@subsquid/manifest'; +import chalk from 'chalk'; +import { startCase, toUpper } from 'lodash'; +import prettyBytes from 'pretty-bytes'; + +import { getSquid, Squid, SquidAddonsPostgres } from '../api'; +import { + SquidAddonsHasuraResponseStatus, + SquidApiResponseStatus, + SquidDiskResponseUsageStatus, + SquidProcessorResponseStatus, + SquidResponseStatus, +} from '../api/schema'; +import { CliCommand, SqdFlags } from '../command'; +import { printSquid } from '../utils'; + +export default class View extends CliCommand { + static description = 'View information about a squid'; + + static flags = { + org: SqdFlags.org({ + required: false, + }), + name: SqdFlags.name({ + required: false, + }), + slot: SqdFlags.slot({ + required: false, + }), + tag: SqdFlags.tag({ + required: false, + }), + reference: SqdFlags.reference({ + required: false, + }), + json: Flags.boolean({ + description: 'Output in JSON format', + }), + }; + + async run(): Promise { + const { + flags: { reference, interactive, json, ...flags }, + } = await this.parse(View); + + this.validateSquidNameFlags({ reference, ...flags }); + + const { org, name, slot, tag } = reference ? reference : (flags as any); + + const organization = name + ? await this.promptSquidOrganization(org, name, { interactive }) + : await this.promptOrganization(org, { interactive }); + + const squid = await getSquid({ organization, squid: { name, tag, slot } }); + + if (json) { + return this.log(JSON.stringify(squid, null, 2)); + } + + this.log(`${printSquid(squid)} (${squid.tags.map((t) => t.name).join(', ')})`); + CliUx.ux.styledHeader('General'); + printInfoTable([ + { + name: 'Status', + value: formatSquidStatus(squid.status), + }, + { + name: 'Description', + value: squid.description, + }, + { + name: 'Hibernated', + value: squid.hibernatedAt && new Date(squid.hibernatedAt).toUTCString(), + }, + { + name: 'Deployed', + value: squid.deployedAt && new Date(squid.deployedAt).toUTCString(), + }, + { + name: 'Created', + value: squid.createdAt && new Date(squid.createdAt).toUTCString(), + }, + ]); + if (squid.status !== 'HIBERNATED') { + if (squid.api) { + CliUx.ux.styledHeader('API'); + printInfoTable([ + { + name: 'Status', + value: formatApiStatus(squid.api?.status), + }, + { + name: 'URL', + value: squid.api?.urls?.map((u) => chalk.underline(u.url)).join('\n'), + }, + { + name: 'Profile', + value: startCase(getManifest(squid).scale?.api?.profile), + }, + { + name: 'Replicas', + value: getManifest(squid).scale?.api?.replicas, + }, + ]); + } + for (const processor of squid.processors || []) { + CliUx.ux.styledHeader(`Processor (${processor.name})`); + printInfoTable([ + { + name: 'Status', + value: formatProcessorStatus(processor.status), + }, + { + name: 'Progress', + value: `${processor.syncState.currentBlock}/${processor.syncState.totalBlocks} (${Math.round((processor.syncState.currentBlock / processor.syncState.totalBlocks) * 100)}%)`, + }, + { + name: 'Profile', + value: startCase(getManifest(squid).scale?.processor?.profile), + }, + ]); + } + if (squid.addons?.postgres) { + CliUx.ux.styledHeader('Addon (Postgres)'); + printInfoTable([ + { + name: 'Usage', + value: formatPostgresStatus(squid.addons?.postgres?.disk.usageStatus), + }, + { + name: 'Disk', + value: `${prettyBytes(squid.addons?.postgres?.disk.usedBytes)}/${prettyBytes(squid.addons?.postgres?.disk.totalBytes)} (${Math.round((squid.addons?.postgres?.disk.usedBytes / squid.addons?.postgres?.disk.totalBytes) * 100)}%)`, + }, + { + name: 'URL', + value: squid.addons?.postgres?.connections?.map((c) => chalk.underline(c.uri)).join('\n'), + }, + { + name: 'Profile', + value: startCase(getManifest(squid).scale?.addons?.postgres?.profile), + }, + ]); + } + if (squid.addons?.neon) { + CliUx.ux.styledHeader('Addon (Neon)'); + printInfoTable([ + { + name: 'URL', + value: squid.addons?.neon?.connections?.map((c) => chalk.underline(c.uri)), + }, + ]); + } + if (squid.addons?.hasura) { + CliUx.ux.styledHeader('Addon (Hasura)'); + printInfoTable([ + { + name: 'Status', + value: formatApiStatus(squid.addons?.hasura?.status), + }, + { + name: 'URL', + value: squid.addons?.hasura?.urls?.map((u) => chalk.underline(u.url)).join('\n'), + }, + { + name: 'Profile', + value: startCase(squid.addons?.hasura?.profile), + }, + { + name: 'Replicas', + value: squid.addons?.hasura?.replicas, + }, + ]); + } + } + this.log(); + this.log(`View this squid in Cloud: ${squid.links.cloudUrl}`); + } +} + +function printInfoTable( + data: { + name: string; + value: any; + }[], +) { + CliUx.ux.table( + data, + { + name: { + get: (v) => chalk.bold(v.name), + minWidth: 12, + }, + value: { + get: (v) => v.value ?? '-', + }, + }, + { 'no-header': true }, + ); +} + +function formatSquidStatus(status?: SquidResponseStatus) { + switch (status) { + case 'HIBERNATED': + return chalk.gray(status); + case 'DEPLOYED': + return chalk.green(status); + case 'DEPLOYING': + return chalk.blue(status); + case 'DEPLOY_ERROR': + return chalk.red(status); + default: + return status; + } +} + +function formatApiStatus(status?: SquidApiResponseStatus | SquidAddonsHasuraResponseStatus) { + switch (status) { + case 'AVAILABLE': + return chalk.green(status); + case 'NOT_AVAILABLE': + return chalk.red(status); + default: + return status; + } +} + +function formatProcessorStatus(status?: SquidProcessorResponseStatus) { + switch (status) { + case 'STARTING': + return chalk.blue(status); + case 'SYNCING': + return chalk.yellow(status); + case 'SYNCED': + return chalk.green(status); + default: + return status; + } +} + +function getManifest(squid: Squid): ManifestValue { + return squid.manifest.current as ManifestValue; +} + +function formatPostgresStatus(status?: SquidDiskResponseUsageStatus): any { + switch (status) { + case 'LOW': + return chalk.green(status); + case 'NORMAL': + return chalk.green(status); + case 'WARNING': + return chalk.yellow(status); + case 'CRITICAL': + return chalk.red(status); + default: + return status; + } +} From b9fa13d8bece79e20fc607e36c8928ee3c2fd704 Mon Sep 17 00:00:00 2001 From: belopash Date: Tue, 11 Mar 2025 11:14:02 +0300 Subject: [PATCH 2/3] feat: update squid view table --- src/commands/view.ts | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/commands/view.ts b/src/commands/view.ts index ef5d2d6..67f08cc 100644 --- a/src/commands/view.ts +++ b/src/commands/view.ts @@ -3,6 +3,7 @@ import { json } from 'stream/consumers'; import { ux as CliUx, Flags } from '@oclif/core'; import { Manifest, ManifestValue } from '@subsquid/manifest'; import chalk from 'chalk'; +import { func } from 'joi'; import { startCase, toUpper } from 'lodash'; import prettyBytes from 'pretty-bytes'; @@ -60,8 +61,14 @@ export default class View extends CliCommand { return this.log(JSON.stringify(squid, null, 2)); } - this.log(`${printSquid(squid)} (${squid.tags.map((t) => t.name).join(', ')})`); - CliUx.ux.styledHeader('General'); + this.log(`${chalk.bold('SQUID:')} ${printSquid(squid)} (${squid.tags.map((t) => t.name).join(', ')})`); + this.printSquidInfo(squid); + this.log(); + this.log(`View this squid in Cloud: ${squid.links.cloudUrl}`); + } + + printSquidInfo(squid: Squid) { + this.printHeader('General'); printInfoTable([ { name: 'Status', @@ -86,7 +93,7 @@ export default class View extends CliCommand { ]); if (squid.status !== 'HIBERNATED') { if (squid.api) { - CliUx.ux.styledHeader('API'); + this.printHeader('API'); printInfoTable([ { name: 'Status', @@ -107,7 +114,7 @@ export default class View extends CliCommand { ]); } for (const processor of squid.processors || []) { - CliUx.ux.styledHeader(`Processor (${processor.name})`); + this.printHeader(`Processor (${processor.name})`); printInfoTable([ { name: 'Status', @@ -115,7 +122,9 @@ export default class View extends CliCommand { }, { name: 'Progress', - value: `${processor.syncState.currentBlock}/${processor.syncState.totalBlocks} (${Math.round((processor.syncState.currentBlock / processor.syncState.totalBlocks) * 100)}%)`, + value: + `${formatNumber(processor.syncState.currentBlock)}/${formatNumber(processor.syncState.totalBlocks)} ` + + `(${Math.round((processor.syncState.currentBlock / processor.syncState.totalBlocks) * 100)}%)`, }, { name: 'Profile', @@ -124,7 +133,7 @@ export default class View extends CliCommand { ]); } if (squid.addons?.postgres) { - CliUx.ux.styledHeader('Addon (Postgres)'); + this.printHeader('Addon (Postgres)'); printInfoTable([ { name: 'Usage', @@ -132,7 +141,9 @@ export default class View extends CliCommand { }, { name: 'Disk', - value: `${prettyBytes(squid.addons?.postgres?.disk.usedBytes)}/${prettyBytes(squid.addons?.postgres?.disk.totalBytes)} (${Math.round((squid.addons?.postgres?.disk.usedBytes / squid.addons?.postgres?.disk.totalBytes) * 100)}%)`, + value: + `${prettyBytes(squid.addons?.postgres?.disk.usedBytes)}/${prettyBytes(squid.addons?.postgres?.disk.totalBytes)} ` + + `(${Math.round((squid.addons?.postgres?.disk.usedBytes / squid.addons?.postgres?.disk.totalBytes) * 100)}%)`, }, { name: 'URL', @@ -145,7 +156,7 @@ export default class View extends CliCommand { ]); } if (squid.addons?.neon) { - CliUx.ux.styledHeader('Addon (Neon)'); + this.printHeader('Addon (Neon)'); printInfoTable([ { name: 'URL', @@ -154,7 +165,7 @@ export default class View extends CliCommand { ]); } if (squid.addons?.hasura) { - CliUx.ux.styledHeader('Addon (Hasura)'); + this.printHeader('Addon (Hasura)'); printInfoTable([ { name: 'Status', @@ -175,8 +186,11 @@ export default class View extends CliCommand { ]); } } + } + + printHeader(value: string) { this.log(); - this.log(`View this squid in Cloud: ${squid.links.cloudUrl}`); + this.log(`${chalk.dim('===')} ${chalk.bold(value)}`); } } @@ -190,8 +204,8 @@ function printInfoTable( data, { name: { - get: (v) => chalk.bold(v.name), - minWidth: 12, + get: (v) => chalk.dim(v.name), + minWidth: 14, }, value: { get: (v) => v.value ?? '-', @@ -258,3 +272,7 @@ function formatPostgresStatus(status?: SquidDiskResponseUsageStatus): any { return status; } } + +export function formatNumber(value: number) { + return new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 }).format(value); +} From 0c3058a7b852e5c333399c07ca8c85fdea594827 Mon Sep 17 00:00:00 2001 From: belopash Date: Tue, 11 Mar 2025 12:13:23 +0300 Subject: [PATCH 3/3] fix: remove underline from pg connection string --- src/commands/view.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/commands/view.ts b/src/commands/view.ts index 67f08cc..792d1ec 100644 --- a/src/commands/view.ts +++ b/src/commands/view.ts @@ -64,7 +64,7 @@ export default class View extends CliCommand { this.log(`${chalk.bold('SQUID:')} ${printSquid(squid)} (${squid.tags.map((t) => t.name).join(', ')})`); this.printSquidInfo(squid); this.log(); - this.log(`View this squid in Cloud: ${squid.links.cloudUrl}`); + this.log(`View this squid in Cloud: ${chalk.underline(squid.links.cloudUrl)}`); } printSquidInfo(squid: Squid) { @@ -146,8 +146,8 @@ export default class View extends CliCommand { `(${Math.round((squid.addons?.postgres?.disk.usedBytes / squid.addons?.postgres?.disk.totalBytes) * 100)}%)`, }, { - name: 'URL', - value: squid.addons?.postgres?.connections?.map((c) => chalk.underline(c.uri)).join('\n'), + name: 'Connection', + value: squid.addons?.postgres?.connections?.map((c) => c.uri).join('\n'), }, { name: 'Profile', @@ -159,8 +159,8 @@ export default class View extends CliCommand { this.printHeader('Addon (Neon)'); printInfoTable([ { - name: 'URL', - value: squid.addons?.neon?.connections?.map((c) => chalk.underline(c.uri)), + name: 'Connection', + value: squid.addons?.neon?.connections?.map((c) => c.uri), }, ]); }