From ac492512b38b0f34e6305297f3e6240b7f093b9d Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 9 Sep 2024 16:06:17 +0300 Subject: [PATCH 1/8] feat: runs ls --- src/commands/runs/ls.ts | 283 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 src/commands/runs/ls.ts diff --git a/src/commands/runs/ls.ts b/src/commands/runs/ls.ts new file mode 100644 index 00000000..8b85ea91 --- /dev/null +++ b/src/commands/runs/ls.ts @@ -0,0 +1,283 @@ +import { Flags } from '@oclif/core'; +import { DurationFormatter as SapphireDurationFormatter, TimeTypes } from '@sapphire/duration'; +import { Timestamp } from '@sapphire/timestamp'; +import chalk from 'chalk'; +import Table from 'cli-table'; + +import { ApifyCommand } from '../../lib/apify_command.js'; +import { error, simpleLog } from '../../lib/outputs.js'; +import { getLocalConfig, getLocalUserInfo, getLoggedClientOrThrow } from '../../lib/utils.js'; + +// TODO: remove this once https://github.com/apify/apify-cli/pull/620 is merged +async function resolveActorContext({ + providedActorNameOrId, + client, +}: { providedActorNameOrId: string | undefined; client: import('apify-client').ApifyClient }) { + const userInfo = await getLocalUserInfo(); + const usernameOrId = userInfo.username || (userInfo.id as string); + const localConfig = getLocalConfig(process.cwd()) || {}; + + // Full ID + if (providedActorNameOrId?.includes('/')) { + const actor = await client.actor(providedActorNameOrId).get(); + if (!actor) { + return { + valid: false as const, + reason: `Actor with ID "${providedActorNameOrId}" was not found`, + }; + } + + return { + valid: true as const, + userFriendlyId: `${actor.username}/${actor.name}`, + id: actor.id, + }; + } + + // Try fetching Actor directly by name/id + if (providedActorNameOrId) { + const actorById = await client.actor(providedActorNameOrId).get(); + + if (actorById) { + return { + valid: true as const, + userFriendlyId: `${actorById.username}/${actorById.name}`, + id: actorById.id, + }; + } + + const actorByName = await client.actor(`${usernameOrId}/${providedActorNameOrId.toLowerCase()}`).get(); + + if (actorByName) { + return { + valid: true as const, + userFriendlyId: `${actorByName.username}/${actorByName.name}`, + id: actorByName.id, + }; + } + + return { + valid: false as const, + reason: `Actor with name or ID "${providedActorNameOrId}" was not found`, + }; + } + + if (localConfig.name) { + const actor = await client.actor(`${usernameOrId}/${localConfig.name}`).get(); + + if (!actor) { + return { + valid: false as const, + reason: `Actor with name "${localConfig.name}" was not found`, + }; + } + + return { + valid: true as const, + userFriendlyId: `${actor.username}/${actor.name}`, + id: actor.id, + }; + } + + return { + valid: false as const, + reason: 'Unable to detect what Actor to create a build for', + }; +} + +function prettyPrintStatus(status: string) { + switch (status) { + case 'READY': + return chalk.green('Ready'); + case 'RUNNING': + return chalk.blue('Running'); + case 'SUCCEEDED': + return chalk.green('Succeeded'); + case 'FAILED': + return chalk.red('Failed'); + case 'ABORTING': + return chalk.yellow('Aborting'); + case 'ABORTED': + return chalk.red('Aborted'); + case 'TIMING-OUT': + return chalk.yellow('Timing Out'); + case 'TIMED-OUT': + return chalk.red('Timed Out'); + default: + return chalk.gray( + (status as string) + .split('-') + .map((part) => part[0].toUpperCase() + part.slice(1).toLowerCase()) + .join(' '), + ); + } +} + +const ShortDurationFormatter = new SapphireDurationFormatter({ + [TimeTypes.Day]: { + DEFAULT: 'd', + }, + [TimeTypes.Hour]: { + DEFAULT: 'h', + }, + [TimeTypes.Minute]: { + DEFAULT: 'm', + }, + [TimeTypes.Month]: { + DEFAULT: 'M', + }, + [TimeTypes.Second]: { + DEFAULT: 's', + }, + [TimeTypes.Week]: { + DEFAULT: 'w', + }, + [TimeTypes.Year]: { + DEFAULT: 'y', + }, +}); + +// END of TODO + +const multilineTimestampFormatter = new Timestamp(`YYYY-MM-DD[\n]HH:mm:ss`); +const tableFactory = (compact = false) => { + const options: Record = { + head: ['ID', 'Status', 'Results', 'Usage', 'Start Date', 'Took', 'Build Number', 'Origin'], + style: { + head: ['cyan', 'cyan', 'cyan', 'cyan', 'cyan', 'cyan', 'cyan', 'cyan'], + compact, + }, + colAligns: ['middle', 'middle', 'middle', 'middle', 'middle', 'middle', 'middle', 'middle'], + }; + + if (compact) { + options.chars = { + 'mid': '', + 'left-mid': '', + 'mid-mid': '', + 'right-mid': '', + middle: ' ', + 'top-mid': '─', + 'bottom-mid': '─', + }; + } + + return new Table<[string, string, string, string, string, string, string, string]>(options); +}; + +export class RunsLsCommand extends ApifyCommand { + static override description = 'Lists all runs of the Actor.'; + + static override flags = { + actor: Flags.string({ + description: + 'Optional Actor ID or Name to list runs for. By default, it will use the Actor from the current directory.', + }), + offset: Flags.integer({ + description: 'Number of runs that will be skipped.', + default: 0, + }), + limit: Flags.integer({ + description: 'Number of runs that will be listed.', + default: 10, + }), + desc: Flags.boolean({ + description: 'Sort runs in descending order.', + default: false, + }), + compact: Flags.boolean({ + description: 'Display a compact table.', + default: false, + char: 'c', + }), + }; + + static override enableJsonFlag = true; + + async run() { + const { actor, desc, limit, offset, compact, json } = this.flags; + + const client = await getLoggedClientOrThrow(); + + // TODO: technically speaking, we don't *need* an actor id to list builds. But it makes more sense to have a table of builds for a specific actor. + const ctx = await resolveActorContext({ providedActorNameOrId: actor, client }); + + if (!ctx.valid) { + error({ + message: `${ctx.reason}. Please run this command in an Actor directory, or specify the Actor ID by running this command with "--actor=".`, + }); + + return; + } + + const allRuns = await client.actor(ctx.id).runs().list({ desc, limit, offset }); + + if (json) { + return allRuns; + } + + if (!allRuns.items.length) { + simpleLog({ + message: 'There are no recent runs found for this Actor.', + }); + + return; + } + + const table = tableFactory(compact); + + const message = [ + `${chalk.reset('Showing')} ${chalk.yellow(allRuns.items.length)} out of ${chalk.yellow(allRuns.total)} runs for Actor ${chalk.yellow(ctx.userFriendlyId)} (${chalk.gray(ctx.id)})\n`, + ]; + + const datasetInfos = new Map( + await Promise.all( + allRuns.items.map(async (run) => + client + .dataset(run.defaultDatasetId) + .get() + .then( + (data) => [run.id, chalk.yellow(data?.itemCount ?? 0)] as const, + () => [run.id, chalk.gray('N/A')] as const, + ), + ), + ), + ); + + for (const run of allRuns.items) { + // 'ID', 'Status', 'Results', 'Usage', 'Took', 'Build Number', 'Origin' + const tableRow: [string, string, string, string, string, string, string, string] = [ + chalk.gray(run.id), + prettyPrintStatus(run.status), + chalk.gray('N/A'), + chalk.cyan(`$${(run.usageTotalUsd ?? 0).toFixed(3)}`), + multilineTimestampFormatter.display(run.startedAt), + '', + run.buildNumber, + run.meta.origin ?? 'UNKNOWN', + ]; + + if (run.finishedAt) { + const diff = run.finishedAt.getTime() - run.startedAt.getTime(); + + tableRow[5] = chalk.gray(`${ShortDurationFormatter.format(diff, undefined, { left: '' })}`); + } else { + const diff = Date.now() - run.startedAt.getTime(); + + tableRow[5] = chalk.gray(`Running for ${ShortDurationFormatter.format(diff, undefined, { left: '' })}`); + } + + tableRow[2] = datasetInfos.get(run.id) || chalk.gray('N/A'); + + table.push(tableRow); + } + + message.push(table.toString()); + + simpleLog({ + message: message.join('\n'), + }); + + return undefined; + } +} From bacc80e54aaa4e1d53f2e0c732ef9446c53bd1d7 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 9 Sep 2024 17:39:30 +0300 Subject: [PATCH 2/8] chore: support terminal sizes of < 100 columns (80columns is min) --- src/commands/runs/ls.ts | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/commands/runs/ls.ts b/src/commands/runs/ls.ts index 8b85ea91..88646cb0 100644 --- a/src/commands/runs/ls.ts +++ b/src/commands/runs/ls.ts @@ -140,9 +140,16 @@ const ShortDurationFormatter = new SapphireDurationFormatter({ // END of TODO const multilineTimestampFormatter = new Timestamp(`YYYY-MM-DD[\n]HH:mm:ss`); +const terminalColumns = process.stdout.columns ?? 100; const tableFactory = (compact = false) => { + const head = + terminalColumns < 100 + ? // Smaller terminals should show less data + ['ID', 'Status', 'Results', 'Usage', 'Started At', 'Took'] + : ['ID', 'Status', 'Results', 'Usage', 'Started At', 'Took', 'Build No.', 'Origin']; + const options: Record = { - head: ['ID', 'Status', 'Results', 'Usage', 'Start Date', 'Took', 'Build Number', 'Origin'], + head, style: { head: ['cyan', 'cyan', 'cyan', 'cyan', 'cyan', 'cyan', 'cyan', 'cyan'], compact, @@ -162,7 +169,12 @@ const tableFactory = (compact = false) => { }; } - return new Table<[string, string, string, string, string, string, string, string]>(options); + return new Table< + // Small terminal (drop origin and build number) + | [string, string, string, string, string, string] + // large enough terminal + | [string, string, string, string, string, string, string, string] + >(options); }; export class RunsLsCommand extends ApifyCommand { @@ -246,15 +258,15 @@ export class RunsLsCommand extends ApifyCommand { for (const run of allRuns.items) { // 'ID', 'Status', 'Results', 'Usage', 'Took', 'Build Number', 'Origin' - const tableRow: [string, string, string, string, string, string, string, string] = [ + const tableRow: + | [string, string, string, string, string, string] + | [string, string, string, string, string, string, string, string] = [ chalk.gray(run.id), prettyPrintStatus(run.status), chalk.gray('N/A'), chalk.cyan(`$${(run.usageTotalUsd ?? 0).toFixed(3)}`), multilineTimestampFormatter.display(run.startedAt), '', - run.buildNumber, - run.meta.origin ?? 'UNKNOWN', ]; if (run.finishedAt) { @@ -269,6 +281,10 @@ export class RunsLsCommand extends ApifyCommand { tableRow[2] = datasetInfos.get(run.id) || chalk.gray('N/A'); + if (terminalColumns >= 100) { + tableRow.push(run.buildNumber, run.meta.origin ?? 'UNKNOWN'); + } + table.push(tableRow); } From f891ccec0d122b982d48133736b0e18f87e7450a Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 9 Sep 2024 17:46:49 +0300 Subject: [PATCH 3/8] chore: post rebase cleanup --- src/commands/runs/ls.ts | 136 +--------------------------------------- 1 file changed, 3 insertions(+), 133 deletions(-) diff --git a/src/commands/runs/ls.ts b/src/commands/runs/ls.ts index 88646cb0..f193a5de 100644 --- a/src/commands/runs/ls.ts +++ b/src/commands/runs/ls.ts @@ -1,143 +1,13 @@ import { Flags } from '@oclif/core'; -import { DurationFormatter as SapphireDurationFormatter, TimeTypes } from '@sapphire/duration'; import { Timestamp } from '@sapphire/timestamp'; import chalk from 'chalk'; import Table from 'cli-table'; import { ApifyCommand } from '../../lib/apify_command.js'; +import { prettyPrintStatus } from '../../lib/commands/pretty-print-status.js'; +import { resolveActorContext } from '../../lib/commands/resolve-actor-context.js'; import { error, simpleLog } from '../../lib/outputs.js'; -import { getLocalConfig, getLocalUserInfo, getLoggedClientOrThrow } from '../../lib/utils.js'; - -// TODO: remove this once https://github.com/apify/apify-cli/pull/620 is merged -async function resolveActorContext({ - providedActorNameOrId, - client, -}: { providedActorNameOrId: string | undefined; client: import('apify-client').ApifyClient }) { - const userInfo = await getLocalUserInfo(); - const usernameOrId = userInfo.username || (userInfo.id as string); - const localConfig = getLocalConfig(process.cwd()) || {}; - - // Full ID - if (providedActorNameOrId?.includes('/')) { - const actor = await client.actor(providedActorNameOrId).get(); - if (!actor) { - return { - valid: false as const, - reason: `Actor with ID "${providedActorNameOrId}" was not found`, - }; - } - - return { - valid: true as const, - userFriendlyId: `${actor.username}/${actor.name}`, - id: actor.id, - }; - } - - // Try fetching Actor directly by name/id - if (providedActorNameOrId) { - const actorById = await client.actor(providedActorNameOrId).get(); - - if (actorById) { - return { - valid: true as const, - userFriendlyId: `${actorById.username}/${actorById.name}`, - id: actorById.id, - }; - } - - const actorByName = await client.actor(`${usernameOrId}/${providedActorNameOrId.toLowerCase()}`).get(); - - if (actorByName) { - return { - valid: true as const, - userFriendlyId: `${actorByName.username}/${actorByName.name}`, - id: actorByName.id, - }; - } - - return { - valid: false as const, - reason: `Actor with name or ID "${providedActorNameOrId}" was not found`, - }; - } - - if (localConfig.name) { - const actor = await client.actor(`${usernameOrId}/${localConfig.name}`).get(); - - if (!actor) { - return { - valid: false as const, - reason: `Actor with name "${localConfig.name}" was not found`, - }; - } - - return { - valid: true as const, - userFriendlyId: `${actor.username}/${actor.name}`, - id: actor.id, - }; - } - - return { - valid: false as const, - reason: 'Unable to detect what Actor to create a build for', - }; -} - -function prettyPrintStatus(status: string) { - switch (status) { - case 'READY': - return chalk.green('Ready'); - case 'RUNNING': - return chalk.blue('Running'); - case 'SUCCEEDED': - return chalk.green('Succeeded'); - case 'FAILED': - return chalk.red('Failed'); - case 'ABORTING': - return chalk.yellow('Aborting'); - case 'ABORTED': - return chalk.red('Aborted'); - case 'TIMING-OUT': - return chalk.yellow('Timing Out'); - case 'TIMED-OUT': - return chalk.red('Timed Out'); - default: - return chalk.gray( - (status as string) - .split('-') - .map((part) => part[0].toUpperCase() + part.slice(1).toLowerCase()) - .join(' '), - ); - } -} - -const ShortDurationFormatter = new SapphireDurationFormatter({ - [TimeTypes.Day]: { - DEFAULT: 'd', - }, - [TimeTypes.Hour]: { - DEFAULT: 'h', - }, - [TimeTypes.Minute]: { - DEFAULT: 'm', - }, - [TimeTypes.Month]: { - DEFAULT: 'M', - }, - [TimeTypes.Second]: { - DEFAULT: 's', - }, - [TimeTypes.Week]: { - DEFAULT: 'w', - }, - [TimeTypes.Year]: { - DEFAULT: 'y', - }, -}); - -// END of TODO +import { getLoggedClientOrThrow, ShortDurationFormatter } from '../../lib/utils.js'; const multilineTimestampFormatter = new Timestamp(`YYYY-MM-DD[\n]HH:mm:ss`); const terminalColumns = process.stdout.columns ?? 100; From a1edce63ed5cfa9d5ccdf90813648e540d036fb5 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Mon, 9 Sep 2024 18:04:06 +0300 Subject: [PATCH 4/8] chore: realign columns --- src/commands/runs/ls.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/commands/runs/ls.ts b/src/commands/runs/ls.ts index f193a5de..5fcd9c19 100644 --- a/src/commands/runs/ls.ts +++ b/src/commands/runs/ls.ts @@ -18,13 +18,18 @@ const tableFactory = (compact = false) => { ['ID', 'Status', 'Results', 'Usage', 'Started At', 'Took'] : ['ID', 'Status', 'Results', 'Usage', 'Started At', 'Took', 'Build No.', 'Origin']; + const colAligns = + terminalColumns < 100 + ? ['left', 'left', 'right', 'right', 'left', 'right'] + : ['left', 'left', 'right', 'right', 'left', 'right', 'left', 'left']; + const options: Record = { head, style: { head: ['cyan', 'cyan', 'cyan', 'cyan', 'cyan', 'cyan', 'cyan', 'cyan'], compact, }, - colAligns: ['middle', 'middle', 'middle', 'middle', 'middle', 'middle', 'middle', 'middle'], + colAligns, }; if (compact) { From 082ac19ae313f04884067aaaad71f9fe8c90bb80 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Tue, 10 Sep 2024 11:12:22 +0300 Subject: [PATCH 5/8] chore: reusable "responsive" table --- package.json | 3 +- src/commands/builds/ls.ts | 55 ++++++----------- src/commands/runs/ls.ts | 90 ++++++++-------------------- src/lib/commands/responsive-table.ts | 79 ++++++++++++++++++++++++ yarn.lock | 31 ++++------ 5 files changed, 132 insertions(+), 126 deletions(-) create mode 100644 src/lib/commands/responsive-table.ts diff --git a/package.json b/package.json index 30d90b5d..6e91063a 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "archiver": "~7.0.1", "axios": "~1.7.3", "chalk": "~5.3.0", - "cli-table": "^0.3.11", + "cli-table3": "^0.6.5", "computer-name": "~0.1.0", "configparser": "~0.3.10", "cors": "~2.8.5", @@ -113,7 +113,6 @@ "@types/adm-zip": "^0.5.5", "@types/archiver": "^6.0.2", "@types/chai": "^4.3.17", - "@types/cli-table": "^0", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/fs-extra": "^11", diff --git a/src/commands/builds/ls.ts b/src/commands/builds/ls.ts index 05475263..2bd2bfe1 100644 --- a/src/commands/builds/ls.ts +++ b/src/commands/builds/ls.ts @@ -1,37 +1,19 @@ import { Flags } from '@oclif/core'; import type { BuildCollectionClientListItem } from 'apify-client'; import chalk from 'chalk'; -import Table from 'cli-table'; import { ApifyCommand } from '../../lib/apify_command.js'; import { prettyPrintStatus } from '../../lib/commands/pretty-print-status.js'; import { resolveActorContext } from '../../lib/commands/resolve-actor-context.js'; +import { ResponsiveTable } from '../../lib/commands/responsive-table.js'; import { error, simpleLog } from '../../lib/outputs.js'; import { getLoggedClientOrThrow, objectGroupBy, ShortDurationFormatter } from '../../lib/utils.js'; -const tableFactory = (compact = false) => { - const options: Record = { - head: ['Number', 'ID', 'Status', 'Took'], - style: { - head: ['cyan', 'cyan', 'cyan', 'cyan'], - compact, - }, - }; - - if (compact) { - options.chars = { - 'mid': '', - 'left-mid': '', - 'mid-mid': '', - 'right-mid': '', - middle: ' ', - 'top-mid': '─', - 'bottom-mid': '─', - }; - } - - return new Table<[string, string, string, string]>(options); -}; +const tableFactory = () => + new ResponsiveTable({ + allColumns: ['Number', 'ID', 'Status', 'Took'], + mandatoryColumns: ['Number', 'ID', 'Status', 'Took'], + }); export class BuildLsCommand extends ApifyCommand { static override description = 'Lists all builds of the Actor.'; @@ -132,7 +114,6 @@ export class BuildLsCommand extends ApifyCommand { const latestBuildTag = actorInfo.versions.find((v) => v.versionNumber === actorVersion)?.buildTag; const table = this.generateTableForActorVersion({ buildsForVersion, - compact, buildTagToActorVersion, }); @@ -142,7 +123,7 @@ export class BuildLsCommand extends ApifyCommand { const message = [ chalk.reset(`Builds for Actor Version ${chalk.yellow(actorVersion)}${latestBuildTagMessage}`), - table.toString(), + table.render(compact), '', ]; @@ -155,15 +136,13 @@ export class BuildLsCommand extends ApifyCommand { } private generateTableForActorVersion({ - compact, buildsForVersion, buildTagToActorVersion, }: { - compact: boolean; buildsForVersion: BuildCollectionClientListItem[]; buildTagToActorVersion: Record; }) { - const table = tableFactory(compact); + const table = tableFactory(); for (const build of buildsForVersion) { // TODO: untyped field, https://github.com/apify/apify-client-js/issues/526 @@ -173,23 +152,23 @@ export class BuildLsCommand extends ApifyCommand { ? ` (${chalk.yellow(buildTagToActorVersion[buildNumber])})` : ''; - const tableRow: [string, string, string, string] = [ - `${buildNumber}${hasTag}`, - chalk.gray(build.id), - prettyPrintStatus(build.status), - '', - ]; + let finishedAt: string; if (build.finishedAt) { const diff = build.finishedAt.getTime() - build.startedAt.getTime(); - tableRow[3] = chalk.gray(`${ShortDurationFormatter.format(diff, undefined, { left: '' })}`); + finishedAt = chalk.gray(`${ShortDurationFormatter.format(diff, undefined, { left: '' })}`); } else { const diff = Date.now() - build.startedAt.getTime(); - tableRow[3] = chalk.gray(`Running for ${ShortDurationFormatter.format(diff, undefined, { left: '' })}`); + finishedAt = chalk.gray(`Running for ${ShortDurationFormatter.format(diff, undefined, { left: '' })}`); } - table.push(tableRow); + table.pushRow({ + Number: `${buildNumber}${hasTag}`, + ID: chalk.gray(build.id), + Status: prettyPrintStatus(build.status), + Took: finishedAt, + }); } return table; diff --git a/src/commands/runs/ls.ts b/src/commands/runs/ls.ts index 5fcd9c19..9696a3d3 100644 --- a/src/commands/runs/ls.ts +++ b/src/commands/runs/ls.ts @@ -1,56 +1,25 @@ import { Flags } from '@oclif/core'; import { Timestamp } from '@sapphire/timestamp'; import chalk from 'chalk'; -import Table from 'cli-table'; import { ApifyCommand } from '../../lib/apify_command.js'; import { prettyPrintStatus } from '../../lib/commands/pretty-print-status.js'; import { resolveActorContext } from '../../lib/commands/resolve-actor-context.js'; +import { ResponsiveTable } from '../../lib/commands/responsive-table.js'; import { error, simpleLog } from '../../lib/outputs.js'; import { getLoggedClientOrThrow, ShortDurationFormatter } from '../../lib/utils.js'; const multilineTimestampFormatter = new Timestamp(`YYYY-MM-DD[\n]HH:mm:ss`); -const terminalColumns = process.stdout.columns ?? 100; -const tableFactory = (compact = false) => { - const head = - terminalColumns < 100 - ? // Smaller terminals should show less data - ['ID', 'Status', 'Results', 'Usage', 'Started At', 'Took'] - : ['ID', 'Status', 'Results', 'Usage', 'Started At', 'Took', 'Build No.', 'Origin']; - - const colAligns = - terminalColumns < 100 - ? ['left', 'left', 'right', 'right', 'left', 'right'] - : ['left', 'left', 'right', 'right', 'left', 'right', 'left', 'left']; - - const options: Record = { - head, - style: { - head: ['cyan', 'cyan', 'cyan', 'cyan', 'cyan', 'cyan', 'cyan', 'cyan'], - compact, - }, - colAligns, - }; - - if (compact) { - options.chars = { - 'mid': '', - 'left-mid': '', - 'mid-mid': '', - 'right-mid': '', - middle: ' ', - 'top-mid': '─', - 'bottom-mid': '─', - }; - } - return new Table< - // Small terminal (drop origin and build number) - | [string, string, string, string, string, string] - // large enough terminal - | [string, string, string, string, string, string, string, string] - >(options); -}; +const table = new ResponsiveTable({ + allColumns: ['ID', 'Status', 'Results', 'Usage', 'Started At', 'Took', 'Build No.', 'Origin'], + mandatoryColumns: ['ID', 'Status', 'Results', 'Usage', 'Started At', 'Took'], + columnAlignments: { + Results: 'right', + Usage: 'right', + Took: 'right', + }, +}); export class RunsLsCommand extends ApifyCommand { static override description = 'Lists all runs of the Actor.'; @@ -111,10 +80,8 @@ export class RunsLsCommand extends ApifyCommand { return; } - const table = tableFactory(compact); - const message = [ - `${chalk.reset('Showing')} ${chalk.yellow(allRuns.items.length)} out of ${chalk.yellow(allRuns.total)} runs for Actor ${chalk.yellow(ctx.userFriendlyId)} (${chalk.gray(ctx.id)})\n`, + `${chalk.reset('Showing')} ${chalk.yellow(allRuns.items.length)} out of ${chalk.yellow(allRuns.total)} runs for Actor ${chalk.yellow(ctx.userFriendlyId)} (${chalk.gray(ctx.id)})`, ]; const datasetInfos = new Map( @@ -132,38 +99,31 @@ export class RunsLsCommand extends ApifyCommand { ); for (const run of allRuns.items) { - // 'ID', 'Status', 'Results', 'Usage', 'Took', 'Build Number', 'Origin' - const tableRow: - | [string, string, string, string, string, string] - | [string, string, string, string, string, string, string, string] = [ - chalk.gray(run.id), - prettyPrintStatus(run.status), - chalk.gray('N/A'), - chalk.cyan(`$${(run.usageTotalUsd ?? 0).toFixed(3)}`), - multilineTimestampFormatter.display(run.startedAt), - '', - ]; + let tookString: string; if (run.finishedAt) { const diff = run.finishedAt.getTime() - run.startedAt.getTime(); - tableRow[5] = chalk.gray(`${ShortDurationFormatter.format(diff, undefined, { left: '' })}`); + tookString = chalk.gray(`${ShortDurationFormatter.format(diff, undefined, { left: '' })}`); } else { const diff = Date.now() - run.startedAt.getTime(); - tableRow[5] = chalk.gray(`Running for ${ShortDurationFormatter.format(diff, undefined, { left: '' })}`); + tookString = chalk.gray(`Running for ${ShortDurationFormatter.format(diff, undefined, { left: '' })}`); } - tableRow[2] = datasetInfos.get(run.id) || chalk.gray('N/A'); - - if (terminalColumns >= 100) { - tableRow.push(run.buildNumber, run.meta.origin ?? 'UNKNOWN'); - } - - table.push(tableRow); + table.pushRow({ + ID: chalk.gray(run.id), + Status: prettyPrintStatus(run.status), + Results: datasetInfos.get(run.id) || chalk.gray('N/A'), + Usage: chalk.cyan(`$${(run.usageTotalUsd ?? 0).toFixed(3)}`), + 'Started At': multilineTimestampFormatter.display(run.startedAt), + Took: tookString, + 'Build No.': run.buildNumber, + Origin: run.meta.origin ?? 'UNKNOWN', + }); } - message.push(table.toString()); + message.push(table.render(compact)); simpleLog({ message: message.join('\n'), diff --git a/src/lib/commands/responsive-table.ts b/src/lib/commands/responsive-table.ts new file mode 100644 index 00000000..db766d6f --- /dev/null +++ b/src/lib/commands/responsive-table.ts @@ -0,0 +1,79 @@ +import Table from 'cli-table3'; + +const compactModeChars = { + 'mid': '', + 'left-mid': '', + 'mid-mid': '', + 'right-mid': '', + middle: ' ', + 'top-mid': '─', + 'bottom-mid': '─', +}; + +function generateHeaderColors(length: number): string[] { + return Array.from({ length }, () => 'cyan'); +} + +const terminalColumns = process.stdout.columns ?? 100; + +export interface ResponsiveTableOptions< + AllColumns extends string, + MandatoryColumns extends NoInfer = AllColumns, +> { + /** + * Represents all the columns the that this table should show, and their order + */ + allColumns: AllColumns[]; + /** + * Represents the columns that are mandatory for the user to see, even if the terminal size is less than adequate (<100). + * Make sure this field includes columns that provide enough context AND that will fit in an 80-column terminal. + */ + mandatoryColumns: MandatoryColumns[]; + /** + * By default, all columns are left-aligned. You can specify columns that should be aligned in the middle or right + */ + columnAlignments?: Partial>; +} + +export class ResponsiveTable = AllColumns> { + private options: ResponsiveTableOptions; + + private rows: Record[] = []; + + constructor(options: ResponsiveTableOptions) { + this.options = options; + } + + pushRow(item: Record) { + this.rows.push(item); + } + + render(compact = false): string { + const head = terminalColumns < 100 ? this.options.mandatoryColumns : this.options.allColumns; + const headColors = generateHeaderColors(head.length); + const chars = compact ? compactModeChars : undefined; + + const colAligns: ('left' | 'right' | 'center')[] = []; + + for (const column of head) { + colAligns.push(this.options.columnAlignments?.[column] || 'left'); + } + + const table = new Table({ + head, + style: { + head: headColors, + compact, + }, + colAligns, + chars, + }); + + for (const rowData of this.rows) { + const row = head.map((col) => rowData[col]); + table.push(row); + } + + return table.toString(); + } +} diff --git a/yarn.lock b/yarn.lock index be61bada..af5e8b43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2791,13 +2791,6 @@ __metadata: languageName: node linkType: hard -"@types/cli-table@npm:^0": - version: 0.3.4 - resolution: "@types/cli-table@npm:0.3.4" - checksum: 10c0/dc266f9b8daae5efab0741546f6edccb96ece167a80ebf0c389d811969ea42f3f340575cc38923abdebae8fed1eec322c6769db1e0e4280bd5360daee7cef0ca - languageName: node - linkType: hard - "@types/connect@npm:*": version: 3.4.38 resolution: "@types/connect@npm:3.4.38" @@ -3628,7 +3621,6 @@ __metadata: "@types/adm-zip": "npm:^0.5.5" "@types/archiver": "npm:^6.0.2" "@types/chai": "npm:^4.3.17" - "@types/cli-table": "npm:^0" "@types/cors": "npm:^2.8.17" "@types/express": "npm:^4.17.21" "@types/fs-extra": "npm:^11" @@ -3651,7 +3643,7 @@ __metadata: axios: "npm:~1.7.3" chai: "npm:^4.4.1" chalk: "npm:~5.3.0" - cli-table: "npm:^0.3.11" + cli-table3: "npm:^0.6.5" computer-name: "npm:~0.1.0" configparser: "npm:~0.3.10" cors: "npm:~2.8.5" @@ -4547,12 +4539,16 @@ __metadata: languageName: node linkType: hard -"cli-table@npm:^0.3.11": - version: 0.3.11 - resolution: "cli-table@npm:0.3.11" +"cli-table3@npm:^0.6.5": + version: 0.6.5 + resolution: "cli-table3@npm:0.6.5" dependencies: - colors: "npm:1.0.3" - checksum: 10c0/6e31da4e19e942bf01749ff78d7988b01e0101955ce2b1e413eecdc115d4bb9271396464761491256a7d3feeedb5f37ae505f4314c4f8044b5d0f4b579c18f29 + "@colors/colors": "npm:1.5.0" + string-width: "npm:^4.2.0" + dependenciesMeta: + "@colors/colors": + optional: true + checksum: 10c0/d7cc9ed12212ae68241cc7a3133c52b844113b17856e11f4f81308acc3febcea7cc9fd298e70933e294dd642866b29fd5d113c2c098948701d0c35f09455de78 languageName: node linkType: hard @@ -4655,13 +4651,6 @@ __metadata: languageName: node linkType: hard -"colors@npm:1.0.3": - version: 1.0.3 - resolution: "colors@npm:1.0.3" - checksum: 10c0/f9e40dd8b3e1a65378a7ced3fced15ddfd60aaf38e99a7521a7fdb25056b15e092f651cd0f5aa1e9b04fa8ce3616d094e07fc6c2bb261e24098db1ddd3d09a1d - languageName: node - linkType: hard - "combined-stream@npm:^1.0.8": version: 1.0.8 resolution: "combined-stream@npm:1.0.8" From d04b77690bbebb55d2130e9bafe047a5e813f36d Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Wed, 11 Sep 2024 10:29:17 +0300 Subject: [PATCH 6/8] chore: alignment issues --- src/commands/runs/ls.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/runs/ls.ts b/src/commands/runs/ls.ts index 9696a3d3..6e84d791 100644 --- a/src/commands/runs/ls.ts +++ b/src/commands/runs/ls.ts @@ -18,6 +18,7 @@ const table = new ResponsiveTable({ Results: 'right', Usage: 'right', Took: 'right', + 'Build No.': 'right', }, }); From 135701e62f833324f0d6d2ef5fd8021352e3c94b Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Wed, 11 Sep 2024 12:24:55 +0300 Subject: [PATCH 7/8] chore: bump to 0.21, drop python 3.8 --- .github/workflows/check.yaml | 2 +- package.json | 2 +- src/commands/create.ts | 6 +++--- src/commands/run.ts | 6 +++--- src/lib/consts.ts | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 7cd3eddc..3fb38818 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -62,7 +62,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12"] runs-on: ${{ matrix.os }} steps: diff --git a/package.json b/package.json index 830f37eb..82601dd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "apify-cli", - "version": "0.20.7", + "version": "0.21.0", "description": "Apify command-line interface (CLI) helps you manage the Apify cloud platform and develop, build, and deploy Apify Actors.", "exports": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/src/commands/create.ts b/src/commands/create.ts index 19c9b470..7133d33f 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -235,16 +235,16 @@ export class CreateCommand extends ApifyCommand { dependenciesInstalled = true; } else { warning({ - message: `Python Actors require Python 3.8 or higher, but you have Python ${pythonVersion}!`, + message: `Python Actors require Python 3.9 or higher, but you have Python ${pythonVersion}!`, }); warning({ - message: 'Please install Python 3.8 or higher to be able to run Python Actors locally.', + message: 'Please install Python 3.9 or higher to be able to run Python Actors locally.', }); } } else { warning({ message: - 'No Python detected! Please install Python 3.8 or higher to be able to run Python Actors locally.', + 'No Python detected! Please install Python 3.9 or higher to be able to run Python Actors locally.', }); } } diff --git a/src/commands/run.ts b/src/commands/run.ts index 706f3fbe..bbc08800 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -341,16 +341,16 @@ export class RunCommand extends ApifyCommand { } } else { error({ - message: `Python Actors require Python 3.8 or higher, but you have Python ${pythonVersion}!`, + message: `Python Actors require Python 3.9 or higher, but you have Python ${pythonVersion}!`, }); error({ - message: 'Please install Python 3.8 or higher to be able to run Python Actors locally.', + message: 'Please install Python 3.9 or higher to be able to run Python Actors locally.', }); } } else { error({ message: - 'No Python detected! Please install Python 3.8 or higher to be able to run Python Actors locally.', + 'No Python detected! Please install Python 3.9 or higher to be able to run Python Actors locally.', }); } } diff --git a/src/lib/consts.ts b/src/lib/consts.ts index 6450a27d..d2aaf4e4 100644 --- a/src/lib/consts.ts +++ b/src/lib/consts.ts @@ -78,7 +78,7 @@ export const CURRENT_APIFY_CLI_VERSION = pkg.version; export const APIFY_CLIENT_DEFAULT_HEADERS = { 'X-Apify-Request-Origin': META_ORIGINS.CLI }; -export const MINIMUM_SUPPORTED_PYTHON_VERSION = '3.8.0'; +export const MINIMUM_SUPPORTED_PYTHON_VERSION = '3.9.0'; export const PYTHON_VENV_PATH = '.venv'; From d028181aff42da8c72b2215be73c0006a17c6691 Mon Sep 17 00:00:00 2001 From: Vlad Frangu Date: Wed, 11 Sep 2024 13:25:27 +0300 Subject: [PATCH 8/8] chore: "better" todo comment --- src/commands/runs/ls.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/runs/ls.ts b/src/commands/runs/ls.ts index 6e84d791..904c8533 100644 --- a/src/commands/runs/ls.ts +++ b/src/commands/runs/ls.ts @@ -56,7 +56,7 @@ export class RunsLsCommand extends ApifyCommand { const client = await getLoggedClientOrThrow(); - // TODO: technically speaking, we don't *need* an actor id to list builds. But it makes more sense to have a table of builds for a specific actor. + // Should we allow users to list any runs, not just actor-specific runs? Right now it works like `builds ls`, requiring an actor const ctx = await resolveActorContext({ providedActorNameOrId: actor, client }); if (!ctx.valid) {