-
Notifications
You must be signed in to change notification settings - Fork 201
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #215 from raix/feature/issue-214-add-show-command
Feature/issue 214 add show command
- Loading branch information
Showing
6 changed files
with
225 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { HttpClient, HttpClientResponse } from 'vso-node-api/HttpClient'; | ||
import { PublishedExtension, ExtensionQueryFlags, FilterCriteria, SortOrderType, | ||
SortByType, ExtensionQueryFilterType, TypeInfo} from 'vso-node-api/interfaces/GalleryInterfaces'; | ||
import { IHeaders } from 'vso-node-api/interfaces/common/VsoBaseInterfaces'; | ||
import { ContractSerializer } from 'vso-node-api/Serialization'; | ||
|
||
export interface ExtensionQuery { | ||
pageNumber?: number; | ||
pageSize?: number; | ||
sortBy?: SortByType; | ||
sortOrder?: SortOrderType; | ||
flags?: ExtensionQueryFlags[]; | ||
criteria?: FilterCriteria[]; | ||
assetTypes?: string[]; | ||
} | ||
|
||
export class PublicGalleryAPI { | ||
client: HttpClient; | ||
|
||
constructor(public baseUrl: string, public apiVersion = '3.0-preview.1') { | ||
this.client = new HttpClient('vsce'); | ||
} | ||
|
||
post(url: string, data: string, additionalHeaders?: IHeaders): Promise<HttpClientResponse> { | ||
return this.client.post(`${this.baseUrl}/_apis/public${url}`, data, additionalHeaders); | ||
} | ||
|
||
extensionQuery({ | ||
pageNumber = 1, | ||
pageSize = 1, | ||
sortBy = SortByType.Relevance, | ||
sortOrder = SortOrderType.Default, | ||
flags = [], | ||
criteria = [], | ||
assetTypes = [], | ||
}: ExtensionQuery): Promise<PublishedExtension[]> { | ||
return this.post('/gallery/extensionquery', JSON.stringify({ | ||
filters: [{pageNumber, pageSize, criteria}], | ||
assetTypes, | ||
flags: flags.reduce((memo, flag) => memo | flag, 0) | ||
}), { | ||
Accept: `application/json;api-version=${this.apiVersion}`, | ||
'Content-Type': 'application/json', | ||
}) | ||
.then(res => res.readBody()) | ||
.then(data => JSON.parse(data)) | ||
.then(({results: [result = {}] = []}) => result) | ||
.then(({extensions = []}) => | ||
ContractSerializer.deserialize(extensions, TypeInfo.PublishedExtension, false, false) | ||
); | ||
} | ||
|
||
getExtension(extensionId: string, flags: ExtensionQueryFlags[] = []): Promise<PublishedExtension> { | ||
return this.extensionQuery({ | ||
criteria: [{ filterType: ExtensionQueryFilterType.Name, value: extensionId }], | ||
flags, | ||
}) | ||
.then(result => result.filter(({publisher: {publisherName}, extensionName}) => | ||
extensionId === `${publisherName}.${extensionName}`) | ||
) | ||
.then(([extension]) => extension); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { getPublicGalleryAPI } from './util'; | ||
import { ExtensionQueryFlags, PublishedExtension } from 'vso-node-api/interfaces/GalleryInterfaces'; | ||
import { ViewTable, formatDate, formatDateTime, ratingStars, tableView, indentRow, wordWrap } from './viewutils'; | ||
|
||
const limitVersions = 6; | ||
|
||
export interface ExtensionStatiticsMap { | ||
install: number; | ||
averagerating: number; | ||
ratingcount:number; | ||
} | ||
|
||
export function show(extensionId: string, json: boolean = false): Promise<any> { | ||
const flags = [ | ||
ExtensionQueryFlags.IncludeCategoryAndTags, | ||
ExtensionQueryFlags.IncludeMetadata, | ||
ExtensionQueryFlags.IncludeStatistics, | ||
ExtensionQueryFlags.IncludeVersions, | ||
]; | ||
return getPublicGalleryAPI() | ||
.getExtension(extensionId, flags) | ||
.then(extension => { | ||
if (json) { | ||
console.log(JSON.stringify(extension, undefined, '\t')); | ||
} else { | ||
if (extension === undefined) { | ||
console.log(`Error: Extension "${extensionId}" not found.`); | ||
} else { | ||
showOverview(extension); | ||
} | ||
} | ||
}); | ||
} | ||
|
||
function showOverview({ | ||
displayName, | ||
extensionName, | ||
shortDescription, | ||
versions, | ||
publisher: { | ||
displayName:publisherDisplayName, | ||
publisherName | ||
}, | ||
categories, | ||
tags, | ||
statistics, | ||
publishedDate, | ||
lastUpdated, | ||
}: PublishedExtension) { | ||
|
||
const [{ version = 'unknown' } = {}] = versions; | ||
|
||
// Create formatted table list of versions | ||
const versionList = <ViewTable>versions | ||
.slice(0, limitVersions) | ||
.map(({version, lastUpdated}) => [version, formatDate(lastUpdated)]); | ||
|
||
const { | ||
install: installs = 0, | ||
averagerating = 0, | ||
ratingcount = 0, | ||
} = statistics | ||
.reduce((map, {statisticName, value}) => ({ ...map, [statisticName]: value }), <ExtensionStatiticsMap>{}); | ||
|
||
// Render | ||
console.log([ | ||
`${displayName}`, | ||
`${publisherDisplayName} | ${'\u2913'}` + | ||
`${Number(installs).toLocaleString()} installs |` + | ||
` ${ratingStars(averagerating)} (${ratingcount})`, | ||
'', | ||
`${shortDescription}`, | ||
'', | ||
'Recent versions:', | ||
...(versionList.length ? tableView(versionList).map(indentRow) : ['no versions found']), | ||
'', | ||
'Categories:', | ||
` ${categories.join(', ')}`, | ||
'', | ||
'Tags:', | ||
` ${tags.join(', ')}`, | ||
'', | ||
'More info:', | ||
...tableView([ | ||
[ 'Uniq identifier:', `${publisherName}.${extensionName}` ], | ||
[ 'Version:', version ], | ||
[ 'Last updated:', formatDateTime(lastUpdated) ], | ||
[ 'Publisher:', publisherDisplayName ], | ||
[ 'Published at:', formatDate(publishedDate) ], | ||
]) | ||
.map(indentRow), | ||
'', | ||
'Statistics:', | ||
...tableView(<ViewTable>statistics.map(({statisticName, value}) => [statisticName, Number(value).toFixed(2)])) | ||
.map(indentRow), | ||
] | ||
.map(line => wordWrap(line)) | ||
.join('\n')); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
export type ViewTableRow = string[]; | ||
export type ViewTable = ViewTableRow[]; | ||
|
||
const fixedLocale = 'en-us'; | ||
const format = { | ||
date: { month: 'long', day: 'numeric', year: 'numeric' }, | ||
time: { hour: 'numeric', minute: 'numeric', second: 'numeric' }, | ||
}; | ||
|
||
export function formatDate(date) { return date.toLocaleString(fixedLocale, format.date); } | ||
export function formatTime(date) { return date.toLocaleString(fixedLocale, format.time); } | ||
export function formatDateTime(date) { return date.toLocaleString(fixedLocale, { ...format.date, ...format.time }); } | ||
|
||
export function repeatString(text: string, count: number): string { | ||
let result: string = ''; | ||
for (let i = 0; i < count; i++) { | ||
result += text; | ||
} | ||
return result; | ||
} | ||
|
||
export function ratingStars(rating: number, total = 5): string { | ||
const c = Math.min(Math.round(rating), total); | ||
return `${repeatString('\u{2605} ', c)}${repeatString('\u{2606} ', total - c)}`; | ||
} | ||
|
||
export function tableView(table: ViewTable, spacing: number = 2): string[] { | ||
const maxLen = {}; | ||
table.forEach(row => row.forEach((cell, i) => maxLen[i] = Math.max(maxLen[i] || 0, cell.length))); | ||
return table.map(row => row.map((cell, i) => `${cell}${repeatString(' ', maxLen[i] - cell.length + spacing)}`).join('')); | ||
} | ||
|
||
export function wordWrap(text: string, width: number = 80): string { | ||
const [indent = ''] = text.match(/^\s+/) || []; | ||
const maxWidth = width - indent.length; | ||
return text | ||
.replace(/^\s+/, '') | ||
.split('') | ||
.reduce(([out, buffer, pos], ch, i) => { | ||
const nl = pos === maxWidth ? `\n${indent}` : ''; | ||
const newPos: number = nl ? 0 : +pos + 1; | ||
return / |-|,|\./.test(ch) ? | ||
[`${out}${buffer}${ch}${nl}`, '', newPos] : [`${out}${nl}`, buffer+ch, newPos]; | ||
}, [indent, '', 0]) | ||
.slice(0, 2) | ||
.join(''); | ||
}; | ||
|
||
export function indentRow(row: string) { return ` ${row}`; }; |