123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- import * as React from 'react';
- import { Panel, SplitPanel, TabBar, Widget } from '@lumino/widgets';
- import { ReadonlyJSONObject, PromiseDelegate } from '@lumino/coreutils';
- import { ISignal, Signal } from '@lumino/signaling';
- import { VirtualElement, h } from '@lumino/virtualdom';
- import { ServerConnection } from '@jupyterlab/services';
- import { TranslationBundle } from '@jupyterlab/translation';
- import { VDomModel, VDomRenderer } from '@jupyterlab/apputils';
- import {
- spreadsheetIcon,
- jsonIcon,
- markdownIcon,
- LabIcon
- } from '@jupyterlab/ui-components';
- /**
- * A license viewer
- */
- export class Licenses extends SplitPanel {
- readonly model: Licenses.Model;
- constructor(options: Licenses.IOptions) {
- super();
- this.addClass('jp-Licenses');
- this.model = options.model;
- this.initLeftPanel();
- this.initFilters();
- this.initBundles();
- this.initGrid();
- this.initLicenseText();
- this.setRelativeSizes([1, 2, 3]);
- void this.model.initLicenses().then(() => this._updateBundles());
- this.model.trackerDataChanged.connect(() => {
- this.title.label = this.model.title;
- });
- }
- /**
- * Handle disposing of the widget
- */
- dispose(): void {
- if (this.isDisposed) {
- return;
- }
- this._bundles.currentChanged.disconnect(this.onBundleSelected, this);
- this.model.dispose();
- super.dispose();
- }
- /**
- * Initialize the left area for filters and bundles
- */
- protected initLeftPanel(): void {
- this._leftPanel = new Panel();
- this._leftPanel.addClass('jp-Licenses-FormArea');
- this.addWidget(this._leftPanel);
- SplitPanel.setStretch(this._leftPanel, 1);
- }
- /**
- * Initialize the filters
- */
- protected initFilters(): void {
- this._filters = new Licenses.Filters(this.model);
- SplitPanel.setStretch(this._filters, 1);
- this._leftPanel.addWidget(this._filters);
- }
- /**
- * Initialize the listing of available bundles
- */
- protected initBundles(): void {
- this._bundles = new TabBar({
- orientation: 'vertical',
- renderer: new Licenses.BundleTabRenderer(this.model)
- });
- this._bundles.addClass('jp-Licenses-Bundles');
- SplitPanel.setStretch(this._bundles, 1);
- this._leftPanel.addWidget(this._bundles);
- this._bundles.currentChanged.connect(this.onBundleSelected, this);
- this.model.stateChanged.connect(() => this._bundles.update());
- }
- /**
- * Initialize the listing of packages within the current bundle
- */
- protected initGrid(): void {
- this._grid = new Licenses.Grid(this.model);
- SplitPanel.setStretch(this._grid, 1);
- this.addWidget(this._grid);
- }
- /**
- * Initialize the full text of the current package
- */
- protected initLicenseText(): void {
- this._licenseText = new Licenses.FullText(this.model);
- SplitPanel.setStretch(this._grid, 1);
- this.addWidget(this._licenseText);
- }
- /**
- * Event handler for updating the model with the current bundle
- */
- protected onBundleSelected(): void {
- if (this._bundles.currentTitle?.label) {
- this.model.currentBundleName = this._bundles.currentTitle.label;
- }
- }
- /**
- * Update the bundle tabs.
- */
- protected _updateBundles(): void {
- this._bundles.clearTabs();
- let i = 0;
- const { currentBundleName } = this.model;
- let currentIndex = 0;
- for (const bundle of this.model.bundleNames) {
- const tab = new Widget();
- tab.title.label = bundle;
- if (bundle === currentBundleName) {
- currentIndex = i;
- }
- this._bundles.insertTab(++i, tab.title);
- }
- this._bundles.currentIndex = currentIndex;
- }
- /**
- * An area for selecting licenses by bundle and filters
- */
- protected _leftPanel: Panel;
- /**
- * Filters on visible licenses
- */
- protected _filters: Licenses.Filters;
- /**
- * Tabs reflecting available bundles
- */
- protected _bundles: TabBar<Widget>;
- /**
- * A grid of the current bundle's packages' license metadata
- */
- protected _grid: Licenses.Grid;
- /**
- * The currently-selected package's full license text
- */
- protected _licenseText: Licenses.FullText;
- }
- /** A namespace for license components */
- export namespace Licenses {
- /** The information about a license report format */
- export interface IReportFormat {
- title: string;
- icon: LabIcon;
- id: string;
- }
- /**
- * License report formats understood by the server (once lower-cased)
- */
- export const REPORT_FORMATS: Record<string, IReportFormat> = {
- markdown: {
- id: 'markdown',
- title: 'Markdown',
- icon: markdownIcon
- },
- csv: {
- id: 'csv',
- title: 'CSV',
- icon: spreadsheetIcon
- },
- json: {
- id: 'csv',
- title: 'JSON',
- icon: jsonIcon
- }
- };
- /**
- * The default format (most human-readable)
- */
- export const DEFAULT_FORMAT = 'markdown';
- /**
- * Options for instantiating a license viewer
- */
- export interface IOptions {
- model: Model;
- }
- /**
- * Options for instantiating a license model
- */
- export interface IModelOptions extends ICreateArgs {
- licensesUrl: string;
- serverSettings?: ServerConnection.ISettings;
- trans: TranslationBundle;
- }
- /**
- * The JSON response from the API
- */
- export interface ILicenseResponse {
- bundles: {
- [key: string]: ILicenseBundle;
- };
- }
- /**
- * A top-level report of the licenses for all code included in a bundle
- *
- * ### Note
- *
- * This is roughly informed by the terms defined in the SPDX spec, though is not
- * an SPDX Document, since there seem to be several (incompatible) specs
- * in that repo.
- *
- * @see https://github.com/spdx/spdx-spec/blob/development/v2.2.1/schemas/spdx-schema.json
- **/
- export interface ILicenseBundle extends ReadonlyJSONObject {
- packages: IPackageLicenseInfo[];
- }
- /**
- * A best-effort single bundled package's information.
- *
- * ### Note
- *
- * This is roughly informed by SPDX `packages` and `hasExtractedLicenseInfos`,
- * as making it conformant would vastly complicate the structure.
- *
- * @see https://github.com/spdx/spdx-spec/blob/development/v2.2.1/schemas/spdx-schema.json
- **/
- export interface IPackageLicenseInfo extends ReadonlyJSONObject {
- /**
- * the name of the package as it appears in package.json
- */
- name: string;
- /**
- * the version of the package, or an empty string if unknown
- */
- versionInfo: string;
- /**
- * an SPDX license identifier or LicenseRef, or an empty string if unknown
- */
- licenseId: string;
- /**
- * the verbatim extracted text of the license, or an empty string if unknown
- */
- extractedText: string;
- }
- /**
- * The format information for a download
- */
- export interface IDownloadOptions {
- format: string;
- }
- /**
- * The fields which can be filtered
- */
- export type TFilterKey = 'name' | 'versionInfo' | 'licenseId';
- export interface ICreateArgs {
- currentBundleName?: string | null;
- packageFilter?: Partial<IPackageLicenseInfo> | null;
- currentPackageIndex?: number | null;
- }
- /**
- * A model for license data
- */
- export class Model extends VDomModel implements ICreateArgs {
- constructor(options: IModelOptions) {
- super();
- this._trans = options.trans;
- this._licensesUrl = options.licensesUrl;
- this._serverSettings =
- options.serverSettings || ServerConnection.makeSettings();
- if (options.currentBundleName) {
- this._currentBundleName = options.currentBundleName;
- }
- if (options.packageFilter) {
- this._packageFilter = options.packageFilter;
- }
- if (options.currentPackageIndex) {
- this._currentPackageIndex = options.currentPackageIndex;
- }
- }
- /**
- * Handle the initial request for the licenses from the server.
- */
- async initLicenses(): Promise<void> {
- try {
- const response = await ServerConnection.makeRequest(
- this._licensesUrl,
- {},
- this._serverSettings
- );
- this._serverResponse = await response.json();
- this._licensesReady.resolve();
- this.stateChanged.emit(void 0);
- } catch (err) {
- this._licensesReady.reject(err);
- }
- }
- /**
- * Create a temporary download link, and emulate clicking it to trigger a named
- * file download.
- */
- async download(options: IDownloadOptions): Promise<void> {
- const url = `${this._licensesUrl}?format=${options.format}&download=1`;
- const element = document.createElement('a');
- element.href = url;
- element.download = '';
- document.body.appendChild(element);
- element.click();
- document.body.removeChild(element);
- return void 0;
- }
- /**
- * A promise that resolves when the licenses from the server change
- */
- get selectedPackageChanged(): ISignal<Model, void> {
- return this._selectedPackageChanged;
- }
- /**
- * A promise that resolves when the trackable data changes
- */
- get trackerDataChanged(): ISignal<Model, void> {
- return this._trackerDataChanged;
- }
- /**
- * The names of the license bundles available
- */
- get bundleNames(): string[] {
- return Object.keys(this._serverResponse?.bundles || {});
- }
- /**
- * The current license bundle
- */
- get currentBundleName(): string | null {
- if (this._currentBundleName) {
- return this._currentBundleName;
- }
- if (this.bundleNames.length) {
- return this.bundleNames[0];
- }
- return null;
- }
- /**
- * Set the current license bundle, and reset the selected index
- */
- set currentBundleName(currentBundleName: string | null) {
- if (this._currentBundleName !== currentBundleName) {
- this._currentBundleName = currentBundleName;
- this.stateChanged.emit(void 0);
- this._trackerDataChanged.emit(void 0);
- }
- }
- /**
- * A promise that resolves when the licenses are available from the server
- */
- get licensesReady(): Promise<void> {
- return this._licensesReady.promise;
- }
- /**
- * All the license bundles, keyed by the distributing packages
- */
- get bundles(): null | { [key: string]: ILicenseBundle } {
- return this._serverResponse?.bundles || {};
- }
- /**
- * The index of the currently-selected package within its license bundle
- */
- get currentPackageIndex(): number | null {
- return this._currentPackageIndex;
- }
- /**
- * Update the currently-selected package within its license bundle
- */
- set currentPackageIndex(currentPackageIndex: number | null) {
- if (this._currentPackageIndex === currentPackageIndex) {
- return;
- }
- this._currentPackageIndex = currentPackageIndex;
- this._selectedPackageChanged.emit(void 0);
- this.stateChanged.emit(void 0);
- this._trackerDataChanged.emit(void 0);
- }
- /**
- * The license data for the currently-selected package
- */
- get currentPackage(): IPackageLicenseInfo | null {
- if (
- this.currentBundleName &&
- this.bundles &&
- this._currentPackageIndex != null
- ) {
- return this.getFilteredPackages(
- this.bundles[this.currentBundleName]?.packages || []
- )[this._currentPackageIndex];
- }
- return null;
- }
- /**
- * A translation bundle
- */
- get trans(): TranslationBundle {
- return this._trans;
- }
- get title(): string {
- return `${this._currentBundleName || ''} ${this._trans.__(
- 'Licenses'
- )}`.trim();
- }
- /**
- * The current package filter
- */
- get packageFilter(): Partial<IPackageLicenseInfo> {
- return this._packageFilter;
- }
- set packageFilter(packageFilter: Partial<IPackageLicenseInfo>) {
- this._packageFilter = packageFilter;
- this.stateChanged.emit(void 0);
- this._trackerDataChanged.emit(void 0);
- }
- /**
- * Get filtered packages from current bundle where at least one token of each
- * key is present.
- */
- getFilteredPackages(allRows: IPackageLicenseInfo[]): IPackageLicenseInfo[] {
- let rows: IPackageLicenseInfo[] = [];
- let filters: [string, string[]][] = Object.entries(this._packageFilter)
- .filter(([k, v]) => v && `${v}`.trim().length)
- .map(([k, v]) => [k, `${v}`.toLowerCase().trim().split(' ')]);
- for (const row of allRows) {
- let keyHits = 0;
- for (const [key, bits] of filters) {
- let bitHits = 0;
- let rowKeyValue = `${row[key]}`.toLowerCase();
- for (const bit of bits) {
- if (rowKeyValue.includes(bit)) {
- bitHits += 1;
- }
- }
- if (bitHits) {
- keyHits += 1;
- }
- }
- if (keyHits === filters.length) {
- rows.push(row);
- }
- }
- return Object.values(rows);
- }
- private _selectedPackageChanged: Signal<Model, void> = new Signal(this);
- private _trackerDataChanged: Signal<Model, void> = new Signal(this);
- private _serverResponse: ILicenseResponse | null;
- private _licensesUrl: string;
- private _serverSettings: ServerConnection.ISettings;
- private _currentBundleName: string | null;
- private _trans: TranslationBundle;
- private _currentPackageIndex: number | null = 0;
- private _licensesReady = new PromiseDelegate<void>();
- private _packageFilter: Partial<IPackageLicenseInfo> = {};
- }
- /**
- * A filter form for limiting the packages displayed
- */
- export class Filters extends VDomRenderer<Model> {
- constructor(model: Model) {
- super(model);
- this.addClass('jp-Licenses-Filters');
- this.addClass('jp-RenderedHTMLCommon');
- }
- protected render(): JSX.Element {
- const { trans } = this.model;
- return (
- <div>
- <label>
- <strong>{trans.__('Filter Licenses By')}</strong>
- </label>
- <ul>
- <li>
- <label>{trans.__('Package')}</label>
- {this.renderFilter('name')}
- </li>
- <li>
- <label>{trans.__('Version')}</label>
- {this.renderFilter('versionInfo')}
- </li>
- <li>
- <label>{trans.__('License')}</label>
- {this.renderFilter('licenseId')}
- </li>
- </ul>
- <label>
- <strong>{trans.__('Distributions')}</strong>
- </label>
- </div>
- );
- }
- /**
- * Render a filter input
- */
- protected renderFilter = (key: TFilterKey): JSX.Element => {
- const value = this.model.packageFilter[key] || '';
- return (
- <input
- type="text"
- name={key}
- defaultValue={value}
- className="jp-mod-styled"
- onInput={this.onFilterInput}
- />
- );
- };
- /**
- * Handle a filter input changing
- */
- protected onFilterInput = (
- evt: React.ChangeEvent<HTMLInputElement>
- ): void => {
- const input = evt.currentTarget;
- const { name, value } = input;
- this.model.packageFilter = { ...this.model.packageFilter, [name]: value };
- };
- }
- /**
- * A fancy bundle renderer with the package count
- */
- export class BundleTabRenderer extends TabBar.Renderer {
- /**
- * A model of the state of license viewing as well as the underlying data
- */
- model: Model;
- readonly closeIconSelector = '.lm-TabBar-tabCloseIcon';
- constructor(model: Model) {
- super();
- this.model = model;
- }
- /**
- * Render a full bundle
- */
- renderTab(data: TabBar.IRenderData<Widget>): VirtualElement {
- let title = data.title.caption;
- let key = this.createTabKey(data);
- let style = this.createTabStyle(data);
- let className = this.createTabClass(data);
- let dataset = this.createTabDataset(data);
- return h.li(
- { key, className, title, style, dataset },
- this.renderIcon(data),
- this.renderLabel(data),
- this.renderCountBadge(data)
- );
- }
- /**
- * Render the package count
- */
- renderCountBadge(data: TabBar.IRenderData<Widget>): VirtualElement {
- const bundle = data.title.label;
- const { bundles } = this.model;
- const packages = this.model.getFilteredPackages(
- (bundles && bundle ? bundles[bundle].packages : []) || []
- );
- return h.label({}, `${packages.length}`);
- }
- }
- /**
- * A grid of licenses
- */
- export class Grid extends VDomRenderer<Licenses.Model> {
- constructor(model: Licenses.Model) {
- super(model);
- this.addClass('jp-Licenses-Grid');
- this.addClass('jp-RenderedHTMLCommon');
- }
- /**
- * Render a grid of package license information
- */
- protected render(): JSX.Element {
- const { bundles, currentBundleName, trans } = this.model;
- const filteredPackages = this.model.getFilteredPackages(
- bundles && currentBundleName
- ? bundles[currentBundleName]?.packages || []
- : []
- );
- if (!filteredPackages.length) {
- return (
- <blockquote>
- <em>{trans.__('No Packages found')}</em>
- </blockquote>
- );
- }
- return (
- <form>
- <table>
- <thead>
- <tr>
- <td></td>
- <th>{trans.__('Package')}</th>
- <th>{trans.__('Version')}</th>
- <th>{trans.__('License')}</th>
- </tr>
- </thead>
- <tbody>{filteredPackages.map(this.renderRow)}</tbody>
- </table>
- </form>
- );
- }
- /**
- * Render a single package's license information
- */
- protected renderRow = (
- row: Licenses.IPackageLicenseInfo,
- index: number
- ): JSX.Element => {
- const selected = index === this.model.currentPackageIndex;
- const onCheck = () => (this.model.currentPackageIndex = index);
- return (
- <tr
- key={row.name}
- className={selected ? 'jp-mod-selected' : ''}
- onClick={onCheck}
- >
- <td>
- <input
- type="radio"
- name="show-package-license"
- value={index}
- onChange={onCheck}
- checked={selected}
- />
- </td>
- <th>{row.name}</th>
- <td>
- <code>{row.versionInfo}</code>
- </td>
- <td>
- <code>{row.licenseId}</code>
- </td>
- </tr>
- );
- };
- }
- /**
- * A package's full license text
- */
- export class FullText extends VDomRenderer<Model> {
- constructor(model: Model) {
- super(model);
- this.addClass('jp-Licenses-Text');
- this.addClass('jp-RenderedHTMLCommon');
- this.addClass('jp-RenderedMarkdown');
- }
- /**
- * Render the license text, or a null state if no package is selected
- */
- protected render(): JSX.Element[] {
- const { currentPackage, trans } = this.model;
- let head = '';
- let quote = trans.__('No Package selected');
- let code = '';
- if (currentPackage) {
- const { name, versionInfo, licenseId, extractedText } = currentPackage;
- head = `${name} v${versionInfo}`;
- quote = `${trans.__('License')}: ${
- licenseId || trans.__('No License ID found')
- }`;
- code = extractedText || trans.__('No License Text found');
- }
- return [
- <h1 key="h1">{head}</h1>,
- <blockquote key="quote">
- <em>{quote}</em>
- </blockquote>,
- <code key="code">{code}</code>
- ];
- }
- }
- }
|