From 7ba7836b24378d29d665555f4e0a3fd2a3c89cf0 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Thu, 25 Apr 2024 17:45:20 +0100 Subject: [PATCH] Cleanup: move components out --- src/components/card.tsx | 31 +++ src/components/section.tsx | 43 ++++ src/components/table.tsx | 368 +++++++++++++++++++++++++++++++ src/dialogs.tsx | 2 +- src/launcher.tsx | 431 +------------------------------------ tsconfig.json | 2 +- 6 files changed, 448 insertions(+), 429 deletions(-) create mode 100644 src/components/card.tsx create mode 100644 src/components/section.tsx create mode 100644 src/components/table.tsx diff --git a/src/components/card.tsx b/src/components/card.tsx new file mode 100644 index 0000000..8a44bd6 --- /dev/null +++ b/src/components/card.tsx @@ -0,0 +1,31 @@ +// Copyright (c) Nebari Development Team. +// Distributed under the terms of the Modified BSD License. +import { TranslationBundle } from '@jupyterlab/translation'; +import { classes, LabIcon } from '@jupyterlab/ui-components'; +import * as React from 'react'; +import { IItem } from '../types'; + +export function TypeCard(props: { + trans: TranslationBundle; + item: IItem; +}): React.ReactElement { + const { item } = props; + return ( +
item.execute()} + className="jp-Launcher-TypeCard jp-LauncherCard" + title={item.caption} + tabIndex={0} + > +
+ +
+
+

{item.label}

+
+
+ ); +} diff --git a/src/components/section.tsx b/src/components/section.tsx new file mode 100644 index 0000000..730f619 --- /dev/null +++ b/src/components/section.tsx @@ -0,0 +1,43 @@ +// Copyright (c) Nebari Development Team. +// Distributed under the terms of the Modified BSD License. +import { classes, LabIcon, caretRightIcon } from '@jupyterlab/ui-components'; +import * as React from 'react'; + +export function CollapsibleSection( + props: React.PropsWithChildren<{ + title: string; + className: string; + icon: LabIcon; + open: boolean; + }> +) { + const [open, setOpen] = React.useState(props.open); + + const handleToggle = (event: { currentTarget: { open: boolean } }) => + setOpen(event.currentTarget.open); + + return ( +
+ + + +

{props.title}

