Skip to content

Commit b03da3c

Browse files
Merge pull request #765 from salesforcecli/sm/sandboxes-in-list
Sm/sandboxes-in-list
2 parents bd6bd7f + def136b commit b03da3c

File tree

8 files changed

+210
-106
lines changed

8 files changed

+210
-106
lines changed

messages/list.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Use one of the "org login" commands or "org create scratch" to add or create a s
5858

5959
# noResultsFound
6060

61-
No non-scratch orgs found.
61+
No Orgs found.
6262

6363
# cleanWarning
6464

schemas/org-list.json

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,35 @@
99
"type": "array",
1010
"items": {
1111
"$ref": "#/definitions/ExtendedAuthFields"
12-
}
12+
},
13+
"deprecated": "preserved for backward json compatibility. Duplicates devHubs, sandboxes, regularOrgs, which should be preferred"
1314
},
1415
"scratchOrgs": {
1516
"type": "array",
1617
"items": {
1718
"$ref": "#/definitions/FullyPopulatedScratchOrgFields"
1819
}
20+
},
21+
"sandboxes": {
22+
"type": "array",
23+
"items": {
24+
"$ref": "#/definitions/ExtendedAuthFields"
25+
}
26+
},
27+
"other": {
28+
"type": "array",
29+
"items": {
30+
"$ref": "#/definitions/ExtendedAuthFields"
31+
}
32+
},
33+
"devHubs": {
34+
"type": "array",
35+
"items": {
36+
"$ref": "#/definitions/ExtendedAuthFields"
37+
}
1938
}
2039
},
21-
"required": ["nonScratchOrgs", "scratchOrgs"],
40+
"required": ["nonScratchOrgs", "scratchOrgs", "sandboxes", "other", "devHubs"],
2241
"additionalProperties": false
2342
},
2443
"ExtendedAuthFields": {

src/commands/org/list.ts

Lines changed: 145 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,28 @@
88
import { Flags, loglevel, SfCommand } from '@salesforce/sf-plugins-core';
99
import { AuthInfo, ConfigAggregator, ConfigInfo, Connection, Org, SfError, Messages, Logger } from '@salesforce/core';
1010
import { Interfaces } from '@oclif/core';
11+
import * as chalk from 'chalk';
1112
import { OrgListUtil, identifyActiveOrgByStatus } from '../../shared/orgListUtil';
1213
import { getStyledObject } from '../../shared/orgHighlighter';
1314
import { ExtendedAuthFields, FullyPopulatedScratchOrgFields } from '../../shared/orgTypes';
1415

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

19+
export const defaultOrgEmoji = '🍁';
20+
export const defaultHubEmoji = '🌳';
21+
1822
export type OrgListResult = {
23+
/**
24+
* @deprecated
25+
* preserved for backward json compatibility. Duplicates devHubs, sandboxes, regularOrgs, which should be preferred*/
1926
nonScratchOrgs: ExtendedAuthFields[];
2027
scratchOrgs: FullyPopulatedScratchOrgFields[];
28+
sandboxes: ExtendedAuthFields[];
29+
other: ExtendedAuthFields[];
30+
devHubs: ExtendedAuthFields[];
2131
};
32+
2233
export class OrgListCommand extends SfCommand<OrgListResult> {
2334
public static readonly summary = messages.getMessage('summary');
2435
public static readonly examples = messages.getMessages('examples');
@@ -56,6 +67,9 @@ export class OrgListCommand extends SfCommand<OrgListResult> {
5667
this.flags = flags;
5768
const metaConfigs = await OrgListUtil.readLocallyValidatedMetaConfigsGroupedByOrgType(fileNames, flags);
5869
const groupedSortedOrgs = {
70+
devHubs: metaConfigs.devHubs.map(decorateWithDefaultStatus).sort(comparator),
71+
other: metaConfigs.other.map(decorateWithDefaultStatus).sort(comparator),
72+
sandboxes: metaConfigs.sandboxes.map(decorateWithDefaultStatus).sort(comparator),
5973
nonScratchOrgs: metaConfigs.nonScratchOrgs.map(decorateWithDefaultStatus).sort(comparator),
6074
scratchOrgs: metaConfigs.scratchOrgs.map(decorateWithDefaultStatus).sort(comparator),
6175
expiredScratchOrgs: metaConfigs.scratchOrgs.filter((org) => !identifyActiveOrgByStatus(org)),
@@ -70,15 +84,29 @@ export class OrgListCommand extends SfCommand<OrgListResult> {
7084
}
7185

7286
const result = {
87+
other: groupedSortedOrgs.other,
88+
sandboxes: groupedSortedOrgs.sandboxes,
7389
nonScratchOrgs: groupedSortedOrgs.nonScratchOrgs,
90+
devHubs: groupedSortedOrgs.devHubs,
7491
scratchOrgs: flags.all
7592
? groupedSortedOrgs.scratchOrgs
7693
: groupedSortedOrgs.scratchOrgs.filter(identifyActiveOrgByStatus),
7794
};
7895

79-
this.printOrgTable(result.nonScratchOrgs, flags['skip-connection-status']);
96+
this.printOrgTable({
97+
devHubs: result.devHubs,
98+
other: result.other,
99+
sandboxes: result.sandboxes,
100+
scratchOrgs: result.scratchOrgs,
101+
skipconnectionstatus: flags['skip-connection-status'],
102+
});
80103

81-
this.printScratchOrgTable(result.scratchOrgs);
104+
this.info(
105+
`
106+
Legend: ${defaultHubEmoji}=Default DevHub, ${defaultOrgEmoji}=Default Org ${
107+
flags.all ? '' : ' Use --all to see expired and deleted scratch orgs'
108+
}`
109+
);
82110

83111
return result;
84112
}
@@ -112,108 +140,97 @@ export class OrgListCommand extends SfCommand<OrgListResult> {
112140
);
113141
}
114142

115-
protected printOrgTable(nonScratchOrgs: ExtendedAuthFields[], skipconnectionstatus: boolean): void {
116-
if (!nonScratchOrgs.length) {
117-
this.log(messages.getMessage('noResultsFound'));
118-
} else {
119-
const rows = nonScratchOrgs
120-
.map((row) => getStyledObject(row))
121-
.map((org) =>
122-
Object.fromEntries(
123-
Object.entries(org).filter(([key]) =>
124-
['defaultMarker', 'alias', 'username', 'orgId', 'connectedStatus'].includes(key)
125-
)
126-
)
127-
);
128-
129-
this.table(
130-
rows,
131-
{
132-
defaultMarker: {
133-
header: '',
134-
get: (data): string => data.defaultMarker ?? '',
135-
},
136-
alias: {
137-
header: 'ALIAS',
138-
get: (data): string => data.alias ?? '',
139-
},
140-
username: { header: 'USERNAME' },
141-
orgId: { header: 'ORG ID' },
142-
...(!skipconnectionstatus ? { connectedStatus: { header: 'CONNECTED STATUS' } } : {}),
143-
},
144-
{
145-
title: 'Non-scratch orgs',
146-
}
147-
);
143+
protected printOrgTable({
144+
devHubs,
145+
scratchOrgs,
146+
other,
147+
sandboxes,
148+
skipconnectionstatus,
149+
}: {
150+
devHubs: ExtendedAuthFields[];
151+
other: ExtendedAuthFields[];
152+
sandboxes: ExtendedAuthFields[];
153+
scratchOrgs: FullyPopulatedScratchOrgFields[];
154+
skipconnectionstatus: boolean;
155+
}): void {
156+
if (!devHubs.length && !other.length && !sandboxes.length) {
157+
this.info(messages.getMessage('noResultsFound'));
158+
return;
148159
}
149-
}
160+
const allOrgs: Array<FullyPopulatedScratchOrgFields | ExtendedAuthFieldsWithType> = [
161+
...devHubs
162+
.map(addType('DevHub'))
163+
.map(colorEveryFieldButConnectedStatus(chalk.cyanBright))
164+
.map((row) => getStyledObject(row))
165+
.map(statusToEmoji),
150166

151-
private printScratchOrgTable(scratchOrgs: FullyPopulatedScratchOrgFields[]): void {
152-
if (scratchOrgs.length === 0) {
153-
this.log(messages.getMessage('noActiveScratchOrgs'));
154-
} else {
155-
// One or more rows are available.
156-
// we only need a few of the props for our table. Oclif table doesn't like extra props non-string props.
157-
const rows = scratchOrgs
158-
.map(getStyledObject)
159-
.map((org) =>
160-
Object.fromEntries(
161-
Object.entries(org).filter(([key]) =>
162-
[
163-
'defaultMarker',
164-
'alias',
165-
'username',
166-
'orgId',
167-
'status',
168-
'expirationDate',
169-
'devHubOrgId',
170-
'createdDate',
171-
'instanceUrl',
172-
].includes(key)
173-
)
174-
)
175-
);
176-
this.table(
177-
rows,
178-
{
179-
defaultMarker: {
180-
header: '',
181-
get: (data): string => data.defaultMarker ?? '',
182-
},
183-
alias: {
184-
header: 'ALIAS',
185-
get: (data): string => data.alias ?? '',
186-
},
187-
username: { header: 'USERNAME' },
188-
orgId: { header: 'ORG ID' },
189-
...(this.flags.all || this.flags.verbose ? { status: { header: 'STATUS' } } : {}),
190-
...(this.flags.verbose
191-
? {
192-
devHubOrgId: { header: 'DEV HUB' },
193-
createdDate: { header: 'CREATED DATE' },
194-
instanceUrl: { header: 'INSTANCE URL' },
195-
}
196-
: {}),
197-
expirationDate: { header: 'EXPIRATION DATE' },
167+
...other
168+
.map(colorEveryFieldButConnectedStatus(chalk.magentaBright))
169+
.map((row) => getStyledObject(row))
170+
.map(statusToEmoji),
171+
172+
...sandboxes
173+
.map(addType('Sandbox'))
174+
.map(colorEveryFieldButConnectedStatus(chalk.yellowBright))
175+
.map((row) => getStyledObject(row))
176+
.map(statusToEmoji),
177+
178+
...scratchOrgs
179+
.map((row) => ({ ...row, type: 'Scratch' }))
180+
.map(convertScratchOrgStatus)
181+
.map((row) => getStyledObject(row))
182+
.map(statusToEmoji),
183+
];
184+
185+
this.table(
186+
allOrgs.map((org) => Object.fromEntries(Object.entries(org).filter(fieldFilter))),
187+
{
188+
defaultMarker: {
189+
header: '',
198190
},
199-
{
200-
title: 'Scratch orgs',
201-
}
202-
);
203-
}
191+
type: {
192+
header: 'Type',
193+
},
194+
alias: {
195+
header: 'Alias',
196+
},
197+
username: { header: 'Username' },
198+
orgId: { header: 'Org ID' },
199+
...(!skipconnectionstatus ? { connectedStatus: { header: 'Status' } } : {}),
200+
...(this.flags.verbose
201+
? {
202+
instanceUrl: { header: 'Instance URL' },
203+
devHubOrgId: { header: 'Dev Hub ID' },
204+
createdDate: {
205+
header: 'Created',
206+
get: (data): string => (data.createdDate as string)?.split('T')?.[0] ?? '',
207+
},
208+
}
209+
: {}),
210+
expirationDate: { header: 'Expires' },
211+
}
212+
);
204213
}
205214
}
206215

207216
const decorateWithDefaultStatus = <T extends ExtendedAuthFields | FullyPopulatedScratchOrgFields>(val: T): T => ({
208217
...val,
209218
...(val.isDefaultDevHubUsername ? { defaultMarker: '(D)' } : {}),
210219
...(val.isDefaultUsername ? { defaultMarker: '(U)' } : {}),
220+
...(val.isDefaultDevHubUsername && val.isDefaultUsername ? { defaultMarker: '(D),(U)' } : {}),
211221
});
212222

223+
const statusToEmoji = <T extends ExtendedAuthFields | FullyPopulatedScratchOrgFields>(val: T): T => ({
224+
...val,
225+
defaultMarker: val.defaultMarker?.replace('(D)', defaultHubEmoji)?.replace('(U)', defaultOrgEmoji),
226+
});
227+
228+
const EMPTIES_LAST = 'zzzzzzzzzz';
229+
213230
// sort by alias then username
214231
const comparator = <T extends ExtendedAuthFields | FullyPopulatedScratchOrgFields>(a: T, b: T): number => {
215-
const aliasCompareResult = (a.alias ?? '').localeCompare(b.alias ?? '');
216-
return aliasCompareResult !== 0 ? aliasCompareResult : (a.username ?? '').localeCompare(b.username);
232+
const aliasCompareResult = (a.alias ?? EMPTIES_LAST).localeCompare(b.alias ?? EMPTIES_LAST);
233+
return aliasCompareResult !== 0 ? aliasCompareResult : (a.username ?? EMPTIES_LAST).localeCompare(b.username);
217234
};
218235

219236
const getAuthFileNames = async (): Promise<string[]> => {
@@ -228,3 +245,41 @@ const getAuthFileNames = async (): Promise<string[]> => {
228245
}
229246
}
230247
};
248+
249+
type ExtendedAuthFieldsWithType = ExtendedAuthFields & { type?: string };
250+
251+
const addType =
252+
(type: string) =>
253+
(val: ExtendedAuthFields): ExtendedAuthFieldsWithType => ({ ...val, type });
254+
255+
const colorEveryFieldButConnectedStatus =
256+
(colorFn: chalk.Chalk) =>
257+
(row: ExtendedAuthFieldsWithType): ExtendedAuthFieldsWithType =>
258+
Object.fromEntries(
259+
Object.entries(row).map(([key, val]) => [
260+
key,
261+
typeof val === 'string' && key !== 'connectedStatus' ? colorFn(val) : val,
262+
])
263+
// TS is not smart enough to know this didn't change any types
264+
) as ExtendedAuthFieldsWithType;
265+
266+
const fieldFilter = ([key]: [string, string]): boolean =>
267+
[
268+
'defaultMarker',
269+
'alias',
270+
'username',
271+
'orgId',
272+
'status',
273+
'connectedStatus',
274+
'expirationDate',
275+
'devHubOrgId',
276+
'createdDate',
277+
'instanceUrl',
278+
'type',
279+
'createdDate',
280+
].includes(key);
281+
282+
const convertScratchOrgStatus = (
283+
row: FullyPopulatedScratchOrgFields
284+
): FullyPopulatedScratchOrgFields & { connectedStatus: string } =>
285+
({ ...row, connectedStatus: row.status } as FullyPopulatedScratchOrgFields & { connectedStatus: string });

src/shared/orgHighlighter.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const styledProperties = new Map<string, Map<string, chalk.Chalk>>([
1919
'connectedStatus',
2020
new Map([
2121
['Connected', chalk.green],
22+
['Active', chalk.green],
2223
['else', chalk.red],
2324
]),
2425
],
@@ -34,11 +35,10 @@ export const getStyledValue = (key: string, value: string): string => {
3435
return chalkMethod(value);
3536
};
3637

37-
export const getStyledObject = (
38-
objectToStyle: ExtendedAuthFields | FullyPopulatedScratchOrgFields | Record<string, string>
39-
): Record<string, string> => {
40-
const clonedObject = { ...objectToStyle };
41-
return Object.fromEntries(
42-
Object.entries(clonedObject).map(([key, value]) => [key, getStyledValue(key, value as string)])
43-
);
44-
};
38+
export const getStyledObject = <T extends ExtendedAuthFields | FullyPopulatedScratchOrgFields>(objectToStyle: T): T =>
39+
Object.fromEntries(
40+
Object.entries(objectToStyle).map(([key, value]) => [
41+
key,
42+
typeof value === 'string' ? getStyledValue(key, value) : value,
43+
])
44+
) as T;

0 commit comments

Comments
 (0)