Skip to content

Commit ec974b8

Browse files
committed
feat: pretty org list table
1 parent 5835857 commit ec974b8

File tree

4 files changed

+73
-57
lines changed

4 files changed

+73
-57
lines changed

src/commands/org/list.ts

Lines changed: 53 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import { ExtendedAuthFields, FullyPopulatedScratchOrgFields } from '../../shared
1616
Messages.importMessagesDirectory(__dirname);
1717
const messages = Messages.loadMessages('@salesforce/plugin-org', 'list');
1818

19+
const defaultOrgEmoji = '🍁';
20+
const defaultHubEmoji = '🌳';
21+
1922
export type OrgListResult = {
2023
/**
2124
* @deprecated
@@ -94,11 +97,18 @@ export class OrgListCommand extends SfCommand<OrgListResult> {
9497
devHubs: result.devHubs,
9598
regularOrgs: result.regularOrgs,
9699
sandboxes: result.sandboxes,
100+
scratchOrgs: result.scratchOrgs,
97101
skipconnectionstatus: flags['skip-connection-status'],
98102
});
99-
this.printScratchOrgTable(result.scratchOrgs);
103+
// this.printScratchOrgTable(result.scratchOrgs);
104+
105+
this.log(
106+
`
107+
Legend: ${defaultHubEmoji}=Default DevHub, ${defaultOrgEmoji}=Default Org ${
108+
this.flags.all ? ' Use --all to see expired and deleted scratch orgs' : ''
109+
}`
110+
);
100111

101-
this.info('Legend: (D)=Default DevHub, (U)=Default Org');
102112
return result;
103113
}
104114

@@ -133,43 +143,48 @@ export class OrgListCommand extends SfCommand<OrgListResult> {
133143

134144
protected printOrgTable({
135145
devHubs,
146+
scratchOrgs,
136147
regularOrgs,
137148
sandboxes,
138149
skipconnectionstatus,
139150
}: {
140151
devHubs: ExtendedAuthFields[];
141152
regularOrgs: ExtendedAuthFields[];
142153
sandboxes: ExtendedAuthFields[];
154+
scratchOrgs: FullyPopulatedScratchOrgFields[];
143155
skipconnectionstatus: boolean;
144156
}): void {
145157
if (!devHubs.length && !regularOrgs.length && !sandboxes.length) {
146158
this.info(messages.getMessage('noResultsFound'));
147159
return;
148160
}
149-
this.log();
150-
this.info('Non-scratch orgs');
151-
const nonScratchOrgs = [
161+
const allOrgs: Array<FullyPopulatedScratchOrgFields | ExtendedAuthFieldsWithType> = [
152162
...devHubs
153163
.map(addType('DevHub'))
154164
.map(colorEveryFieldButConnectedStatus(chalk.cyanBright))
155-
.map((row) => getStyledObject(row)),
165+
.map((row) => getStyledObject(row))
166+
.map(statusToEmoji),
156167

157-
...regularOrgs.map(colorEveryFieldButConnectedStatus(chalk.magentaBright)).map((row) => getStyledObject(row)),
168+
...regularOrgs
169+
.map(colorEveryFieldButConnectedStatus(chalk.magentaBright))
170+
.map((row) => getStyledObject(row))
171+
.map(statusToEmoji),
158172

159173
...sandboxes
160174
.map(addType('Sandbox'))
161175
.map(colorEveryFieldButConnectedStatus(chalk.yellowBright))
162-
.map((row) => getStyledObject(row)),
176+
.map((row) => getStyledObject(row))
177+
.map(statusToEmoji),
178+
179+
...scratchOrgs
180+
.map((row) => ({ ...row, type: 'Scratch' }))
181+
.map(convertScratchOrgStatus)
182+
.map((row) => getStyledObject(row))
183+
.map(statusToEmoji),
163184
];
164185

165186
this.table(
166-
nonScratchOrgs.map((org) =>
167-
Object.fromEntries(
168-
Object.entries(org).filter(([key]) =>
169-
['type', 'defaultMarker', 'alias', 'username', 'orgId', 'connectedStatus'].includes(key)
170-
)
171-
)
172-
),
187+
allOrgs.map((org) => Object.fromEntries(Object.entries(org).filter(fieldFilter))),
173188
{
174189
defaultMarker: {
175190
header: '',
@@ -183,51 +198,32 @@ export class OrgListCommand extends SfCommand<OrgListResult> {
183198
username: { header: 'Username' },
184199
orgId: { header: 'Org ID' },
185200
...(!skipconnectionstatus ? { connectedStatus: { header: 'Status' } } : {}),
186-
}
187-
);
188-
189-
this.log();
190-
}
191-
192-
private printScratchOrgTable(scratchOrgs: FullyPopulatedScratchOrgFields[]): void {
193-
if (scratchOrgs.length === 0) {
194-
this.info(messages.getMessage('noActiveScratchOrgs'));
195-
} else {
196-
this.info(this.flags.all ? 'Scratch Orgs' : 'Active Scratch Orgs (use --all to see all)');
197-
198-
// One or more rows are available.
199-
// we only need a few of the props for our table. Oclif table doesn't like extra props non-string props.
200-
const rows = scratchOrgs
201-
.map(getStyledObject)
202-
.map((org) => Object.fromEntries(Object.entries(org).filter(scratchOrgFieldFilter)));
203-
this.table(rows, {
204-
defaultMarker: {
205-
header: '',
206-
},
207-
alias: {
208-
header: 'Alias',
209-
},
210-
username: { header: 'Username' },
211-
orgId: { header: 'Org ID' },
212-
...(this.flags.all || this.flags.verbose ? { status: { header: 'Status' } } : {}),
213201
...(this.flags.verbose
214202
? {
215-
devHubOrgId: { header: 'Dev Hub ID' },
216203
instanceUrl: { header: 'Instance URL' },
217-
createdDate: { header: 'Created', get: (data): string => data.createdDate?.split('T')[0] ?? '' },
204+
devHubOrgId: { header: 'Dev Hub ID' },
205+
createdDate: {
206+
header: 'Created',
207+
get: (data): string => (data.createdDate as string)?.split('T')?.[0] ?? '',
208+
},
218209
}
219210
: {}),
220211
expirationDate: { header: 'Expires' },
221-
});
222-
}
223-
this.log();
212+
}
213+
);
224214
}
225215
}
226216

227217
const decorateWithDefaultStatus = <T extends ExtendedAuthFields | FullyPopulatedScratchOrgFields>(val: T): T => ({
228218
...val,
229219
...(val.isDefaultDevHubUsername ? { defaultMarker: '(D)' } : {}),
230220
...(val.isDefaultUsername ? { defaultMarker: '(U)' } : {}),
221+
...(val.isDefaultDevHubUsername && val.isDefaultUsername ? { defaultMarker: '(D),(U)' } : {}),
222+
});
223+
224+
const statusToEmoji = <T extends ExtendedAuthFields | FullyPopulatedScratchOrgFields>(val: T): T => ({
225+
...val,
226+
defaultMarker: val.defaultMarker?.replace('(D)', defaultHubEmoji)?.replace('(U)', defaultOrgEmoji),
231227
});
232228

233229
// sort by alias then username
@@ -251,6 +247,7 @@ const getAuthFileNames = async (): Promise<string[]> => {
251247
};
252248

253249
type ExtendedAuthFieldsWithType = ExtendedAuthFields & { type?: string };
250+
254251
const addType =
255252
(type: string) =>
256253
(val: ExtendedAuthFields): ExtendedAuthFieldsWithType => ({ ...val, type });
@@ -266,15 +263,23 @@ const colorEveryFieldButConnectedStatus =
266263
// TS is not smart enough to know this didn't change any types
267264
) as ExtendedAuthFieldsWithType;
268265

269-
const scratchOrgFieldFilter = ([key]: [string, string]): boolean =>
266+
const fieldFilter = ([key]: [string, string]): boolean =>
270267
[
271268
'defaultMarker',
272269
'alias',
273270
'username',
274271
'orgId',
275272
'status',
273+
'connectedStatus',
276274
'expirationDate',
277275
'devHubOrgId',
278276
'createdDate',
279277
'instanceUrl',
278+
'type',
279+
'createdDate',
280280
].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;

test/shared/orgHighlighter.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77
import { expect } from 'chai';
88
import * as chalk from 'chalk';
9+
import { ExtendedAuthFields } from '../../src/shared/orgTypes';
910
import { getStyledObject, getStyledValue } from '../../src/shared/orgHighlighter';
1011

1112
describe('highlights value from key-value pair', () => {
@@ -26,7 +27,8 @@ describe('highlights object with green, red, and non-colored', () => {
2627
status: 'Active',
2728
otherProp: 'foo',
2829
connectedStatus: 'Not found',
29-
};
30+
// I know it's not, but it's a test
31+
} as unknown as ExtendedAuthFields;
3032
expect(getStyledObject(object)).to.deep.equal({
3133
status: chalk.green('Active'),
3234
otherProp: 'foo',

test/shared/orgListMock.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ class OrgListMock {
9696
connectedStatus: 'Connected',
9797
},
9898
],
99+
devHubs: [
100+
{
101+
username: '[email protected]',
102+
isDevHub: true,
103+
connectedStatus: 'Connected',
104+
},
105+
],
106+
sandboxes: [],
107+
regularOrgs: [],
99108
};
100109

101110
public static get devHubUsername(): string {

0 commit comments

Comments
 (0)