Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sm/sandboxes-in-list #765

Merged
merged 9 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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