/** * Copyright (c) 2020-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { useApplicationStore } from '@finos/legend-application'; import { observer } from 'mobx-react-lite'; import { BlankPanelContent, Panel, PanelLoadingIndicator, TreeView, type TreeNodeContainerProps, ChevronDownIcon, ChevronRightIcon, GenericTextFileIcon, FolderIcon, FolderOpenIcon, HomeIcon, SearchIcon, TimesIcon, CodeIcon, clsx, CopyIcon, compareLabelFn, } from '@finos/legend-art'; import { SHOWCASE_MANAGER_SEARCH_CATEGORY, SHOWCASE_MANAGER_VIEW, ShowcaseManagerState, type ShowcaseTextSearchMatchResult, type ShowcasesExplorerTreeNodeData, } from '../stores/ShowcaseManagerState.js'; import { SHOWCASE_LAUNCH_ENTRY_POINT, SHOWCASE_FEEDBACK_SURFACE, } from '../__lib__/LegendStudioTelemetryHelper.js'; import { ShowcaseFeedback } from './showcase/ShowcaseFeedback.js'; import { debounce, isNonNullable } from '@finos/legend-shared'; import { flowResult } from 'mobx'; import type { Showcase } from '@finos/legend-server-showcase'; import { CODE_EDITOR_LANGUAGE } from '@finos/legend-code-editor'; import { CodeEditor } from '@finos/legend-lego/code-editor'; import React, { useEffect, useMemo, useRef } from 'react'; import { generateShowcasePath } from '../__lib__/LegendStudioNavigation.js'; const ShowcasesExplorerTreeNodeContainer = observer( ( props: TreeNodeContainerProps< ShowcasesExplorerTreeNodeData, { showcaseManagerState: ShowcaseManagerState; toggleExpandNode: (node: ShowcasesExplorerTreeNodeData) => void; } >, ) => { const { node, level, innerProps } = props; const { toggleExpandNode, showcaseManagerState } = innerProps; const applicationStore = useApplicationStore(); const expandIcon = !node.metadata ? ( node.isOpen ? ( ) : ( ) ) : (
); const nodeIcon = !node.metadata ? ( node.isOpen ? ( ) : ( ) ) : ( ); const onNodeClick = (): void => { if (!node.metadata) { toggleExpandNode(node); } else { flowResult( showcaseManagerState.openShowcase( node.metadata, undefined, SHOWCASE_LAUNCH_ENTRY_POINT.EXPLORER, ), ).catch(applicationStore.alertUnhandledError); } }; return (
{expandIcon}
{nodeIcon}
{node.label}
); }, ); const ShowcaseManagerExplorer = observer( (props: { showcaseManagerState: ShowcaseManagerState }) => { const { showcaseManagerState } = props; const treeData = showcaseManagerState.explorerTreeData; const getChildNodes = ( node: ShowcasesExplorerTreeNodeData, ): ShowcasesExplorerTreeNodeData[] => { if (treeData) { return node.childrenIds .map((id) => treeData.nodes.get(id)) .filter(isNonNullable) .sort(compareLabelFn); } return []; }; const toggleExpandNode = (node: ShowcasesExplorerTreeNodeData): void => { if (treeData) { node.isOpen = !node.isOpen; showcaseManagerState.setExplorerTreeData({ ...treeData }); } }; return (
Showcases
Explorer
{showcaseManagerState.explorerTreeData && ( )}
); }, ); const renderPreviewLine = ( line: number, text: string, result: ShowcaseTextSearchMatchResult, ): React.ReactNode => { const lineMatches = result.match.matches .filter((match) => match.line === line) .sort((a, b) => a.startColumn - b.startColumn); const chunks: React.ReactNode[] = []; let currentIdx = 0; lineMatches.forEach((match, idx) => { if (currentIdx < match.startColumn - 1) { chunks.push(text.substring(currentIdx, match.startColumn - 1)); } chunks.push( {text.substring(match.startColumn - 1, match.endColumn - 1)} , ); currentIdx = match.endColumn - 1; }); if (currentIdx < text.length) { chunks.push(text.substring(currentIdx, text.length)); } return ( <> {chunks.map((chunk, idx) => ( // eslint-disable-next-line react/no-array-index-key {chunk} ))} ); }; const ShowcaseManagerCodeSearchResult = observer( (props: { showcaseManagerState: ShowcaseManagerState; result: ShowcaseTextSearchMatchResult; }) => { const { showcaseManagerState, result } = props; const applicationStore = useApplicationStore(); return (
{ flowResult( showcaseManagerState.openShowcase( result.showcase, undefined, SHOWCASE_LAUNCH_ENTRY_POINT.SEARCH_SHOWCASE_MATCH, ), ).catch(applicationStore.alertUnhandledError); }} >
{result.showcase.path}
{result.match.preview.map((entry) => (
{ flowResult( showcaseManagerState.openShowcase( result.showcase, entry.line, SHOWCASE_LAUNCH_ENTRY_POINT.SEARCH_CODE_MATCH, ), ).catch(applicationStore.alertUnhandledError); }} >
match.line === entry.line, ), ), }, )} > {entry.line}
{renderPreviewLine(entry.line, entry.text, result)}
))}
); }, ); const ShowcaseManagerSearchPanel = observer( (props: { showcaseManagerState: ShowcaseManagerState }) => { const { showcaseManagerState } = props; const applicationStore = useApplicationStore(); const searchTextInpurRef = useRef(null); const debouncedSearch = useMemo( () => debounce( () => flowResult(showcaseManagerState.search()).catch( applicationStore.alertUnhandledError, ), 300, ), [applicationStore, showcaseManagerState], ); const clearSearchText = (): void => { debouncedSearch.cancel(); showcaseManagerState.resetSearch(); }; const onSearchTextChange: React.ChangeEventHandler = ( event, ): void => { const value = event.target.value; showcaseManagerState.setSearchText(value); debouncedSearch.cancel(); if (!value) { showcaseManagerState.resetSearch(); } else { debouncedSearch()?.catch(applicationStore.alertUnhandledError); } }; const onSearchKeyDown: React.KeyboardEventHandler = ( event, ) => { if (event.code === 'Enter') { debouncedSearch.cancel(); if (showcaseManagerState.searchText) { debouncedSearch()?.catch(applicationStore.alertUnhandledError); } } else if (event.code === 'Escape') { searchTextInpurRef.current?.select(); } }; useEffect(() => { searchTextInpurRef.current?.select(); searchTextInpurRef.current?.focus(); }, []); return (
{ showcaseManagerState.closeShowcase(); showcaseManagerState.setCurrentView( SHOWCASE_MANAGER_VIEW.EXPLORER, ); }} >
Showcases
Search
{!showcaseManagerState.searchText ? (
) : ( )}
showcaseManagerState.setCurrentSearchCategory( SHOWCASE_MANAGER_SEARCH_CATEGORY.SHOWCASE, ) } title="Click to select category" >
Showcases
{showcaseManagerState.showcaseSearchResults?.length ?? 0}
showcaseManagerState.setCurrentSearchCategory( SHOWCASE_MANAGER_SEARCH_CATEGORY.CODE, ) } title="Click to select category" >
Code
{showcaseManagerState.textSearchResults?.length ?? 0}
{showcaseManagerState.currentSearchCaterogy === SHOWCASE_MANAGER_SEARCH_CATEGORY.SHOWCASE && ( <> {!showcaseManagerState.showcaseSearchResults?.length && ( No results )} {showcaseManagerState.showcaseSearchResults?.map( (showcase) => (
{ flowResult( showcaseManagerState.openShowcase(showcase), ).catch(applicationStore.alertUnhandledError); }} >
{showcase.title}
{showcase.description ?? '(no description)'}
), )} )} {showcaseManagerState.currentSearchCaterogy === SHOWCASE_MANAGER_SEARCH_CATEGORY.CODE && ( <> {!showcaseManagerState.textSearchResults?.length && ( No results )} {showcaseManagerState.textSearchResults?.map((result) => ( ))} )}
); }, ); const ShowcaseViewer = observer( (props: { showcaseManagerState: ShowcaseManagerState; showcase: Showcase; }) => { const { showcaseManagerState, showcase } = props; const prettyPath = showcase.path.replaceAll(/\s*\/\s*/g, ' / '); const launchShowcase = (): void => { const applicationStore = showcaseManagerState.applicationStore; applicationStore.navigationService.navigator.visitAddress( applicationStore.navigationService.navigator.generateAddress( generateShowcasePath(showcase.path), ), ); }; const handleCopy = showcaseManagerState.applicationStore.guardUnhandledError(() => showcaseManagerState.applicationStore.clipboardService.copyTextToClipboard( showcase.code, { notifySuccessMessage: 'Showcase grammar copied to clipboard', }, ), ); return (
{ showcaseManagerState.closeShowcase(); showcaseManagerState.setCurrentView( SHOWCASE_MANAGER_VIEW.EXPLORER, ); }} >
Showcases
{showcase.title}
{showcase.title}
{prettyPath}
); }, ); const ShowcaseManagerContent = observer( (props: { showcaseManagerState: ShowcaseManagerState }) => { const { showcaseManagerState } = props; const currentShowcase = showcaseManagerState.currentShowcase; const currentView = showcaseManagerState.currentView; return (
{currentShowcase && ( )} {!currentShowcase && ( <> {currentView === SHOWCASE_MANAGER_VIEW.EXPLORER && ( )} {currentView === SHOWCASE_MANAGER_VIEW.SEARCH && ( )} )}
); }, ); export const ShowcaseManager = observer(() => { const applicationStore = useApplicationStore(); const showcaseManagerState = ShowcaseManagerState.retrieveNullableState(applicationStore); if (!showcaseManagerState) { return null; } return ( {showcaseManagerState.initState.isInProgress && ( Initializing... )} {showcaseManagerState.initState.hasFailed && ( Failed to initialize )} {showcaseManagerState.initState.hasSucceeded && ( )} ); });