+
+
+ {props.children} +
+
+ ); +} diff --git a/src/components/table.tsx b/src/components/table.tsx new file mode 100644 index 0000000..69fb929 --- /dev/null +++ b/src/components/table.tsx @@ -0,0 +1,368 @@ +// Copyright (c) Nebari Development Team. +// Distributed under the terms of the Modified BSD License. +import type { CommandRegistry } from '@lumino/commands'; +import { ReadonlyJSONObject } from '@lumino/coreutils'; +import { Time } from '@jupyterlab/coreutils'; +import { ISettingRegistry } from '@jupyterlab/settingregistry'; +import { TranslationBundle } from '@jupyterlab/translation'; +import { + FilterBox, + Table, + UseSignal, + MenuSvg +} from '@jupyterlab/ui-components'; +import * as React from 'react'; +import { ISettingsLayout, CommandIDs, IKernelItem } from '../types'; +import { starIcon } from '../icons'; + +const STAR_BUTTON_CLASS = 'jp-starIconButton'; +const KERNEL_ITEM_CLASS = 'jp-TableKernelItem'; + +function columnLabelFromKey(key: string): string { + if (key.length === 0) { + return '(empty)'; + } + switch (key) { + // Added by nb_conda_kernels <= 2.5.0 + case 'conda_env_name': + return 'Environment'; + case 'conda_env_path': + return 'Environment path'; + // Will be added once https://github.com/anaconda/nb_conda_kernels/pull/262/ is released + case 'conda_language': + return 'Language'; + case 'conda_raw_kernel_name': + return 'Kernel'; + case 'conda_is_base_environment': + return 'Base?'; + case 'conda_is_currently_running': + return 'Running?'; + } + return key[0].toUpperCase() + key.substring(1); +} + +export function KernelTable(props: { + trans: TranslationBundle; + items: IKernelItem[]; + commands: CommandRegistry; + settings: ISettingRegistry.ISettings; + showSearchBox: boolean; + query: string; + onClick: (item: IKernelItem) => void; + hideColumns?: string[]; +}) { + const { trans } = props; + let query: string; + let updateQuery: (value: string) => void; + if (props.showSearchBox) { + const [_query, _updateQuery] = React.useState(''); + query = _query; + updateQuery = _updateQuery; + } else { + query = props.query; + } + + // Hoisted to avoid "Rendered fewer hooks than expected" error on toggling the Star column + const [, forceUpdate] = React.useReducer(x => x + 1, 0); + + const metadataAvailable = new Set(); + for (const item of props.items) { + const kernelMetadata = item.metadata?.kernel; + if (!kernelMetadata) { + continue; + } + for (const key of Object.keys(kernelMetadata)) { + metadataAvailable.add(key); + } + } + + const extraColumns: Table.IColumn[] = [...metadataAvailable].map( + metadataKey => { + return { + id: metadataKey, + label: columnLabelFromKey(metadataKey), + renderCell: (item: IKernelItem) => { + const kernelMeta = item.metadata?.kernel as + | ReadonlyJSONObject + | undefined; + if (!kernelMeta) { + return '-'; + } + const value = kernelMeta[metadataKey]; + if (typeof value === 'string') { + return value; + } + return JSON.stringify(value); + }, + sort: (a: IKernelItem, b: IKernelItem) => { + const aKernelMeta = a.metadata?.kernel as + | ReadonlyJSONObject + | undefined; + const bKernelMeta = b.metadata?.kernel as + | ReadonlyJSONObject + | undefined; + const aValue = aKernelMeta ? aKernelMeta[metadataKey] : undefined; + const bValue = bKernelMeta ? bKernelMeta[metadataKey] : undefined; + if (aValue === bValue) { + return 0; + } + if (!aValue) { + return 1; + } + if (!bValue) { + return -1; + } + if (typeof aValue === 'string' && typeof bValue === 'string') { + return aValue.localeCompare(bValue); + } + return aValue > bValue ? 1 : -1; + } + }; + } + ); + + const availableColumns: Table.IColumn[] = [ + { + id: 'icon', + label: trans.__('Icon'), + renderCell: (row: IKernelItem) => ( +
props.onClick(row)} + > + {row.kernelIconUrl ? ( + {row.label} + ) : ( +
+ {row.label[0].toUpperCase()} +
+ )} +
+ ), + sort: (a: IKernelItem, b: IKernelItem) => + a.command.localeCompare(b.command) + }, + { + id: 'kernel', + label: trans.__('Kernel'), + renderCell: (row: IKernelItem) => ( + { + props.onClick(row); + event.stopPropagation(); + }} + onKeyDown={event => { + // TODO memoize func defs for perf + if (event.key === 'Enter') { + row.execute(); + } + }} + tabIndex={0} + > + {row.label} + + ), + sort: (a: IKernelItem, b: IKernelItem) => a.label.localeCompare(b.label) + }, + ...extraColumns, + { + id: 'last-used', + label: trans.__('Last Used'), + renderCell: (row: IKernelItem) => { + return ( + + {() => { + return row.lastUsed ? ( + + {Time.formatHuman(row.lastUsed)} + + ) : ( + trans.__('Never') + ); + }} + + ); + }, + sort: (a: IKernelItem, b: IKernelItem) => { + if (a.lastUsed === b.lastUsed) { + return 0; + } + if (!a.lastUsed) { + return 1; + } + if (!b.lastUsed) { + return -1; + } + return a.lastUsed > b.lastUsed ? 1 : -1; + } + }, + { + id: 'star', + label: '', + renderCell: (row: IKernelItem) => { + const starred = row.starred; + const title = starred + ? trans.__('Click to add this kernel to favourites') + : trans.__('Click to remove the kernel from favourites'); + return ( + + ); + }, + sort: (a: IKernelItem, b: IKernelItem) => + Number(a.starred) - Number(b.starred) + } + ]; + const forceHiddenColumns = props.hideColumns ?? []; + const columns = availableColumns.filter( + column => !forceHiddenColumns.includes(column.id) + ); + + const [hiddenColumns, setHiddenColumns] = React.useState< + ISettingsLayout['hiddenColumns'] + >( + (props.settings.composite + .hiddenColumns as ISettingsLayout['hiddenColumns']) ?? {} + ); + const initialColumnOrder = columns.map(c => c.id); + const [columnOrder, setColumnOrder] = React.useState< + ISettingsLayout['columnOrder'] + >( + (props.settings.composite.columnOrder as ISettingsLayout['columnOrder']) ?? + initialColumnOrder + ); + const KernelItemTable = Table; + + const onSettings = () => { + const newHiddenColumns = + (props.settings.composite + .hiddenColumns as ISettingsLayout['hiddenColumns']) ?? {}; + if (hiddenColumns !== newHiddenColumns) { + setHiddenColumns(newHiddenColumns); + } + const newColumnOrder = + (props.settings.composite + .columnOrder as ISettingsLayout['columnOrder']) ?? initialColumnOrder; + if (columnOrder !== newColumnOrder) { + setColumnOrder(newColumnOrder); + } + }; + + React.useEffect(() => { + props.settings.changed.connect(onSettings); + return () => { + props.settings.changed.disconnect(onSettings); + }; + }); + + return ( +
+ {props.showSearchBox ? ( +
+ { + updateQuery(query ?? ''); + }} + initialQuery={''} + useFuzzyFilter={false} + /> +
+ ) : null} +
{ + event.preventDefault(); + const contextMenu = new MenuSvg({ commands: props.commands }); + const columnsSubMenu = new MenuSvg({ commands: props.commands }); + for (const column of columns) { + columnsSubMenu.addItem({ + command: CommandIDs.toggleColumn, + args: { id: column.id, label: column.label } + }); + } + columnsSubMenu.title.label = trans.__('Visible Columns'); + contextMenu.addItem({ + type: 'submenu', + submenu: columnsSubMenu + }); + const id = ( + (event.target as HTMLElement).closest('th[data-id]') as HTMLElement + )?.dataset['id']; + if (id) { + contextMenu.addItem({ + command: CommandIDs.moveColumn, + args: { direction: 'left', order: columnOrder, id } + }); + contextMenu.addItem({ + command: CommandIDs.moveColumn, + args: { direction: 'right', order: columnOrder, id } + }); + } + contextMenu.open(event.clientX, event.clientY); + }} + > + + kernel.label.toLowerCase().indexOf(query.toLowerCase()) !== -1 + ) + .map(data => { + return { + data: data, + key: data.command + JSON.stringify(data.args) + }; + })} + blankIndicator={() => { + return
{trans.__('No entries')}
; + }} + sortKey={'kernel'} + onRowClick={event => { + const target = event.target as HTMLElement; + const row = target.closest('tr'); + if (!row) { + return; + } + const cell = target.closest('td'); + const starButton = cell?.querySelector(`.${STAR_BUTTON_CLASS}`); + if (starButton) { + return (starButton as HTMLElement).click(); + } + const element = row.querySelector(`.${KERNEL_ITEM_CLASS}`)!; + (element as HTMLElement).click(); + }} + columns={columns + .filter(column => !hiddenColumns[column.id]) + .map(column => { + return { + ...column, + rank: columnOrder.indexOf(column.id) ?? 10 + }; + }) + .sort((a, b) => { + return a.rank - b.rank; + })} + /> +
+
+ ); +} diff --git a/src/dialogs.tsx b/src/dialogs.tsx index 3c8f38a..d177518 100644 --- a/src/dialogs.tsx +++ b/src/dialogs.tsx @@ -19,7 +19,7 @@ import { TranslationBundle } from '@jupyterlab/translation'; -import { KernelTable } from './launcher'; +import { KernelTable } from './components/table'; import { IItem, ILastUsedDatabase, diff --git a/src/launcher.tsx b/src/launcher.tsx index edd6d03..d864fc4 100644 --- a/src/launcher.tsx +++ b/src/launcher.tsx @@ -1,123 +1,26 @@ // Copyright (c) Nebari Development Team. // Distributed under the terms of the Modified BSD License. import type { CommandRegistry } from '@lumino/commands'; -import { ReadonlyJSONObject } from '@lumino/coreutils'; -import { Time } from '@jupyterlab/coreutils'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { ILauncher, Launcher } from '@jupyterlab/launcher'; import { TranslationBundle } from '@jupyterlab/translation'; import { - classes, FilterBox, - LabIcon, - caretRightIcon, - Table, - UseSignal, - MenuSvg, notebookIcon, consoleIcon } from '@jupyterlab/ui-components'; import * as React from 'react'; import { - ISettingsLayout, - CommandIDs, IItem, IKernelItem, ILastUsedDatabase, IFavoritesDatabase } from './types'; -import { starIcon, fileIcon } from './icons'; +import { fileIcon } from './icons'; import { Item } from './item'; - -const STAR_BUTTON_CLASS = 'jp-starIconButton'; -const KERNEL_ITEM_CLASS = 'jp-TableKernelItem'; - -function TypeCard(props: { - trans: TranslationBundle; - item: IItem; -}): React.ReactElement { - const { item } = props; - return ( -
item.execute()} - className="jp-Launcher-TypeCard jp-LauncherCard" - title={item.caption} - tabIndex={0} - > -
- -
-
-

