diff --git a/.eslintrc.json b/.eslintrc.json index 42c8f3e3e..7ce147f0b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -25,7 +25,7 @@ "@typescript-eslint/no-unused-vars": ["warn", { "args": "none" }], "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/camelcase": "off", - "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-namespace": "off", "header/header": [ diff --git a/packages/code-snippet/src/CodeSnippetService.ts b/packages/code-snippet/src/CodeSnippetService.ts index 94693b46f..dcc8b3a61 100644 --- a/packages/code-snippet/src/CodeSnippetService.ts +++ b/packages/code-snippet/src/CodeSnippetService.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { IMetadata } from '@elyra/metadata-common'; -import { MetadataService } from '@elyra/services'; +import { IMetadataResource, MetadataService } from '@elyra/services'; import { Dialog, showDialog } from '@jupyterlab/apputils'; @@ -23,15 +22,15 @@ export const CODE_SNIPPET_SCHEMASPACE = 'code-snippets'; export const CODE_SNIPPET_SCHEMA = 'code-snippet'; export class CodeSnippetService { - static async findAll(): Promise { - return MetadataService.getMetadata(CODE_SNIPPET_SCHEMASPACE); + static async findAll(): Promise { + return (await MetadataService.getMetadata(CODE_SNIPPET_SCHEMASPACE)) ?? []; } // TODO: Test this function - static async findByLanguage(language: string): Promise { + static async findByLanguage(language: string): Promise { try { - const allCodeSnippets: IMetadata[] = await this.findAll(); - const codeSnippetsByLanguage: IMetadata[] = []; + const allCodeSnippets = await this.findAll(); + const codeSnippetsByLanguage: IMetadataResource[] = []; for (const codeSnippet of allCodeSnippets) { if (codeSnippet.metadata.language === language) { @@ -54,11 +53,11 @@ export class CodeSnippetService { * @returns A boolean promise that is true if the dialog confirmed * the deletion, and false if the deletion was cancelled. */ - static deleteCodeSnippet(codeSnippet: IMetadata): Promise { + static deleteCodeSnippet(codeSnippet: IMetadataResource): Promise { return showDialog({ title: `Delete snippet '${codeSnippet.display_name}'?`, buttons: [Dialog.cancelButton(), Dialog.okButton()] - }).then((result: any) => { + }).then((result) => { // Do nothing if the cancel button is pressed if (result.button.accept) { return MetadataService.deleteMetadata( diff --git a/packages/code-snippet/src/CodeSnippetWidget.tsx b/packages/code-snippet/src/CodeSnippetWidget.tsx index aa07478f9..906fb2ca7 100644 --- a/packages/code-snippet/src/CodeSnippetWidget.tsx +++ b/packages/code-snippet/src/CodeSnippetWidget.tsx @@ -17,16 +17,15 @@ import '../style/index.css'; import { - IMetadata, IMetadataActionButton, IMetadataDisplayProps, - //IMetadataDisplayState, IMetadataWidgetProps, MetadataCommonService, MetadataDisplay, MetadataWidget, METADATA_ITEM } from '@elyra/metadata-common'; +import { IMetadataResource } from '@elyra/services'; import { ExpandableComponent, importIcon, @@ -40,7 +39,7 @@ import { CodeCell, MarkdownCell, ICodeCellModel, - IMarkdownCellModel /*RawCell*/ + IMarkdownCellModel } from '@jupyterlab/cells'; import { CodeEditor, IEditorServices } from '@jupyterlab/codeeditor'; import { EditorLanguageRegistry } from '@jupyterlab/codemirror'; @@ -48,10 +47,7 @@ import { PathExt } from '@jupyterlab/coreutils'; import { DocumentWidget } from '@jupyterlab/docregistry'; import { FileEditor } from '@jupyterlab/fileeditor'; import * as nbformat from '@jupyterlab/nbformat'; -import { - Notebook, - /*NotebookModel,*/ NotebookPanel /*,NotebookActions*/ -} from '@jupyterlab/notebook'; +import { Notebook, NotebookPanel } from '@jupyterlab/notebook'; import { copyIcon, editIcon, @@ -65,8 +61,6 @@ import { Drag } from '@lumino/dragdrop'; import { Widget } from '@lumino/widgets'; import React from 'react'; -//import { CodeBlock } from '../../ui-components/src/FormComponents/CodeBlock'; -//import { MarkdownDocument } from '@jupyterlab/markdownviewer'; import { CodeSnippetService, @@ -92,13 +86,6 @@ const JUPYTER_CELL_MIME = 'application/vnd.jupyter.cells'; * CodeSnippetDisplay props. */ interface ICodeSnippetDisplayProps extends IMetadataDisplayProps { - metadata: IMetadata[]; - openMetadataEditor: (args: any) => void; - updateMetadata: () => void; - schemaspace: string; - schema: string; - sortMetadata: boolean; - className: string; getCurrentWidget: () => Widget | null; editorServices: IEditorServices; shell: JupyterFrontEnd.IShell; @@ -108,7 +95,6 @@ interface ICodeSnippetDisplayProps extends IMetadataDisplayProps { * A React Component for code-snippets display list. */ class CodeSnippetDisplay extends MetadataDisplay { - //,IMetadataDisplayState editors: { [codeSnippetId: string]: CodeEditor.IEditor } = {}; constructor(props: ICodeSnippetDisplayProps) { @@ -120,10 +106,12 @@ class CodeSnippetDisplay extends MetadataDisplay { } // Handle code snippet insertion into an editor - private insertCodeSnippet = async (snippet: IMetadata): Promise => { + private insertCodeSnippet = async ( + snippet: IMetadataResource + ): Promise => { const widget = this.props.getCurrentWidget(); - const codeSnippet = snippet.metadata.code.join('\n'); - const snippetLanguage = snippet.metadata.language; + const codeSnippet = this.extractMetadataCodeSnippet(snippet); + const snippetLanguage = this.extractMetadataLanguage(snippet); if (widget === null) { return; @@ -142,15 +130,13 @@ class CodeSnippetDisplay extends MetadataDisplay { this.addMarkdownCodeBlock(snippetLanguage, codeSnippet) ); } else if (editorLanguage) { - this.verifyLanguageAndInsert(snippet, editorLanguage, fileEditor); + await this.verifyLanguageAndInsert(snippet, editorLanguage, fileEditor); } else { fileEditor.replaceSelection?.(codeSnippet); } } else if (widget instanceof NotebookPanel) { const notebookWidget: NotebookPanel = widget as NotebookPanel; const notebookCell = (notebookWidget.content as Notebook).activeCell; - //const notebookCellIndex = (notebookWidget.content as Notebook) - //.activeCellIndex; if (notebookCell === null) { return; @@ -163,7 +149,7 @@ class CodeSnippetDisplay extends MetadataDisplay { const kernelInfo = await notebookWidget.sessionContext.session?.kernel?.info; const kernelLanguage: string = kernelInfo?.language_info.name || ''; - this.verifyLanguageAndInsert( + await this.verifyLanguageAndInsert( snippet, kernelLanguage, notebookCellEditor @@ -202,6 +188,7 @@ class CodeSnippetDisplay extends MetadataDisplay { contentFactory: contentFactory }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- API mismatch const codeCell: any = contentFactory.createCodeCell(options); codeCell.cell_type = 'code'; //insert the new code cell into the notebook at the specified index @@ -247,12 +234,12 @@ class CodeSnippetDisplay extends MetadataDisplay { // Handle language compatibility between code snippet and editor private verifyLanguageAndInsert = async ( - snippet: IMetadata, + snippet: IMetadataResource, editorLanguage: string, editor: CodeEditor.IEditor ): Promise => { - const codeSnippet: string = snippet.metadata.code.join('\n'); - const snippetLanguage = snippet.metadata.language; + const codeSnippet = this.extractMetadataCodeSnippet(snippet); + const snippetLanguage = this.extractMetadataLanguage(snippet); if ( editorLanguage && snippetLanguage.toLowerCase() !== editorLanguage.toLowerCase() @@ -293,8 +280,8 @@ class CodeSnippetDisplay extends MetadataDisplay { // Initial setup to handle dragging a code snippet private handleDragSnippet( - event: React.MouseEvent, - metadata: IMetadata + event: React.MouseEvent, + metadata: IMetadataResource ): void { const { button } = event; @@ -329,7 +316,7 @@ class CodeSnippetDisplay extends MetadataDisplay { private _evtMouseUp( event: MouseEvent, - metadata: IMetadata, + _metadata: IMetadataResource, mouseMoveListener: (event: MouseEvent) => void ): void { event.preventDefault(); @@ -341,7 +328,7 @@ class CodeSnippetDisplay extends MetadataDisplay { private handleDragMove( event: MouseEvent, - metadata: IMetadata, + metadata: IMetadataResource, mouseMoveListener: (event: MouseEvent) => void, mouseUpListener: (event: MouseEvent) => void ): void { @@ -379,6 +366,23 @@ class CodeSnippetDisplay extends MetadataDisplay { } } + private extractMetadataCodeSnippet = ( + metadata: IMetadataResource + ): string => { + const codeLines = (metadata.metadata.code as string[] | undefined) ?? []; + return codeLines.join('\n'); + }; + + private extractMetadataLanguage = (metadata: IMetadataResource): string => { + return (metadata.metadata.language as string | undefined) ?? 'Unknown'; + }; + + private extractMetadataDescription = ( + metadata: IMetadataResource + ): string => { + return (metadata.metadata.description as string | undefined) ?? ''; + }; + /** * Detect if a drag event should be started. This is down if the * mouse is moved beyond a certain distance (DRAG_THRESHOLD). @@ -401,7 +405,7 @@ class CodeSnippetDisplay extends MetadataDisplay { private async startDrag( dragImage: HTMLElement, - metadata: IMetadata, + metadata: IMetadataResource, clientX: number, clientY: number ): Promise { @@ -431,11 +435,11 @@ class CodeSnippetDisplay extends MetadataDisplay { const markdownCell = contentFactory.createMarkdownCell(options2); - const language = metadata.metadata.language; + const language = this.extractMetadataLanguage(metadata); const model = language.toLowerCase() !== 'markdown' ? codeCell : markdownCell; - const content = metadata.metadata.code.join('\n'); + const content = this.extractMetadataCodeSnippet(metadata); if (language.toLowerCase() !== 'markdown') { if (model.model.type === 'code') { @@ -470,14 +474,14 @@ class CodeSnippetDisplay extends MetadataDisplay { }); } - actionButtons = (metadata: IMetadata): IMetadataActionButton[] => { + actionButtons = (metadata: IMetadataResource): IMetadataActionButton[] => { return [ { title: 'Copy to clipboard', icon: pasteIcon, feedback: 'Copied!', onClick: (): void => { - Clipboard.copyToSystem(metadata.metadata.code.join('\n')); + Clipboard.copyToSystem(this.extractMetadataCodeSnippet(metadata)); } }, { @@ -508,10 +512,12 @@ class CodeSnippetDisplay extends MetadataDisplay { metadata, this.props.metadata ) - .then((response: any): void => { + .then((_response): void => { this.props.updateMetadata(); }) - .catch((error) => RequestErrors.serverError(error)); + .catch(async (error) => { + await RequestErrors.serverError(error); + }); } }, { @@ -519,7 +525,7 @@ class CodeSnippetDisplay extends MetadataDisplay { icon: trashIcon, onClick: (): void => { CodeSnippetService.deleteCodeSnippet(metadata) - .then((deleted: any): void => { + .then((deleted: boolean): void => { if (deleted) { this.props.updateMetadata(); delete this.editors[metadata.name]; @@ -537,14 +543,17 @@ class CodeSnippetDisplay extends MetadataDisplay { } } }) - .catch((error) => RequestErrors.serverError(error)); + .catch(async (error) => { + await RequestErrors.serverError(error); + }); } } ]; }; - getDisplayName(metadata: IMetadata): string { - return `[${metadata.metadata.language}] ${metadata.display_name}`; + getDisplayName(metadata: IMetadataResource): string { + const language = this.extractMetadataLanguage(metadata); + return `[${language}] ${metadata.display_name}`; } sortMetadata(): void { @@ -553,19 +562,19 @@ class CodeSnippetDisplay extends MetadataDisplay { ); } - matchesSearch(searchValue: string, metadata: IMetadata): boolean { + matchesSearch(searchValue: string, metadata: IMetadataResource): boolean { searchValue = searchValue.toLowerCase(); // True if search string is in name, display_name, or language of snippet // or if the search string is empty return ( metadata.name.toLowerCase().includes(searchValue) || metadata.display_name.toLowerCase().includes(searchValue) || - metadata.metadata.language.toLowerCase().includes(searchValue) + this.extractMetadataLanguage(metadata).toLowerCase().includes(searchValue) ); } // Render display of a code snippet - renderMetadata = (metadata: IMetadata): JSX.Element => { + renderMetadata = (metadata: IMetadataResource): JSX.Element => { return (
{ > { this.editors[metadata.name].redo(); }} - onMouseDown={(event: any): void => { + onMouseDown={( + event: React.MouseEvent + ): void => { this.handleDragSnippet(event, metadata); }} > @@ -595,8 +606,8 @@ class CodeSnippetDisplay extends MetadataDisplay { createPreviewEditors = (): void => { const editorFactory = this.props.editorServices.factoryService.newInlineEditor; - this.props.metadata.map((codeSnippet: IMetadata) => { - const content = codeSnippet.metadata.code.join('\n'); + this.props.metadata.map((codeSnippet: IMetadataResource) => { + const content = this.extractMetadataCodeSnippet(codeSnippet); if (codeSnippet.name in this.editors) { // Make sure code is up to date @@ -608,11 +619,13 @@ class CodeSnippetDisplay extends MetadataDisplay { return; } + const language = this.extractMetadataLanguage(codeSnippet); + const mimeType = this.props.editorServices.mimeTypeService.getMimeTypeByLanguage({ - value: codeSnippet.metadata.code.join('\n'), - name: codeSnippet.metadata.language, - codemirror_mode: codeSnippet.metadata.language + value: content, + name: language, + codemirror_mode: language }); const newEditor = editorFactory({ @@ -664,13 +677,14 @@ export class CodeSnippetWidget extends MetadataWidget { } // Request code snippets from server - async fetchMetadata(): Promise { - return CodeSnippetService.findAll().catch((error) => - RequestErrors.serverError(error) - ); + async fetchMetadata(): Promise { + return CodeSnippetService.findAll().catch(async (error) => { + await RequestErrors.serverError(error); + return []; + }); } - renderDisplay(metadata: IMetadata[]): React.ReactElement { + renderDisplay(metadata: IMetadataResource[]): React.ReactElement { if (Array.isArray(metadata) && !metadata.length) { // Empty metadata return ( @@ -689,7 +703,6 @@ export class CodeSnippetWidget extends MetadataWidget { openMetadataEditor={this.openMetadataEditor} updateMetadata={this.updateMetadata} schemaspace={CODE_SNIPPET_SCHEMASPACE} - schema={CODE_SNIPPET_SCHEMA} getCurrentWidget={this.props.getCurrentWidget} className={CODE_SNIPPETS_METADATA_CLASS} editorServices={this.props.editorServices} diff --git a/packages/metadata-common/src/MetadataCommonService.tsx b/packages/metadata-common/src/MetadataCommonService.tsx index 0b742e69f..a6bfb89f3 100644 --- a/packages/metadata-common/src/MetadataCommonService.tsx +++ b/packages/metadata-common/src/MetadataCommonService.tsx @@ -14,9 +14,7 @@ * limitations under the License. */ -import { MetadataService } from '@elyra/services'; - -import { IMetadata } from './MetadataWidget'; +import { IMetadataResource, MetadataService } from '@elyra/services'; export class MetadataCommonService { /** @@ -31,9 +29,9 @@ export class MetadataCommonService { */ static duplicateMetadataInstance( schemaSpace: string, - metadataInstance: IMetadata, - existingInstances: IMetadata[] - ): Promise { + metadataInstance: IMetadataResource, + existingInstances: IMetadataResource[] + ): Promise { // iterate through the list of currently defined // instance names and find the next available one // using '-Copy' @@ -56,9 +54,6 @@ export class MetadataCommonService { const duplicated_metadata = JSON.parse(JSON.stringify(metadataInstance)); duplicated_metadata.display_name = `${base_name}-Copy${count}`; delete duplicated_metadata.name; - return MetadataService.postMetadata( - schemaSpace, - JSON.stringify(duplicated_metadata) - ); + return MetadataService.postMetadata(schemaSpace, duplicated_metadata); } } diff --git a/packages/metadata-common/src/MetadataEditor.tsx b/packages/metadata-common/src/MetadataEditor.tsx index b5ec08e18..a79fea5fd 100644 --- a/packages/metadata-common/src/MetadataEditor.tsx +++ b/packages/metadata-common/src/MetadataEditor.tsx @@ -14,8 +14,12 @@ * limitations under the License. */ -import { MetadataService } from '@elyra/services'; -import { RequestErrors, FormEditor } from '@elyra/ui-components'; +import { ISchemaResource, MetadataService } from '@elyra/services'; +import { + RequestErrors, + FormEditor, + GenericObjectType +} from '@elyra/ui-components'; import * as React from 'react'; @@ -30,12 +34,12 @@ interface IMetadataEditorComponentProps extends IMetadataEditorProps { /** * Schema including the metadata wrapper and other fields like display name. */ - schemaTop: any; + schemaTop: ISchemaResource; /** * Metadata that has already been defined (if this is not a new instance) */ - initialMetadata: any; + initialMetadata: GenericObjectType; /** * Handler for setting dirty state in the parent component. @@ -79,7 +83,7 @@ export const MetadataEditor: React.FC = ({ }: IMetadataEditorComponentProps) => { const [invalidForm, setInvalidForm] = React.useState(name === undefined); - const schema = schemaTop.properties.metadata; + const schema = schemaTop.properties?.metadata; const [metadata, setMetadata] = React.useState(initialMetadata); const displayName = initialMetadata?.['_noCategory']?.['display_name']; @@ -88,37 +92,37 @@ export const MetadataEditor: React.FC = ({ /** * Saves metadata through either put or post request. */ - const saveMetadata = (): void => { + const saveMetadata = () => { if (invalidForm) { return; } - const newMetadata: any = { + const newMetadata = { schema_name: schemaName, display_name: metadata?.['_noCategory']?.['display_name'], metadata: flattenFormData(metadata) }; if (!name) { - MetadataService.postMetadata(schemaspace, JSON.stringify(newMetadata)) - .then((response: any): void => { + MetadataService.postMetadata(schemaspace, newMetadata) + .then(() => { setDirty(false); onSave(); close(); }) - .catch((error) => RequestErrors.serverError(error)); + .catch(async (error) => { + await RequestErrors.serverError(error); + }); } else { - MetadataService.putMetadata( - schemaspace, - name, - JSON.stringify(newMetadata) - ) - .then((response: any): void => { + MetadataService.putMetadata(schemaspace, name, newMetadata) + .then(() => { setDirty(false); onSave(); close(); }) - .catch((error) => RequestErrors.serverError(error)); + .catch(async (error) => { + await RequestErrors.serverError(error); + }); } }; @@ -132,8 +136,10 @@ export const MetadataEditor: React.FC = ({ * @param newFormData - Form data with category wrappers. * @returns - Form data as the server expects it. */ - const flattenFormData = (newFormData: any): any => { - const flattened: { [id: string]: any } = {}; + const flattenFormData = ( + newFormData: GenericObjectType + ): GenericObjectType => { + const flattened: GenericObjectType = {}; for (const category in newFormData) { for (const property in newFormData[category]) { flattened[property] = newFormData[category][property]; @@ -167,8 +173,8 @@ export const MetadataEditor: React.FC = ({ ) : null}

{ + schema={schema as GenericObjectType} + onChange={(formData: GenericObjectType, invalid: boolean): void => { setMetadata(formData); setInvalidForm(invalid); setDirty(true); diff --git a/packages/metadata-common/src/MetadataEditorWidget.tsx b/packages/metadata-common/src/MetadataEditorWidget.tsx index 4fb376e4a..e9e8762b2 100644 --- a/packages/metadata-common/src/MetadataEditorWidget.tsx +++ b/packages/metadata-common/src/MetadataEditorWidget.tsx @@ -14,8 +14,16 @@ * limitations under the License. */ -import { MetadataService } from '@elyra/services'; -import { RequestErrors } from '@elyra/ui-components'; +import { + IMetadataResource, + ISchemaResource, + MetadataService +} from '@elyra/services'; +import { + GenericObjectType, + IErrorResponse, + RequestErrors +} from '@elyra/ui-components'; import { ILabStatus } from '@jupyterlab/application'; import { ReactWidget, showDialog, Dialog } from '@jupyterlab/apputils'; @@ -23,6 +31,7 @@ import { IEditorServices } from '@jupyterlab/codeeditor'; import { TranslationBundle } from '@jupyterlab/translation'; import { IFormRendererRegistry } from '@jupyterlab/ui-components'; import { find } from '@lumino/algorithm'; +import { IDisposable } from '@lumino/disposable'; import { Message } from '@lumino/messaging'; import * as React from 'react'; @@ -93,13 +102,13 @@ export interface IMetadataEditorProps { export class MetadataEditorWidget extends ReactWidget { props: IMetadataEditorProps; widgetClass: string; - schema: any = {}; - metadata: any = {}; + schema: ISchemaResource | undefined; + metadata: GenericObjectType | undefined; loading = true; dirty = false; - clearDirty: any; + clearDirty: IDisposable | null = null; allTags: string[] = []; - allMetadata: any; + allMetadata: IMetadataResource[] | undefined; constructor(props: IMetadataEditorProps) { super(); @@ -120,30 +129,54 @@ export class MetadataEditorWidget extends ReactWidget { try { // Load all schema and all metadata in schemaspace. const allSchema = await MetadataService.getSchema(this.props.schemaspace); - const allMetadata = (this.allMetadata = await MetadataService.getMetadata( + this.allMetadata = await MetadataService.getMetadata( this.props.schemaspace - )); + ); + + if (!this.allMetadata) { + throw new Error( + `No metadata found for schemaspace ${this.props.schemaspace}` + ); + } // Loads all tags to display as options in the editor. - this.allTags = allMetadata.reduce((acc: string[], metadata: any) => { - if (metadata.metadata.tags) { - acc.push( - ...metadata.metadata.tags.filter((tag: string) => { - return !acc.includes(tag); - }) - ); - } - return acc; - }, []); + this.allTags = this.allMetadata.reduce( + (acc: string[], metadata: IMetadataResource) => { + const tags = metadata.metadata.tags as string[] | undefined; + if (tags) { + acc.push( + ...tags.filter((tag: string) => { + return !acc.includes(tag); + }) + ); + } + return acc; + }, + [] + ); // Finds schema based on schemaName. - const schema = - allSchema.find((s: any) => { - return s.name === this.props.schemaName; - }) ?? {}; + const schema = allSchema?.find((s) => { + return s.name === this.props.schemaName; + }); + + if (!schema) { + throw new Error(`Schema not found for ${this.props.schemaName}`); + } + if (!schema.properties?.metadata) { + throw new Error('Metadata not found in schema'); + } // Sets const fields to readonly. - const properties = schema.properties.metadata.properties; + const schemaMetadata = schema.properties?.metadata as + | GenericObjectType + | undefined; + const properties = schemaMetadata?.properties; + + if (!properties) { + throw new Error('Metadata properties not found in schema'); + } + for (const prop in properties) { if (properties[prop].uihints?.hidden) { delete properties[prop]; @@ -157,11 +190,11 @@ export class MetadataEditorWidget extends ReactWidget { } } - const metadata = allMetadata.find((m: any) => m.name === this.props.name); + const metadata = this.allMetadata.find((m) => m.name === this.props.name); // Adds categories as wrapper objects in the schema. - const metadataWithCategories: { [id: string]: any } = {}; - const schemaPropertiesByCategory: { [id: string]: any } = { + const metadataWithCategories: GenericObjectType = {}; + const schemaPropertiesByCategory: GenericObjectType = { _noCategory: { type: 'object', title: ' ', @@ -180,9 +213,8 @@ export class MetadataEditorWidget extends ReactWidget { // Adds required fields to the wrapper required fields. const requiredCategories: string[] = []; - for (const schemaProperty in schema.properties.metadata.properties) { - const properties = - schema.properties.metadata.properties[schemaProperty]; + for (const schemaProperty in schemaMetadata.properties) { + const properties = schemaMetadata.properties[schemaProperty]; const category = (properties.uihints && properties.uihints.category) ?? '_noCategory'; @@ -201,7 +233,8 @@ export class MetadataEditorWidget extends ReactWidget { required: [] }; } - if (schema.properties.metadata.required?.includes(schemaProperty)) { + + if (schemaMetadata.required?.includes(schemaProperty)) { schemaPropertiesByCategory[category].required.push(schemaProperty); if (!requiredCategories.includes(category)) { requiredCategories.push(category); @@ -215,14 +248,16 @@ export class MetadataEditorWidget extends ReactWidget { metadata?.['display_name']; } this.schema = schema; - this.schema.properties.metadata.properties = schemaPropertiesByCategory; - this.schema.properties.metadata.required = requiredCategories; + (this.schema.properties?.metadata as GenericObjectType).properties = + schemaPropertiesByCategory; + (this.schema.properties?.metadata as GenericObjectType).required = + requiredCategories; this.metadata = metadataWithCategories; this.title.label = metadata?.display_name ?? `New ${this.schema.title}`; this.loading = false; this.update(); } catch (error) { - RequestErrors.serverError(error); + await RequestErrors.serverError(error as IErrorResponse); } } @@ -245,7 +280,7 @@ export class MetadataEditorWidget extends ReactWidget { } } - onAfterShow(msg: Message): void { + onAfterShow(_msg: Message): void { this.setFormFocus(); } @@ -279,7 +314,7 @@ export class MetadataEditorWidget extends ReactWidget { title: this.props.translator.__('Close without saving?'), body:

Metadata has unsaved changes, close without saving?

, buttons: [Dialog.cancelButton(), Dialog.okButton()] - }).then((response: any): void => { + }).then((response): void => { if (response.button.accept) { this.dispose(); super.onCloseRequest(msg); @@ -291,8 +326,11 @@ export class MetadataEditorWidget extends ReactWidget { } } - getDefaultChoices(fieldName: string): any[] { - const schema = this.schema.properties.metadata; + getDefaultChoices(fieldName: string): string[] { + if (!this.schema || !this.allMetadata) { + return []; + } + const schema = this.schema.properties?.metadata as GenericObjectType; for (const category in schema.properties) { const properties = schema.properties[category].properties[fieldName]; if (!properties) { @@ -309,7 +347,7 @@ export class MetadataEditorWidget extends ReactWidget { !find(defaultChoices, (choice: string) => { return ( choice.toLowerCase() === - otherMetadata.metadata[fieldName].toLowerCase() + (otherMetadata.metadata[fieldName] as string).toLowerCase() ); }) ) { @@ -325,6 +363,11 @@ export class MetadataEditorWidget extends ReactWidget { if (this.loading) { return

Loading...

; } + + if (!this.schema || !this.metadata) { + return

Error loading metadata

; + } + return ( ; - schema_name: string; -} - export interface IMetadataActionButton { title: string; icon: LabIcon; @@ -71,18 +74,27 @@ export interface IMetadataActionButton { onClick: () => void; } +export interface IOpenMetadataEditorArgs { + titleContext?: string; + schema: string; + schemaspace: string; + name?: string; + onSave: () => void; + code?: string[]; +} + /** * MetadataDisplay props. */ export interface IMetadataDisplayProps { - metadata: IMetadata[]; - openMetadataEditor: (args: any) => void; + metadata: IMetadataResource[]; + openMetadataEditor: (args: IOpenMetadataEditorArgs) => void; updateMetadata: () => void; schemaspace: string; sortMetadata: boolean; className: string; titleContext?: string; - labelName?: (args: any) => string; + labelName?: (args: IMetadataResource) => string; omitTags?: boolean; } @@ -90,11 +102,14 @@ export interface IMetadataDisplayProps { * MetadataDisplay state. */ export interface IMetadataDisplayState { - metadata: IMetadata[]; + metadata: IMetadataResource[]; searchValue: string; filterTags: string[]; - matchesSearch: (searchValue: string, metadata: IMetadata) => boolean; - matchesTags: (filterTags: Set, metadata: IMetadata) => boolean; + matchesSearch: (searchValue: string, metadata: IMetadataResource) => boolean; + matchesTags: ( + filterTags: Set, + metadata: IMetadataResource + ) => boolean; } /** @@ -115,24 +130,26 @@ export class MetadataDisplay< }; } - deleteMetadata = (metadata: IMetadata): Promise => { + deleteMetadata = (metadata: IMetadataResource): Promise => { return showDialog({ title: `Delete ${ this.props.labelName ? this.props.labelName(metadata) : '' } ${this.props.titleContext || ''} '${metadata.display_name}'?`, buttons: [Dialog.cancelButton(), Dialog.okButton()] - }).then((result: any) => { + }).then((result) => { // Do nothing if the cancel button is pressed if (result.button.accept) { MetadataService.deleteMetadata( this.props.schemaspace, metadata.name - ).catch((error) => RequestErrors.serverError(error)); + ).catch(async (error) => { + await RequestErrors.serverError(error); + }); } }); }; - actionButtons(metadata: IMetadata): IMetadataActionButton[] { + actionButtons(metadata: IMetadataResource): IMetadataActionButton[] { return [ { title: 'Edit', @@ -155,17 +172,19 @@ export class MetadataDisplay< metadata, this.props.metadata ) - .then((response: any): void => { + .then((): void => { this.props.updateMetadata(); }) - .catch((error) => RequestErrors.serverError(error)); + .catch(async (error) => { + await RequestErrors.serverError(error); + }); } }, { title: 'Delete', icon: trashIcon, onClick: (): void => { - this.deleteMetadata(metadata).then((response: any): void => { + this.deleteMetadata(metadata).then((): void => { this.props.updateMetadata(); }); } @@ -176,7 +195,7 @@ export class MetadataDisplay< /** * Classes that extend MetadataWidget should override this */ - renderExpandableContent(metadata: IDictionary): JSX.Element { + renderExpandableContent(metadata: IMetadataResource): JSX.Element { const metadataWithoutTags = metadata.metadata; delete metadataWithoutTags.tags; return ( @@ -187,7 +206,7 @@ export class MetadataDisplay< } // Render display of metadata list - renderMetadata = (metadata: IMetadata): JSX.Element => { + renderMetadata = (metadata: IMetadataResource): JSX.Element => { return (
{this.renderExpandableContent(metadata)}
@@ -221,7 +240,7 @@ export class MetadataDisplay< filteredMetadata = (searchValue: string, filterTags: string[]): void => { // filter with search let filteredMetadata = this.props.metadata.filter( - (metadata: IMetadata, index: number, array: IMetadata[]): boolean => { + (metadata: IMetadataResource): boolean => { return ( metadata.name.toLowerCase().includes(searchValue.toLowerCase()) || metadata.display_name @@ -235,8 +254,9 @@ export class MetadataDisplay< if (filterTags.length !== 0) { filteredMetadata = filteredMetadata.filter((metadata) => { return filterTags.some((tag) => { - if (metadata.metadata.tags) { - return metadata.metadata.tags.includes(tag); + const tags = metadata.metadata.tags as string[] | undefined; + if (tags) { + return tags.includes(tag); } return false; }); @@ -251,30 +271,30 @@ export class MetadataDisplay< }; getActiveTags(): string[] { - const tags: string[] = []; + const activeTags: string[] = []; for (const metadata of this.props.metadata) { - if (metadata.metadata.tags) { - for (const tag of metadata.metadata.tags) { - if (!tags.includes(tag)) { - tags.push(tag); + const metadataTags = metadata.metadata.tags as string[] | undefined; + if (metadataTags) { + for (const tag of metadataTags) { + if (!activeTags.includes(tag)) { + activeTags.push(tag); } } } } - return tags; + return activeTags; } - matchesTags(filterTags: Set, metadata: IMetadata): boolean { + matchesTags(filterTags: Set, metadata: IMetadataResource): boolean { // True if there are no tags selected or if there are tags that match // tags of metadata + const tags = metadata.metadata.tags as string[]; return ( - filterTags.size === 0 || - (metadata.metadata.tags && - metadata.metadata.tags.some((tag: string) => filterTags.has(tag))) + filterTags.size === 0 || tags?.some((tag: string) => filterTags.has(tag)) ); } - matchesSearch(searchValue: string, metadata: IMetadata): boolean { + matchesSearch(searchValue: string, metadata: IMetadataResource): boolean { searchValue = searchValue.toLowerCase(); // True if search string is in name or display_name, // or if the search string is empty @@ -353,9 +373,9 @@ export interface IMetadataWidgetProps { * A abstract widget for viewing metadata. */ export class MetadataWidget extends ReactWidget { - renderSignal: Signal; + renderSignal: Signal; props: IMetadataWidgetProps; - schemas?: IDictionary[]; + schemas?: ISchemaResource[]; titleContext?: string; addLabel?: string; refreshButtonTooltip?: string; @@ -365,7 +385,7 @@ export class MetadataWidget extends ReactWidget { this.addClass('elyra-metadata'); this.props = props; - this.renderSignal = new Signal(this); + this.renderSignal = new Signal(this); this.titleContext = props.titleContext; this.addLabel = props.addLabel; this.fetchMetadata = this.fetchMetadata.bind(this); @@ -383,7 +403,9 @@ export class MetadataWidget extends ReactWidget { try { this.schemas = await MetadataService.getSchema(this.props.schemaspace); const sortedSchema = - this.schemas?.sort((a, b) => a.title.localeCompare(b.title)) ?? []; + this.schemas?.sort((a, b) => + (a.title ?? '').localeCompare(b.title ?? '') + ) ?? []; if (sortedSchema.length > 1) { for (const schema of sortedSchema) { this.props.app.contextMenu.addItem({ @@ -397,13 +419,13 @@ export class MetadataWidget extends ReactWidget { titleContext: this.props.titleContext, addLabel: this.props.addLabel, appendToTitle: this.props.appendToTitle - } as any + } as unknown as ReadonlyJSONObject }); } } this.update(); } catch (error) { - RequestErrors.serverError(error); + await RequestErrors.serverError(error as IErrorResponse); } } @@ -424,16 +446,20 @@ export class MetadataWidget extends ReactWidget { * * @returns metadata in the format expected by `renderDisplay` */ - async fetchMetadata(): Promise { + async fetchMetadata(): Promise { try { return await MetadataService.getMetadata(this.props.schemaspace); } catch (error) { - return RequestErrors.serverError(error); + await RequestErrors.serverError(error as IErrorResponse); + return; } } updateMetadata(): void { - this.fetchMetadata().then((metadata: any[]) => { + this.fetchMetadata().then((metadata) => { + if (!metadata) { + return; + } this.renderSignal.emit(metadata); }); } @@ -443,17 +469,23 @@ export class MetadataWidget extends ReactWidget { } // Triggered when the widget button on side panel is clicked - onAfterShow(msg: Message): void { + onAfterShow(_msg: Message): void { this.updateMetadata(); } - openMetadataEditor = (args: any): void => { - this.props.app.commands.execute(commands.OPEN_METADATA_EDITOR, args); + openMetadataEditor = (args: IOpenMetadataEditorArgs): void => { + this.props.app.commands.execute( + commands.OPEN_METADATA_EDITOR, + args as unknown as ReadonlyPartialJSONObject + ); }; omitTags(): boolean { for (const schema of this.schemas ?? []) { - if (schema.properties?.metadata?.properties?.tags) { + const metadata = schema.properties?.metadata as + | GenericObjectType + | undefined; + if (metadata?.properties?.tags) { return false; } } @@ -465,7 +497,7 @@ export class MetadataWidget extends ReactWidget { * * @returns a rendered instance of `MetadataDisplay` */ - renderDisplay(metadata: IMetadata[]): React.ReactElement { + renderDisplay(metadata: IMetadataResource[]): React.ReactElement { if (Array.isArray(metadata) && !metadata.length) { // Empty metadata return ( @@ -528,10 +560,11 @@ export class MetadataWidget extends ReactWidget { singleSchema ? (): void => this.addMetadata( - this.schemas?.[0].name, + this.schemas?.[0].name ?? '', this.titleContext ) - : (event: any): void => { + : // eslint-disable-next-line @typescript-eslint/no-explicit-any -- types mismatch + (event: any): void => { this.props.app.contextMenu.open(event); } } @@ -542,7 +575,9 @@ export class MetadataWidget extends ReactWidget {
- {(_, metadata): React.ReactElement => this.renderDisplay(metadata)} + {(_, metadata): React.ReactElement => + this.renderDisplay(metadata ?? []) + }
); diff --git a/packages/metadata/src/index.ts b/packages/metadata/src/index.ts index e733dc950..a786391d3 100644 --- a/packages/metadata/src/index.ts +++ b/packages/metadata/src/index.ts @@ -22,7 +22,8 @@ import { PasswordField, CodeBlock, TagsField, - DropDown + DropDown, + IErrorResponse } from '@elyra/ui-components'; import { JupyterFrontEnd, @@ -50,6 +51,21 @@ const commandIDs = { closeTabCommand: 'elyra-metadata:close' }; +interface IOpenMetadataWidgetArgs { + display_name: string; + schemaspace: string; + icon: string; + addLabel?: string; +} + +interface IOpenMetadataEditorArgs { + schema: string; + schemaspace: string; + name?: string; + onSave: () => void; + titleContext?: string; +} + /** * Initialization data for the metadata-extension extension. */ @@ -94,13 +110,7 @@ const extension: JupyterFrontEndPlugin = { } }); - const openMetadataEditor = (args: { - schema: string; - schemaspace: string; - name?: string; - onSave: () => void; - titleContext?: string; - }): void => { + const openMetadataEditor = (args: IOpenMetadataEditorArgs): void => { let widgetLabel: string; if (args.name) { widgetLabel = args.name; @@ -110,12 +120,9 @@ const extension: JupyterFrontEndPlugin = { const widgetId = `${METADATA_EDITOR_ID}:${args.schemaspace}:${ args.schema }:${args.name ? args.name : 'new'}`; - const openWidget = find( - app.shell.widgets('main'), - (widget: Widget, index: number) => { - return widget.id === widgetId; - } - ); + const openWidget = find(app.shell.widgets('main'), (widget: Widget) => { + return widget.id === widgetId; + }); if (openWidget) { app.shell.activateById(widgetId); return; @@ -139,22 +146,17 @@ const extension: JupyterFrontEndPlugin = { }; app.commands.addCommand(`${METADATA_EDITOR_ID}:open`, { - label: (args: any) => { + label: (args) => { return `New ${args.title} ${ args.appendToTitle ? args.titleContext : '' }`; }, - execute: (args: any) => { - openMetadataEditor(args); + execute: (args) => { + openMetadataEditor(args as unknown as IOpenMetadataEditorArgs); } }); - const openMetadataWidget = (args: { - display_name: string; - schemaspace: string; - icon: string; - addLabel?: string; - }): void => { + const openMetadataWidget = (args: IOpenMetadataWidgetArgs): void => { const labIcon = LabIcon.resolve({ icon: args.icon }); const widgetId = `${METADATA_WIDGET_ID}:${args.schemaspace}`; const metadataWidget = new MetadataWidget({ @@ -179,11 +181,11 @@ const extension: JupyterFrontEndPlugin = { const openMetadataCommand: string = commandIDs.openMetadata; app.commands.addCommand(openMetadataCommand, { - label: (args: any) => args['label'], - execute: (args: any) => { + label: (args) => args.label as string, + execute: (args) => { // Rank has been chosen somewhat arbitrarily to give priority // to the running sessions widget in the sidebar. - openMetadataWidget(args); + openMetadataWidget(args as unknown as IOpenMetadataWidgetArgs); } }); @@ -191,7 +193,7 @@ const extension: JupyterFrontEndPlugin = { const closeTabCommand: string = commandIDs.closeTabCommand; app.commands.addCommand(closeTabCommand, { label: 'Close Tab', - execute: (args) => { + execute: (_args) => { const contextNode: HTMLElement | undefined = app.contextMenuHitTest( (node) => !!node.dataset.id ); @@ -199,7 +201,7 @@ const extension: JupyterFrontEndPlugin = { const id = contextNode.dataset['id']!; const widget = find( app.shell.widgets('left'), - (widget: Widget, index: number) => { + (widget: Widget, _index: number) => { return widget.id === id; } ); @@ -217,6 +219,11 @@ const extension: JupyterFrontEndPlugin = { try { const schemas = await MetadataService.getAllSchema(); + + if (!schemas) { + throw new Error('Failed to retrieve metadata schemas'); + } + for (const schema of schemas) { let icon = 'ui-components:text-editor'; let title = schema.title; @@ -232,7 +239,7 @@ const extension: JupyterFrontEndPlugin = { command: commandIDs.openMetadata, args: { label: `Manage ${title}`, - display_name: schema.uihints.title, + display_name: schema.uihints?.title ?? '', schemaspace: schema.schemaspace, icon: icon }, @@ -240,7 +247,7 @@ const extension: JupyterFrontEndPlugin = { }); } } catch (error) { - RequestErrors.serverError(error); + await RequestErrors.serverError(error as IErrorResponse); } } }; diff --git a/packages/pipeline-editor/package.json b/packages/pipeline-editor/package.json index d90922e19..a7bf384e7 100644 --- a/packages/pipeline-editor/package.json +++ b/packages/pipeline-editor/package.json @@ -88,6 +88,7 @@ "react-redux": "^7.2.0", "react-scripts": "4.0.3", "react-toastify": "^9.0.8", + "redux": "^4.2.0", "swr": "^0.5.6", "uuid": "^3.4.0" }, diff --git a/packages/pipeline-editor/src/ComponentCatalogsWidget.tsx b/packages/pipeline-editor/src/ComponentCatalogsWidget.tsx index 31639c84e..c9b3ced86 100644 --- a/packages/pipeline-editor/src/ComponentCatalogsWidget.tsx +++ b/packages/pipeline-editor/src/ComponentCatalogsWidget.tsx @@ -17,14 +17,16 @@ import { MetadataWidget, IMetadataWidgetProps, - IMetadata, MetadataDisplay, IMetadataDisplayProps, - //IMetadataDisplayState, IMetadataActionButton } from '@elyra/metadata-common'; -import { IDictionary, MetadataService } from '@elyra/services'; -import { RequestErrors } from '@elyra/ui-components'; +import { IMetadataResource, MetadataService } from '@elyra/services'; +import { + GenericObjectType, + IErrorResponse, + RequestErrors +} from '@elyra/ui-components'; import { JupyterFrontEnd } from '@jupyterlab/application'; import { LabIcon, refreshIcon } from '@jupyterlab/ui-components'; @@ -36,25 +38,18 @@ export const COMPONENT_CATALOGS_SCHEMASPACE = 'component-catalogs'; const COMPONENT_CATALOGS_CLASS = 'elyra-metadata-component-catalogs'; -const handleError = (error: any): void => { - // silently eat a 409, the server will log in in the console - if (error.status !== 409) { - RequestErrors.serverError(error); - } -}; - /** * A React Component for displaying the component catalogs list. */ class ComponentCatalogsDisplay extends MetadataDisplay { - actionButtons(metadata: IMetadata): IMetadataActionButton[] { + actionButtons(metadata: IMetadataResource): IMetadataActionButton[] { return [ { title: 'Reload components from catalog', icon: refreshIcon, onClick: (): void => { PipelineService.refreshComponentsCache(metadata.name) - .then((response: any): void => { + .then((): void => { this.props.updateMetadata(); }) .catch((error) => @@ -70,12 +65,12 @@ class ComponentCatalogsDisplay extends MetadataDisplay { } //render catalog entries - renderExpandableContent(metadata: IDictionary): JSX.Element { - let category_output =
  • No category
  • ; + renderExpandableContent(metadata: IMetadataResource): JSX.Element { + let category_output = [
  • No category
  • ]; if (metadata.metadata.categories) { - category_output = metadata.metadata.categories.map((category: string) => ( -
  • {category}
  • - )); + category_output = (metadata.metadata.categories as string[]).map( + (category) =>
  • {category}
  • + ); } return ( @@ -85,7 +80,7 @@ class ComponentCatalogsDisplay extends MetadataDisplay {

    Description
    - {metadata.metadata.description ?? 'No description'} + {(metadata.metadata.description as string) ?? 'No description'}

    Categories
    @@ -95,11 +90,13 @@ class ComponentCatalogsDisplay extends MetadataDisplay { } // Allow for filtering by display_name, name, and description - matchesSearch(searchValue: string, metadata: IMetadata): boolean { + matchesSearch(searchValue: string, metadata: IMetadataResource): boolean { searchValue = searchValue.toLowerCase(); // True if search string is in name or display_name, // or if the search string is empty - const description = (metadata.metadata.description || '').toLowerCase(); + const description = ( + (metadata.metadata.description as string) || '' + ).toLowerCase(); return ( metadata.name.toLowerCase().includes(searchValue) || metadata.display_name.toLowerCase().includes(searchValue) || @@ -139,17 +136,21 @@ export class ComponentCatalogsWidget extends MetadataWidget { async getSchemas(): Promise { try { const schemas = await MetadataService.getSchema(this.props.schemaspace); + if (!schemas) { + return; + } this.runtimeTypes = await PipelineService.getRuntimeTypes(); - const sortedSchema = schemas.sort((a: any, b: any) => - a.title.localeCompare(b.title) + const sortedSchema = schemas.sort((a, b) => + (a.title ?? '').localeCompare(b.title ?? '') ); - this.schemas = sortedSchema.filter((schema: any) => { - return !!this.runtimeTypes.find( - (r) => - schema.properties?.metadata?.properties?.runtime_type?.enum?.includes( - r.id - ) && r.runtime_enabled - ); + this.schemas = sortedSchema.filter((schema) => { + return !!this.runtimeTypes.find((r) => { + const metadata = schema.properties?.metadata as GenericObjectType; + return ( + metadata?.properties.runtime_type?.enum?.includes(r.id) && + r.runtime_enabled + ); + }); }); if (this.schemas?.length ?? 0 > 1) { for (const schema of this.schemas ?? []) { @@ -163,13 +164,13 @@ export class ComponentCatalogsWidget extends MetadataWidget { title: schema.title, titleContext: this.props.titleContext, appendToTitle: this.props.appendToTitle - } as any + } as GenericObjectType }); } } this.update(); } catch (error) { - RequestErrors.serverError(error); + await RequestErrors.serverError(error as IErrorResponse); } } @@ -183,13 +184,18 @@ export class ComponentCatalogsWidget extends MetadataWidget { refreshMetadata(): void { PipelineService.refreshComponentsCache() - .then((response: any): void => { + .then((): void => { this.updateMetadataAndRefresh(); }) - .catch((error) => handleError(error)); + .catch(async (error) => { + // silently eat a 409, the server will log in in the console + if (error.status !== 409) { + await RequestErrors.serverError(error); + } + }); } - renderDisplay(metadata: IMetadata[]): React.ReactElement { + renderDisplay(metadata: IMetadataResource[]): React.ReactElement { if (Array.isArray(metadata) && !metadata.length) { // Empty metadata return ( diff --git a/packages/pipeline-editor/src/ParameterInputForm.tsx b/packages/pipeline-editor/src/ParameterInputForm.tsx index 0f800f871..a6ccbe950 100644 --- a/packages/pipeline-editor/src/ParameterInputForm.tsx +++ b/packages/pipeline-editor/src/ParameterInputForm.tsx @@ -20,10 +20,12 @@ const DIALOG_WIDTH = 27; export interface IParameterProps { parameters?: { name: string; - default_value?: { - type: 'String' | 'Integer' | 'Float' | 'Bool'; - value: any; - }; + default_value?: + | { type: 'String'; value: string } + | { type: 'Integer'; value: number } + | { type: 'Float'; value: number } + | { type: 'Bool'; value: boolean }; + type?: string; required?: boolean; description?: string; @@ -67,7 +69,7 @@ export const ParameterInputForm: React.FC = ({