Skip to content

Commit

Permalink
Merge pull request #765 from salesforcecli/sm/sandboxes-in-list
Browse files Browse the repository at this point in the history
Sm/sandboxes-in-list
  • Loading branch information
WillieRuemmele committed Aug 9, 2023
2 parents bd6bd7f + def136b commit b03da3c
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 106 deletions.
2 changes: 1 addition & 1 deletion messages/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Use one of the "org login" commands or "org create scratch" to add or create a s

# noResultsFound

No non-scratch orgs found.
No Orgs found.

# cleanWarning

Expand Down
23 changes: 21 additions & 2 deletions schemas/org-list.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,35 @@
"type": "array",
"items": {
"$ref": "#/definitions/ExtendedAuthFields"
}
},
"deprecated": "preserved for backward json compatibility. Duplicates devHubs, sandboxes, regularOrgs, which should be preferred"
},
"scratchOrgs": {
"type": "array",
"items": {
"$ref": "#/definitions/FullyPopulatedScratchOrgFields"
}
},
"sandboxes": {
"type": "array",
"items": {
"$ref": "#/definitions/ExtendedAuthFields"
}
},
"other": {
"type": "array",
"items": {
"$ref": "#/definitions/ExtendedAuthFields"
}
},
"devHubs": {
"type": "array",
"items": {
"$ref": "#/definitions/ExtendedAuthFields"
}
}
},
"required": ["nonScratchOrgs", "scratchOrgs"],
"required": ["nonScratchOrgs", "scratchOrgs", "sandboxes", "other", "devHubs"],
"additionalProperties": false
},
"ExtendedAuthFields": {
Expand Down
235 changes: 145 additions & 90 deletions src/commands/org/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,28 @@
import { Flags, loglevel, SfCommand } from '@salesforce/sf-plugins-core';
import { AuthInfo, ConfigAggregator, ConfigInfo, Connection, Org, SfError, Messages, Logger } from '@salesforce/core';
import { Interfaces } from '@oclif/core';
import * as chalk from 'chalk';
import { OrgListUtil, identifyActiveOrgByStatus } from '../../shared/orgListUtil';
import { getStyledObject } from '../../shared/orgHighlighter';
import { ExtendedAuthFields, FullyPopulatedScratchOrgFields } from '../../shared/orgTypes';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-org', 'list');

export const defaultOrgEmoji = '🍁';
export const defaultHubEmoji = '🌳';

export type OrgListResult = {
/**
* @deprecated
* preserved for backward json compatibility. Duplicates devHubs, sandboxes, regularOrgs, which should be preferred*/
nonScratchOrgs: ExtendedAuthFields[];
scratchOrgs: FullyPopulatedScratchOrgFields[];
sandboxes: ExtendedAuthFields[];
other: ExtendedAuthFields[];
devHubs: ExtendedAuthFields[];
};

export class OrgListCommand extends SfCommand<OrgListResult> {
public static readonly summary = messages.getMessage('summary');
public static readonly examples = messages.getMessages('examples');
Expand Down Expand Up @@ -56,6 +67,9 @@ export class OrgListCommand extends SfCommand<OrgListResult> {
this.flags = flags;
const metaConfigs = await OrgListUtil.readLocallyValidatedMetaConfigsGroupedByOrgType(fileNames, flags);
const groupedSortedOrgs = {
devHubs: metaConfigs.devHubs.map(decorateWithDefaultStatus).sort(comparator),
other: metaConfigs.other.map(decorateWithDefaultStatus).sort(comparator),
sandboxes: metaConfigs.sandboxes.map(decorateWithDefaultStatus).sort(comparator),
nonScratchOrgs: metaConfigs.nonScratchOrgs.map(decorateWithDefaultStatus).sort(comparator),
scratchOrgs: metaConfigs.scratchOrgs.map(decorateWithDefaultStatus).sort(comparator),
expiredScratchOrgs: metaConfigs.scratchOrgs.filter((org) => !identifyActiveOrgByStatus(org)),
Expand All @@ -70,15 +84,29 @@ export class OrgListCommand extends SfCommand<OrgListResult> {
}

const result = {
other: groupedSortedOrgs.other,
sandboxes: groupedSortedOrgs.sandboxes,
nonScratchOrgs: groupedSortedOrgs.nonScratchOrgs,
devHubs: groupedSortedOrgs.devHubs,
scratchOrgs: flags.all
? groupedSortedOrgs.scratchOrgs
: groupedSortedOrgs.scratchOrgs.filter(identifyActiveOrgByStatus),
};

this.printOrgTable(result.nonScratchOrgs, flags['skip-connection-status']);
this.printOrgTable({
devHubs: result.devHubs,
other: result.other,
sandboxes: result.sandboxes,
scratchOrgs: result.scratchOrgs,
skipconnectionstatus: flags['skip-connection-status'],
});

this.printScratchOrgTable(result.scratchOrgs);
this.info(
`
Legend: ${defaultHubEmoji}=Default DevHub, ${defaultOrgEmoji}=Default Org ${
flags.all ? '' : ' Use --all to see expired and deleted scratch orgs'
}`
);

return result;
}
Expand Down Expand Up @@ -112,108 +140,97 @@ export class OrgListCommand extends SfCommand<OrgListResult> {
);
}

protected printOrgTable(nonScratchOrgs: ExtendedAuthFields[], skipconnectionstatus: boolean): void {
if (!nonScratchOrgs.length) {
this.log(messages.getMessage('noResultsFound'));
} else {
const rows = nonScratchOrgs
.map((row) => getStyledObject(row))
.map((org) =>
Object.fromEntries(
Object.entries(org).filter(([key]) =>
['defaultMarker', 'alias', 'username', 'orgId', 'connectedStatus'].includes(key)
)
)
);

this.table(
rows,
{
defaultMarker: {
header: '',
get: (data): string => data.defaultMarker ?? '',
},
alias: {
header: 'ALIAS',
get: (data): string => data.alias ?? '',
},
username: { header: 'USERNAME' },
orgId: { header: 'ORG ID' },
...(!skipconnectionstatus ? { connectedStatus: { header: 'CONNECTED STATUS' } } : {}),
},
{
title: 'Non-scratch orgs',
}
);
protected printOrgTable({
devHubs,
scratchOrgs,
other,
sandboxes,
skipconnectionstatus,
}: {
devHubs: ExtendedAuthFields[];
other: ExtendedAuthFields[];
sandboxes: ExtendedAuthFields[];
scratchOrgs: FullyPopulatedScratchOrgFields[];
skipconnectionstatus: boolean;
}): void {
if (!devHubs.length && !other.length && !sandboxes.length) {
this.info(messages.getMessage('noResultsFound'));
return;
}
}
const allOrgs: Array<FullyPopulatedScratchOrgFields | ExtendedAuthFieldsWithType> = [
...devHubs
.map(addType('DevHub'))
.map(colorEveryFieldButConnectedStatus(chalk.cyanBright))
.map((row) => getStyledObject(row))
.map(statusToEmoji),

private printScratchOrgTable(scratchOrgs: FullyPopulatedScratchOrgFields[]): void {
if (scratchOrgs.length === 0) {
this.log(messages.getMessage('noActiveScratchOrgs'));
} else {
// One or more rows are available.
// we only need a few of the props for our table. Oclif table doesn't like extra props non-string props.
const rows = scratchOrgs
.map(getStyledObject)
.map((org) =>
Object.fromEntries(
Object.entries(org).filter(([key]) =>
[
'defaultMarker',
'alias',
'username',
'orgId',
'status',
'expirationDate',
'devHubOrgId',
'createdDate',
'instanceUrl',
].includes(key)
)
)
);
this.table(
rows,
{
defaultMarker: {
header: '',
get: (data): string => data.defaultMarker ?? '',
},
alias: {
header: 'ALIAS',
get: (data): string => data.alias ?? '',
},
username: { header: 'USERNAME' },
orgId: { header: 'ORG ID' },
...(this.flags.all || this.flags.verbose ? { status: { header: 'STATUS' } } : {}),
...(this.flags.verbose
? {
devHubOrgId: { header: 'DEV HUB' },
createdDate: { header: 'CREATED DATE' },
instanceUrl: { header: 'INSTANCE URL' },
}
: {}),
expirationDate: { header: 'EXPIRATION DATE' },
...other
.map(colorEveryFieldButConnectedStatus(chalk.magentaBright))
.map((row) => getStyledObject(row))
.map(statusToEmoji),

...sandboxes
.map(addType('Sandbox'))
.map(colorEveryFieldButConnectedStatus(chalk.yellowBright))
.map((row) => getStyledObject(row))
.map(statusToEmoji),

...scratchOrgs
.map((row) => ({ ...row, type: 'Scratch' }))
.map(convertScratchOrgStatus)
.map((row) => getStyledObject(row))
.map(statusToEmoji),
];

this.table(
allOrgs.map((org) => Object.fromEntries(Object.entries(org).filter(fieldFilter))),
{
defaultMarker: {
header: '',
},
{
title: 'Scratch orgs',
}
);
}
type: {
header: 'Type',
},
alias: {
header: 'Alias',
},
username: { header: 'Username' },
orgId: { header: 'Org ID' },
...(!skipconnectionstatus ? { connectedStatus: { header: 'Status' } } : {}),
...(this.flags.verbose
? {
instanceUrl: { header: 'Instance URL' },
devHubOrgId: { header: 'Dev Hub ID' },
createdDate: {
header: 'Created',
get: (data): string => (data.createdDate as string)?.split('T')?.[0] ?? '',
},
}
: {}),
expirationDate: { header: 'Expires' },
}
);
}
}

const decorateWithDefaultStatus = <T extends ExtendedAuthFields | FullyPopulatedScratchOrgFields>(val: T): T => ({
...val,
...(val.isDefaultDevHubUsername ? { defaultMarker: '(D)' } : {}),
...(val.isDefaultUsername ? { defaultMarker: '(U)' } : {}),
...(val.isDefaultDevHubUsername && val.isDefaultUsername ? { defaultMarker: '(D),(U)' } : {}),
});

const statusToEmoji = <T extends ExtendedAuthFields | FullyPopulatedScratchOrgFields>(val: T): T => ({
...val,
defaultMarker: val.defaultMarker?.replace('(D)', defaultHubEmoji)?.replace('(U)', defaultOrgEmoji),
});

const EMPTIES_LAST = 'zzzzzzzzzz';

// sort by alias then username
const comparator = <T extends ExtendedAuthFields | FullyPopulatedScratchOrgFields>(a: T, b: T): number => {
const aliasCompareResult = (a.alias ?? '').localeCompare(b.alias ?? '');
return aliasCompareResult !== 0 ? aliasCompareResult : (a.username ?? '').localeCompare(b.username);
const aliasCompareResult = (a.alias ?? EMPTIES_LAST).localeCompare(b.alias ?? EMPTIES_LAST);
return aliasCompareResult !== 0 ? aliasCompareResult : (a.username ?? EMPTIES_LAST).localeCompare(b.username);
};

const getAuthFileNames = async (): Promise<string[]> => {
Expand All @@ -228,3 +245,41 @@ const getAuthFileNames = async (): Promise<string[]> => {
}
}
};

type ExtendedAuthFieldsWithType = ExtendedAuthFields & { type?: string };

const addType =
(type: string) =>
(val: ExtendedAuthFields): ExtendedAuthFieldsWithType => ({ ...val, type });

const colorEveryFieldButConnectedStatus =
(colorFn: chalk.Chalk) =>
(row: ExtendedAuthFieldsWithType): ExtendedAuthFieldsWithType =>
Object.fromEntries(
Object.entries(row).map(([key, val]) => [
key,
typeof val === 'string' && key !== 'connectedStatus' ? colorFn(val) : val,
])
// TS is not smart enough to know this didn't change any types
) as ExtendedAuthFieldsWithType;

const fieldFilter = ([key]: [string, string]): boolean =>
[
'defaultMarker',
'alias',
'username',
'orgId',
'status',
'connectedStatus',
'expirationDate',
'devHubOrgId',
'createdDate',
'instanceUrl',
'type',
'createdDate',
].includes(key);

const convertScratchOrgStatus = (
row: FullyPopulatedScratchOrgFields
): FullyPopulatedScratchOrgFields & { connectedStatus: string } =>
({ ...row, connectedStatus: row.status } as FullyPopulatedScratchOrgFields & { connectedStatus: string });
16 changes: 8 additions & 8 deletions src/shared/orgHighlighter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const styledProperties = new Map<string, Map<string, chalk.Chalk>>([
'connectedStatus',
new Map([
['Connected', chalk.green],
['Active', chalk.green],
['else', chalk.red],
]),
],
Expand All @@ -34,11 +35,10 @@ export const getStyledValue = (key: string, value: string): string => {
return chalkMethod(value);
};

export const getStyledObject = (
objectToStyle: ExtendedAuthFields | FullyPopulatedScratchOrgFields | Record<string, string>
): Record<string, string> => {
const clonedObject = { ...objectToStyle };
return Object.fromEntries(
Object.entries(clonedObject).map(([key, value]) => [key, getStyledValue(key, value as string)])
);
};
export const getStyledObject = <T extends ExtendedAuthFields | FullyPopulatedScratchOrgFields>(objectToStyle: T): T =>
Object.fromEntries(
Object.entries(objectToStyle).map(([key, value]) => [
key,
typeof value === 'string' ? getStyledValue(key, value) : value,
])
) as T;
Loading

0 comments on commit b03da3c

Please sign in to comment.