{item.label}

-
-
- ); -} - -function CollapsibleSection( - props: React.PropsWithChildren<{ - title: string; - className: string; - icon: LabIcon; - open: boolean; - }> -) { - const [open, setOpen] = React.useState(props.open); - - const handleToggle = (event: { currentTarget: { open: boolean } }) => - setOpen(event.currentTarget.open); - - return ( -
- - - -

{props.title}

-
-
- {props.children} -
-
- ); -} - -function columnLabelFromKey(key: string): string { - if (key.length === 0) { - return '(empty)'; - } - switch (key) { - // Added by nb_conda_kernels <= 2.5.0 - case 'conda_env_name': - return 'Environment'; - case 'conda_env_path': - return 'Environment path'; - // Will be added once https://github.com/anaconda/nb_conda_kernels/pull/262/ is released - case 'conda_language': - return 'Language'; - case 'conda_raw_kernel_name': - return 'Kernel'; - case 'conda_is_base_environment': - return 'Base?'; - case 'conda_is_currently_running': - return 'Running?'; - } - return key[0].toUpperCase() + key.substring(1); -} +import { KernelTable } from './components/table'; +import { CollapsibleSection } from './components/section'; +import { TypeCard } from './components/card'; function LauncherBody(props: { trans: TranslationBundle; @@ -219,332 +122,6 @@ function LauncherBody(props: { ); } -export function KernelTable(props: { - trans: TranslationBundle; - items: IKernelItem[]; - commands: CommandRegistry; - settings: ISettingRegistry.ISettings; - showSearchBox: boolean; - query: string; - onClick: (item: IKernelItem) => void; - hideColumns?: string[]; -}) { - const { trans } = props; - let query: string; - let updateQuery: (value: string) => void; - if (props.showSearchBox) { - const [_query, _updateQuery] = React.useState(''); - query = _query; - updateQuery = _updateQuery; - } else { - query = props.query; - } - - // Hoisted to avoid "Rendered fewer hooks than expected" error on toggling the Star column - const [, forceUpdate] = React.useReducer(x => x + 1, 0); - - const metadataAvailable = new Set(); - for (const item of props.items) { - const kernelMetadata = item.metadata?.kernel; - if (!kernelMetadata) { - continue; - } - for (const key of Object.keys(kernelMetadata)) { - metadataAvailable.add(key); - } - } - - const extraColumns: Table.IColumn[] = [...metadataAvailable].map( - metadataKey => { - return { - id: metadataKey, - label: columnLabelFromKey(metadataKey), - renderCell: (item: IKernelItem) => { - const kernelMeta = item.metadata?.kernel as - | ReadonlyJSONObject - | undefined; - if (!kernelMeta) { - return '-'; - } - const value = kernelMeta[metadataKey]; - if (typeof value === 'string') { - return value; - } - return JSON.stringify(value); - }, - sort: (a: IKernelItem, b: IKernelItem) => { - const aKernelMeta = a.metadata?.kernel as - | ReadonlyJSONObject - | undefined; - const bKernelMeta = b.metadata?.kernel as - | ReadonlyJSONObject - | undefined; - const aValue = aKernelMeta ? aKernelMeta[metadataKey] : undefined; - const bValue = bKernelMeta ? bKernelMeta[metadataKey] : undefined; - if (aValue === bValue) { - return 0; - } - if (!aValue) { - return 1; - } - if (!bValue) { - return -1; - } - if (typeof aValue === 'string' && typeof bValue === 'string') { - return aValue.localeCompare(bValue); - } - return aValue > bValue ? 1 : -1; - } - }; - } - ); - - const availableColumns: Table.IColumn[] = [ - { - id: 'icon', - label: trans.__('Icon'), - renderCell: (row: IKernelItem) => ( -
props.onClick(row)} - > - {row.kernelIconUrl ? ( - {row.label} - ) : ( -
- {row.label[0].toUpperCase()} -
- )} -
- ), - sort: (a: IKernelItem, b: IKernelItem) => - a.command.localeCompare(b.command) - }, - { - id: 'kernel', - label: trans.__('Kernel'), - renderCell: (row: IKernelItem) => ( - { - props.onClick(row); - event.stopPropagation(); - }} - onKeyDown={event => { - // TODO memoize func defs for perf - if (event.key === 'Enter') { - row.execute(); - } - }} - tabIndex={0} - > - {row.label} - - ), - sort: (a: IKernelItem, b: IKernelItem) => a.label.localeCompare(b.label) - }, - ...extraColumns, - { - id: 'last-used', - label: trans.__('Last Used'), - renderCell: (row: IKernelItem) => { - return ( - - {() => { - return row.lastUsed ? ( - - {Time.formatHuman(row.lastUsed)} - - ) : ( - trans.__('Never') - ); - }} - - ); - }, - sort: (a: IKernelItem, b: IKernelItem) => { - if (a.lastUsed === b.lastUsed) { - return 0; - } - if (!a.lastUsed) { - return 1; - } - if (!b.lastUsed) { - return -1; - } - return a.lastUsed > b.lastUsed ? 1 : -1; - } - }, - { - id: 'star', - label: '', - renderCell: (row: IKernelItem) => { - const starred = row.starred; - const title = starred - ? trans.__('Click to add this kernel to favourites') - : trans.__('Click to remove the kernel from favourites'); - return ( - - ); - }, - sort: (a: IKernelItem, b: IKernelItem) => - Number(a.starred) - Number(b.starred) - } - ]; - const forceHiddenColumns = props.hideColumns ?? []; - const columns = availableColumns.filter( - column => !forceHiddenColumns.includes(column.id) - ); - - const [hiddenColumns, setHiddenColumns] = React.useState< - ISettingsLayout['hiddenColumns'] - >( - (props.settings.composite - .hiddenColumns as ISettingsLayout['hiddenColumns']) ?? {} - ); - const initialColumnOrder = columns.map(c => c.id); - const [columnOrder, setColumnOrder] = React.useState< - ISettingsLayout['columnOrder'] - >( - (props.settings.composite.columnOrder as ISettingsLayout['columnOrder']) ?? - initialColumnOrder - ); - const KernelItemTable = Table; - - const onSettings = () => { - const newHiddenColumns = - (props.settings.composite - .hiddenColumns as ISettingsLayout['hiddenColumns']) ?? {}; - if (hiddenColumns !== newHiddenColumns) { - setHiddenColumns(newHiddenColumns); - } - const newColumnOrder = - (props.settings.composite - .columnOrder as ISettingsLayout['columnOrder']) ?? initialColumnOrder; - if (columnOrder !== newColumnOrder) { - setColumnOrder(newColumnOrder); - } - }; - - React.useEffect(() => { - props.settings.changed.connect(onSettings); - return () => { - props.settings.changed.disconnect(onSettings); - }; - }); - - return ( -
- {props.showSearchBox ? ( -
- { - updateQuery(query ?? ''); - }} - initialQuery={''} - useFuzzyFilter={false} - /> -
- ) : null} -
{ - event.preventDefault(); - const contextMenu = new MenuSvg({ commands: props.commands }); - const columnsSubMenu = new MenuSvg({ commands: props.commands }); - for (const column of columns) { - columnsSubMenu.addItem({ - command: CommandIDs.toggleColumn, - args: { id: column.id, label: column.label } - }); - } - columnsSubMenu.title.label = trans.__('Visible Columns'); - contextMenu.addItem({ - type: 'submenu', - submenu: columnsSubMenu - }); - const id = ( - (event.target as HTMLElement).closest('th[data-id]') as HTMLElement - )?.dataset['id']; - if (id) { - contextMenu.addItem({ - command: CommandIDs.moveColumn, - args: { direction: 'left', order: columnOrder, id } - }); - contextMenu.addItem({ - command: CommandIDs.moveColumn, - args: { direction: 'right', order: columnOrder, id } - }); - } - contextMenu.open(event.clientX, event.clientY); - }} - > - - kernel.label.toLowerCase().indexOf(query.toLowerCase()) !== -1 - ) - .map(data => { - return { - data: data, - key: data.command + JSON.stringify(data.args) - }; - })} - blankIndicator={() => { - return
{trans.__('No entries')}
; - }} - sortKey={'kernel'} - onRowClick={event => { - const target = event.target as HTMLElement; - const row = target.closest('tr'); - if (!row) { - return; - } - const cell = target.closest('td'); - const starButton = cell?.querySelector(`.${STAR_BUTTON_CLASS}`); - if (starButton) { - return (starButton as HTMLElement).click(); - } - const element = row.querySelector(`.${KERNEL_ITEM_CLASS}`)!; - (element as HTMLElement).click(); - }} - columns={columns - .filter(column => !hiddenColumns[column.id]) - .map(column => { - return { - ...column, - rank: columnOrder.indexOf(column.id) ?? 10 - }; - }) - .sort((a, b) => { - return a.rank - b.rank; - })} - /> -
-
- ); -} - export namespace NewLauncher { export interface IOptions extends ILauncher.IOptions { lastUsedDatabase: ILastUsedDatabase; diff --git a/tsconfig.json b/tsconfig.json index 9897917..bbb3f18 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,5 +19,5 @@ "strictNullChecks": true, "target": "ES2018" }, - "include": ["src/*"] + "include": ["src/*", "src/*/*"] }