diff --git a/src/browser/components/buttons/index.tsx b/src/browser/components/buttons/index.tsx index ead9bd8812e..a565529fa20 100644 --- a/src/browser/components/buttons/index.tsx +++ b/src/browser/components/buttons/index.tsx @@ -36,12 +36,7 @@ export const EditorButton = (props: any): JSX.Element => { const { icon, title, color, width, onClick, ...rest } = props const overrideColor = { ...(color ? { color } : {}) } return ( - + { ) } -const BaseButton: any = styled.span` +const BaseButton = styled.span` font-family: ${props => props.theme.streamlineFontFamily}; font-style: normal !important; font-weight: 400 !important; @@ -107,10 +102,10 @@ export const StyledNavigationButton = styled.button` } ` -export const NavigationButtonContainer: any = styled.li` +export const NavigationButtonContainer = styled.li<{ isOpen: boolean }>` min-height: 70px; height: 70px; - background-color: ${(props: any) => + background-color: ${props => !props.isOpen ? 'transparent' : props.theme.drawerBackground}; &:focus { outline: none; diff --git a/src/browser/modules/Carousel/Carousel.tsx b/src/browser/modules/Carousel/Carousel.tsx index b1b25929c20..ea3d02b6b65 100644 --- a/src/browser/modules/Carousel/Carousel.tsx +++ b/src/browser/modules/Carousel/Carousel.tsx @@ -108,7 +108,7 @@ export default function Carousel({ onKeyDown(e)} - tabIndex="0" + tabIndex={0} > {visibleSlide > 0 && ( { /> ) } - const inspectingItemType = - !this.state.inspectorContracted && - ((this.state.hoveredItem && this.state.hoveredItem.type !== 'canvas') || - (this.state.selectedItem && this.state.selectedItem.type !== 'canvas')) return ( { className={ Object.keys(this.state.stats.relTypes).length ? '' : 'one-legend-row' } - forcePaddingBottom={ - inspectingItemType ? this.state.forcePaddingBottom : null - } > {legend} { /> { }) } - getVisualAreaHeight() { - return this.props.frameHeight && this.props.fullscreen - ? this.props.frameHeight - - (dim.frameStatusbarHeight + dim.frameTitlebarHeight * 2) - : this.props.frameHeight - dim.frameStatusbarHeight || - this.svgElement.parentNode.offsetHeight - } - componentDidMount() { if (this.svgElement != null) { this.initGraphView() @@ -81,16 +72,9 @@ export class GraphComponent extends Component { initGraphView() { if (!this.graphView) { const NeoConstructor = graphView - const measureSize = () => { - return { - width: this.svgElement.offsetWidth, - height: this.getVisualAreaHeight() - } - } this.graph = createGraph(this.props.nodes, this.props.relationships) this.graphView = new NeoConstructor( this.svgElement, - measureSize, this.graph, this.props.graphStyle ) @@ -104,7 +88,6 @@ export class GraphComponent extends Component { ) this.graphEH.bindEventHandlers() this.props.onGraphModelChange(getGraphStats(this.graph)) - this.graphView.resize() this.graphView.update() } } @@ -124,12 +107,6 @@ export class GraphComponent extends Component { if (prevProps.styleVersion !== this.props.styleVersion) { this.graphView.update() } - if ( - this.props.fullscreen !== prevProps.fullscreen || - this.props.frameHeight !== prevProps.frameHeight - ) { - this.graphView.resize() - } } zoomButtons() { @@ -141,7 +118,7 @@ export class GraphComponent extends Component { } onClick={this.zoomInClicked.bind(this)} > - + { } onClick={this.zoomOutClicked.bind(this)} > - + ) } + //TODO zoom in iconsize render() { return ( diff --git a/src/browser/modules/D3Visualization/components/Inspector.tsx b/src/browser/modules/D3Visualization/components/Inspector.tsx index 025a9165b72..0d9e056f69b 100644 --- a/src/browser/modules/D3Visualization/components/Inspector.tsx +++ b/src/browser/modules/D3Visualization/components/Inspector.tsx @@ -216,10 +216,7 @@ export class InspectorComponent extends Component< } return ( - + props.theme.secondaryText}; @@ -340,7 +340,7 @@ export const StyledCaptionSelector = styled.a` } ` -export const StyledFullSizeContainer: any = styled.div` +export const StyledFullSizeContainer = styled.div` position: relative; height: 100%; ` diff --git a/src/browser/modules/D3Visualization/lib/visualization/components/graphView.ts b/src/browser/modules/D3Visualization/lib/visualization/components/graphView.ts index 6d503781a2f..d5efc3a8c96 100644 --- a/src/browser/modules/D3Visualization/lib/visualization/components/graphView.ts +++ b/src/browser/modules/D3Visualization/lib/visualization/components/graphView.ts @@ -25,11 +25,11 @@ export default class graphView { graph: any style: any viz: any - constructor(element: any, measureSize: any, graph: any, style: any) { + constructor(element: any, graph: any, style: any) { this.graph = graph this.style = style const forceLayout = layout.force() - this.viz = viz(element, measureSize, this.graph, forceLayout, this.style) + this.viz = viz(element, this.graph, forceLayout, this.style) this.callbacks = {} const { callbacks } = this this.viz.trigger = (() => (event: any, ...args: any[]) => @@ -67,11 +67,6 @@ export default class graphView { return this } - resize() { - this.viz.resize() - return this - } - boundingBox() { return this.viz.boundingBox() } diff --git a/src/browser/modules/D3Visualization/lib/visualization/components/visualization.ts b/src/browser/modules/D3Visualization/lib/visualization/components/visualization.ts index 227c262fd21..5956bd4a3c5 100644 --- a/src/browser/modules/D3Visualization/lib/visualization/components/visualization.ts +++ b/src/browser/modules/D3Visualization/lib/visualization/components/visualization.ts @@ -24,13 +24,7 @@ import * as vizRenderers from '../renders/init' import { menu as menuRenderer } from '../renders/menu' import vizClickHandler from '../utils/clickHandler' -const vizFn = function( - el: any, - measureSize: any, - graph: any, - layout: any, - style: any -) { +const vizFn = function(el: any, graph: any, layout: any, style: any) { const viz: any = { style } const root = d3.select(el) @@ -338,26 +332,12 @@ const vizFn = function( if (updateViz) { force.update(graph, [layoutDimension, layoutDimension]) - viz.resize() viz.trigger('updated') } return (updateViz = true) } - viz.resize = function() { - const size = measureSize() - return root.attr( - 'viewBox', - [ - 0, - (layoutDimension - size.height) / 2, - layoutDimension, - size.height - ].join(' ') - ) - } - // @ts-expect-error ts-migrate(2339) FIXME: Property 'boundingBox' does not exist on type '{ s... Remove this comment to see the full error message viz.boundingBox = () => container.node().getBBox() diff --git a/src/browser/modules/Frame/DefaultFrame.tsx b/src/browser/modules/Frame/DefaultFrame.tsx index 3b79b01eb38..c2b1d977765 100644 --- a/src/browser/modules/Frame/DefaultFrame.tsx +++ b/src/browser/modules/Frame/DefaultFrame.tsx @@ -34,6 +34,6 @@ const DefaultFrame = ({ frame }: any): JSX.Element => { frameContents = 'Unknown command' } - return + return } export default DefaultFrame diff --git a/src/browser/modules/Frame/FrameTemplate.tsx b/src/browser/modules/Frame/FrameTemplate.tsx index e848490d892..ab3e0ac63b6 100644 --- a/src/browser/modules/Frame/FrameTemplate.tsx +++ b/src/browser/modules/Frame/FrameTemplate.tsx @@ -18,12 +18,9 @@ * along with this program. If not, see . */ -import React, { useRef, useState, useEffect } from 'react' -import { Frame } from 'shared/modules/stream/streamDuck' -import FrameTitlebar from './FrameTitlebar' +import React from 'react' import { - StyledFrame, StyledFrameBody, StyledFrameContents, StyledFrameStatusbar, @@ -33,134 +30,40 @@ import { type FrameTemplateProps = { contents: JSX.Element | null | string - header?: Frame - className?: string - onResize?: (fullscreen: boolean, collapsed: boolean, height: number) => void - numRecords?: number - getRecords?: () => any - visElement?: any - runQuery?: () => any sidebar?: () => JSX.Element | null aside?: JSX.Element | null statusbar?: JSX.Element | null + removePadding?: boolean + hasSlides?: boolean } function FrameTemplate({ - header, contents, - onResize = () => { - /*noop*/ - }, - className, - numRecords = 0, - getRecords, - visElement, - runQuery, sidebar, aside, - statusbar + statusbar, + removePadding = false, + hasSlides = false }: FrameTemplateProps): JSX.Element { - const [lastHeight, setLastHeight] = useState(10) - const frameContentElementRef = useRef(null) - - const { - isFullscreen, - isCollapsed, - isPinned, - toggleFullScreen, - toggleCollapse, - togglePin - } = useSizeToggles() - - useEffect(() => { - if (!frameContentElementRef.current?.clientHeight) return - const currHeight = frameContentElementRef.current.clientHeight - if (currHeight < 300) return // No need to report a transition - - if (lastHeight !== currHeight) { - onResize(isFullscreen, isCollapsed, currHeight) - setLastHeight(currHeight) - } - }, [lastHeight, isPinned, isFullscreen, isCollapsed, onResize]) - - const classNames = [] - if (className) { - classNames.push(className) - } - if (isFullscreen) { - classNames.push('is-fullscreen') - } - return ( - - {header && ( - - )} - - + <> + {sidebar && sidebar()} {aside && {aside}} - + {contents} {statusbar && ( - + {statusbar} )} - + ) } -function useSizeToggles() { - const [isFullscreen, setFullscreen] = useState(false) - const [isCollapsed, setCollapsed] = useState(false) - const [isPinned, setPinned] = useState(false) - - function toggleFullScreen() { - setFullscreen(full => !full) - } - function toggleCollapse() { - setCollapsed(coll => !coll) - } - function togglePin() { - setPinned(pin => !pin) - } - return { - isFullscreen, - isCollapsed, - isPinned, - toggleFullScreen, - toggleCollapse, - togglePin - } -} - export default FrameTemplate diff --git a/src/browser/modules/Frame/FrameTitlebar.tsx b/src/browser/modules/Frame/FrameTitlebar.tsx index 61292f6695f..1f2787e0f45 100644 --- a/src/browser/modules/Frame/FrameTitlebar.tsx +++ b/src/browser/modules/Frame/FrameTitlebar.tsx @@ -19,12 +19,8 @@ */ import { connect } from 'react-redux' -import React, { useCallback, useEffect, useRef, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { withBus } from 'react-suber' -import { saveAs } from 'file-saver' -import { map } from 'lodash-es' -import SVGInline from 'react-svg-inline' -import controlsPlay from 'icons/controls-play.svg' import * as app from 'shared/modules/app/appDuck' import * as commands from 'shared/modules/commands/commandsDuck' @@ -53,7 +49,6 @@ import { CloseIcon, ContractIcon, DownIcon, - DownloadIcon, ExpandIcon, PinIcon, RunIcon, @@ -61,49 +56,27 @@ import { SaveFavoriteIcon, StopIcon } from 'browser-components/icons/Icons' -import { - DottedLineHover, - DropdownButton, - DropdownContent, - DropdownItem, - DropDownItemDivider, - DropdownList -} from '../Stream/styled' +import { DottedLineHover } from '../Stream/styled' import { StyledFrameTitleBar, StyledFrameTitlebarButtonSection, FrameTitleEditorContainer, StyledFrameCommand } from './styled' -import { - downloadPNGFromSVG, - downloadSVG -} from 'shared/services/exporting/imageUtils' -import { - stringifyResultArray, - transformResultRecordsToResultArray, - recordToJSONMapper -} from 'browser/modules/Stream/CypherFrame/helpers' -import { csvFormat, stringModifier } from 'services/bolt/cypherTypesFormatting' -import arrayHasItems from 'shared/utils/array-has-items' -import { stringifyMod } from 'services/utils' import Monaco, { MonacoHandles } from '../Editor/Monaco' import { Bus } from 'suber' import { addFavorite } from 'shared/modules/favorites/favoritesDuck' +import { GlobalState } from 'shared/globalState' type FrameTitleBarBaseProps = { - frame: any + frame: Frame fullscreen: boolean fullscreenToggle: () => void collapse: boolean collapseToggle: () => void - pinned: boolean - togglePin: () => void - numRecords: number - getRecords: () => any - visElement: any - runQuery: () => any + isPinned: boolean bus: Bus + ExportButton: JSX.Element } type FrameTitleBarProps = FrameTitleBarBaseProps & { @@ -117,7 +90,6 @@ type FrameTitleBarProps = FrameTitleBarBaseProps & { requestId: string, request: BrowserRequest | null ) => void - onRunClick: () => void reRun: (obj: Frame, cmd: string) => void togglePinning: (id: string, isPinned: boolean) => void onTitlebarClick: (cmd: string) => void @@ -126,137 +98,19 @@ type FrameTitleBarProps = FrameTitleBarBaseProps & { } function FrameTitlebar(props: FrameTitleBarProps) { - const [editorValue, setEditorValue] = useState(props.frame.cmd) - const [renderEditor, setRenderEditor] = useState(props.frame.isRerun) + const { frame } = props + const [editorValue, setEditorValue] = useState(frame.cmd) + const [renderEditor, setRenderEditor] = useState(frame.isRerun) useEffect(() => { // makes sure the frame is updated as links in frame is followed editorRef.current?.setValue(props.frame.cmd) }, [props.frame.cmd]) const editorRef = useRef(null) - /* When the frametype is changed the titlebar is unmounted - and replaced with a new instance. This means focus cursor position are lost. - To regain editor focus we run an effect dependant on the isRerun prop. - However, when the frame prop changes in some way the effect is retriggered - although the "isRun" is still true. Use effect does not check for equality - but instead re-runs the effect to take focus again. To prevent this - we use the useCallback hook as well. As a best effort we set the cursor position - to be at the end of the query. - - A better solution is to change the frame titlebar to reside outside of the - frame contents. - */ - - const gainFocusCallback = useCallback(() => { - if (props.frame.isRerun) { - editorRef.current?.focus() - - const lines = (editorRef.current?.getValue() || '').split('\n') - const linesLength = lines.length - editorRef.current?.setPosition({ - lineNumber: linesLength, - column: lines[linesLength - 1].length + 1 - }) - } - }, [props.frame.isRerun]) - useEffect(gainFocusCallback, [gainFocusCallback]) - - function hasData() { - return props.numRecords > 0 - } - - function exportCSV(records: any) { - const exportData = stringifyResultArray( - csvFormat, - transformResultRecordsToResultArray(records) - ) - const data = exportData.slice() - const csv = CSVSerializer(data.shift()) - csv.appendRows(data) - const blob = new Blob([csv.output()], { - type: 'text/plain;charset=utf-8' - }) - saveAs(blob, 'export.csv') - } - - function exportTXT() { - const { frame } = props - - if (frame.type === 'history') { - const asTxt = frame.result - .map((result: string) => { - const safe = `${result}`.trim() - - if (safe.startsWith(':')) { - return safe - } - - return safe.endsWith(';') ? safe : `${safe};` - }) - .join('\n\n') - const blob = new Blob([asTxt], { - type: 'text/plain;charset=utf-8' - }) - - saveAs(blob, 'history.txt') - } - } - - function exportJSON(records: any) { - const exportData = map(records, recordToJSONMapper) - const data = stringifyMod(exportData, stringModifier, true) - const blob = new Blob([data], { - type: 'text/plain;charset=utf-8' - }) - saveAs(blob, 'records.json') - } - - function exportPNG() { - const { svgElement, graphElement, type } = props.visElement - downloadPNGFromSVG(svgElement, graphElement, type) - } - - function exportSVG() { - const { svgElement, graphElement, type } = props.visElement - downloadSVG(svgElement, graphElement, type) - } - - function exportGrass(data: any) { - const blob = new Blob([data], { - type: 'text/plain;charset=utf-8' - }) - saveAs(blob, 'style.grass') - } - - /* - * Displaying the download icon even if there is no result or visualization - * prevents the run/stop icon from "jumping" as the download icon disappears - * and reappears when running fast queries. - */ - const displayDownloadIcon = () => - props?.frame?.type === 'cypher' || canExport() - - function canExport() { - const { frame = {}, visElement } = props - - return ( - canExportTXT() || - (frame.type === 'cypher' && (hasData() || visElement)) || - (frame.type === 'style' && hasData()) - ) - } - - function canExportTXT() { - const { frame = {} } = props - - return frame.type === 'history' && arrayHasItems(frame.result) - } - function run(cmd: string) { props.reRun(frame, cmd) } - const { frame = {} } = props const fullscreenIcon = props.fullscreen ? : const expandCollapseIcon = props.collapse ? : // the last run command (history index 1) is already in the editor @@ -313,62 +167,13 @@ function FrameTitlebar(props: FrameTitleBarProps) { > - - - - - - - - props.newProjectFile(frame.cmd)} - > - Save as project file - - - - - exportCSV(props.getRecords())}> - Export CSV - - exportJSON(props.getRecords())} - > - Export JSON - - - - exportPNG()}> - Export PNG - - exportSVG()}> - Export SVG - - - - Export TXT - - - exportGrass(props.getRecords())} - > - Export GraSS - - - - - - - + {props.ExportButton} { - props.togglePin() - // using frame.isPinned causes issues when there are multiple frames in one - props.togglePinning(frame.id, props.pinned) + props.togglePinning(frame.id, props.isPinned) }} - pressed={props.pinned} + pressed={props.isPinned} > @@ -393,11 +198,6 @@ function FrameTitlebar(props: FrameTitleBarProps) { > {expandCollapseIcon} - - props.onRunClick()}> - - - { @@ -411,7 +211,10 @@ function FrameTitlebar(props: FrameTitleBarProps) { ) } -const mapStateToProps = (state: any, ownProps: FrameTitleBarBaseProps) => { +const mapStateToProps = ( + state: GlobalState, + ownProps: FrameTitleBarBaseProps +) => { const request = ownProps.frame.requestId ? getRequest(state, ownProps.frame.requestId) : null @@ -455,9 +258,6 @@ const mapDispatchToProps = ( } dispatch(remove(id)) }, - onRunClick: () => { - ownProps.runQuery() - }, reRun: ({ useDb, id, requestId }: Frame, cmd: string) => { if (requestId) { dispatch(cancelRequest(requestId)) @@ -475,7 +275,7 @@ const mapDispatchToProps = ( togglePinning: (id: string, isPinned: boolean) => { isPinned ? dispatch(unpin(id)) : dispatch(pin(id)) }, - onTitlebarClick: (cmd: any) => { + onTitlebarClick: (cmd: string) => { ownProps.bus.send(editor.SET_CONTENT, editor.setContent(cmd)) } } diff --git a/src/browser/modules/Frame/styled.tsx b/src/browser/modules/Frame/styled.tsx index 37bdf848f1e..028bb6cb3ec 100644 --- a/src/browser/modules/Frame/styled.tsx +++ b/src/browser/modules/Frame/styled.tsx @@ -47,36 +47,28 @@ z-index: 130;` border-radius: 2px; ` -export const StyledFrameBody = styled.div< - FullscreenProps & { collapsed: boolean } ->` +export const StyledFrameBody = styled.div<{ + removePadding?: boolean + hasSlides?: boolean +}>` overflow: auto; min-height: ${dim.frameBodyHeight / 2}px; - max-height: ${props => { - if (props.collapsed) { - return 0 - } - if (props.fullscreen) { - return '100%' - } - return dim.frameBodyHeight - dim.frameStatusbarHeight + 1 + 'px' - }}; - display: ${props => (props.collapsed ? 'none' : 'flex')}; + max-height: 100%; + display: flex; flex-direction: row; width: 100%; padding: 30px; - .has-carousel &, - .has-stack & { + ${props => + props.hasSlides + ? ` position: relative; padding-bottom: 40px; padding-left: 40px; - padding-right: 40px; - } + padding-right: 40px;` + : ''} - .no-padding & { - padding: 0; - } + ${props => (props.removePadding ? 'padding: 0;' : '')} ` export const StyledFrameMainSection = styled.div` @@ -99,18 +91,15 @@ export const StyledFrameAside = styled.div` min-width: 120px; ` -export const StyledFrameContents = styled.div` +export const StyledFrameContents = styled.div` font-size: 14px; overflow: auto; min-height: ${dim.frameBodyHeight / 2}px; - max-height: ${props => - props.fullscreen - ? '100vh' - : dim.frameBodyHeight - dim.frameStatusbarHeight * 2 + 'px'}; - ${props => (props.fullscreen ? 'height: 100vh' : null)}; - flex: auto; + max-height: 100%; + flex: 1 1 auto; display: flex; width: 100%; + height: 100%; .has-carousel & { overflow: visible; @@ -121,10 +110,9 @@ export const StyledFrameContents = styled.div` } ` -export const StyledFrameStatusbar = styled.div` +export const StyledFrameStatusbar = styled.div` border-top: ${props => props.theme.inFrameBorder}; height: ${dim.frameStatusbarHeight - 1}px; - ${props => (props.fullscreen ? 'margin-top: -78px;' : '')}; display: flex; flex-direction: row; flex: none; @@ -193,7 +181,7 @@ export const FrameTitleEditorContainer = styled.div` } ` -export const StyledFrameCommand = styled.label<{ selectedDb: string }>` +export const StyledFrameCommand = styled.label<{ selectedDb: string | null }>` font-family: ${props => props.theme.editorFont}; color: ${props => props.theme.secondaryButtonText}; background-color: ${props => props.theme.frameSidebarBackground}; diff --git a/src/browser/modules/Main/SyncConsentBanner.tsx b/src/browser/modules/Main/SyncConsentBanner.tsx index c5d683b01f8..4078df01c89 100644 --- a/src/browser/modules/Main/SyncConsentBanner.tsx +++ b/src/browser/modules/Main/SyncConsentBanner.tsx @@ -57,7 +57,7 @@ const SyncReminderBanner = React.memo(function SyncReminderBanner({ return ( - + To enjoy the full Neo4j Browser experience, we advise you to use diff --git a/src/browser/modules/Main/SyncReminderBanner.tsx b/src/browser/modules/Main/SyncReminderBanner.tsx index d9770f1b0fd..2b93b5fa063 100644 --- a/src/browser/modules/Main/SyncReminderBanner.tsx +++ b/src/browser/modules/Main/SyncReminderBanner.tsx @@ -92,7 +92,7 @@ class SyncReminderBanner extends Component { return ( - + You are currently not signed into Neo4j Browser Sync. Connect through a simple social sign-in to get started. diff --git a/src/browser/modules/Main/styled.tsx b/src/browser/modules/Main/styled.tsx index b7d67bc2c7f..6b8e70cbe8d 100644 --- a/src/browser/modules/Main/styled.tsx +++ b/src/browser/modules/Main/styled.tsx @@ -82,10 +82,11 @@ export const StyledCodeBlockFrame = styled(StyledCodeBlock)` cursor: pointer; ` -export const SyncDisconnectedBanner: any = styled(Banner)` +export const SyncDisconnectedBanner = styled(Banner)` background-color: ${props => props.theme.auth}; display: flex; justify-content: space-between; + height: 100px; ` export const SyncSignInBarButton: any = styled(SyncSignInButton)` diff --git a/src/browser/modules/Stream/Auth/ChangePasswordFrame.tsx b/src/browser/modules/Stream/Auth/ChangePasswordFrame.tsx index 3506e93157a..d3cd55c5bf3 100644 --- a/src/browser/modules/Stream/Auth/ChangePasswordFrame.tsx +++ b/src/browser/modules/Stream/Auth/ChangePasswordFrame.tsx @@ -89,7 +89,6 @@ export class ChangePasswordFrame extends Component< ) return ( { render() { return ( { } const Frame = (props: any) => { - return ( - } /> - ) + return } /> } export default Frame diff --git a/src/browser/modules/Stream/Auth/DisconnectFrame.tsx b/src/browser/modules/Stream/Auth/DisconnectFrame.tsx index 1035a69f27a..48414bb0ec2 100644 --- a/src/browser/modules/Stream/Auth/DisconnectFrame.tsx +++ b/src/browser/modules/Stream/Auth/DisconnectFrame.tsx @@ -24,10 +24,9 @@ import { H3 } from 'browser-components/headers' import { Lead } from 'browser-components/Text' import Render from 'browser-components/Render' -const Disconnect = ({ frame, activeConnectionData }: any) => { +const Disconnect = ({ activeConnectionData }: any) => { return ( diff --git a/src/browser/modules/Stream/Auth/ServerStatusFrame.tsx b/src/browser/modules/Stream/Auth/ServerStatusFrame.tsx index 530cfd2653d..3805bbbfc45 100644 --- a/src/browser/modules/Stream/Auth/ServerStatusFrame.tsx +++ b/src/browser/modules/Stream/Auth/ServerStatusFrame.tsx @@ -88,12 +88,7 @@ export const ServerStatusFrame = (props: any) => { } const Frame = (props: any) => { - return ( - } - /> - ) + return } /> } const mapStateToProps = (state: any) => { diff --git a/src/browser/modules/Stream/Auth/ServerSwitchFrame.tsx b/src/browser/modules/Stream/Auth/ServerSwitchFrame.tsx index a2acf4920b0..55c79c7d0bf 100644 --- a/src/browser/modules/Stream/Auth/ServerSwitchFrame.tsx +++ b/src/browser/modules/Stream/Auth/ServerSwitchFrame.tsx @@ -123,12 +123,7 @@ export const ServerSwitchFrame = (props: any) => { } const Frame = (props: any) => { - return ( - } - /> - ) + return } /> } export default Frame diff --git a/src/browser/modules/Stream/Auth/UseDbFrame.tsx b/src/browser/modules/Stream/Auth/UseDbFrame.tsx index b8fca5ac611..4584d638952 100644 --- a/src/browser/modules/Stream/Auth/UseDbFrame.tsx +++ b/src/browser/modules/Stream/Auth/UseDbFrame.tsx @@ -64,9 +64,7 @@ export const UseDbFrame = (props: any) => { } const Frame = (props: any) => { - return ( - } /> - ) + return } /> } export default Frame diff --git a/src/browser/modules/Stream/CypherFrame/CodeView.test.tsx b/src/browser/modules/Stream/CypherFrame/CodeView.test.tsx index e78b9eff6fd..9d3b8dd8c5b 100644 --- a/src/browser/modules/Stream/CypherFrame/CodeView.test.tsx +++ b/src/browser/modules/Stream/CypherFrame/CodeView.test.tsx @@ -31,7 +31,12 @@ describe('CodeViews', () => { describe('CodeView', () => { test('displays nothing if not successful query', () => { // Given - const props = { request: { status: 'error' } } + const props = { + maxFieldItems: 100, + query: '', + request: { status: 'error' }, + result: null + } // When const { container } = render() @@ -42,6 +47,7 @@ describe('CodeViews', () => { test('displays request and response info if successful query', () => { // Given const props = { + maxFieldItems: 100, query: 'MATCH xx0', request: { status: 'success', @@ -54,7 +60,8 @@ describe('CodeViews', () => { }, records: [{ res: 'xx3' }] } - } + }, + result: null } // When diff --git a/src/browser/modules/Stream/CypherFrame/CodeView.tsx b/src/browser/modules/Stream/CypherFrame/CodeView.tsx index fc4bcf49834..0685c9caa18 100644 --- a/src/browser/modules/Stream/CypherFrame/CodeView.tsx +++ b/src/browser/modules/Stream/CypherFrame/CodeView.tsx @@ -37,11 +37,20 @@ import { getMaxFieldItems } from 'shared/modules/settings/settingsDuck' import { connect } from 'react-redux' import { map, take } from 'lodash-es' import { GlobalState } from 'shared/globalState' +import { BrowserRequestResult } from 'shared/modules/requests/requestsDuck' -type ExpandableContentState = any +type ExpandableContentState = { expanded: boolean } +type ExpandableContentProps = { + title: string + content: JSX.Element + summary: string +} +class ExpandableContent extends Component< + ExpandableContentProps, + ExpandableContentState +> { + state: ExpandableContentState = { expanded: false } -class ExpandableContent extends Component { - state: any = {} render() { return ( @@ -73,11 +82,20 @@ const fieldLimiterFactory = (maxFieldItems: any) => (key: any, val: any) => { }) } -export class CodeViewComponent extends Component { - shouldComponentUpdate(props: any) { +type CodeViewOwnProps = { + result: BrowserRequestResult + request: any //we know it's BrowserRequest needs refactor + query: string +} +type CodeViewProps = CodeViewOwnProps & { + maxFieldItems: number +} +export class CodeViewComponent extends Component { + shouldComponentUpdate(props: CodeViewProps): boolean { return !this.props.result || !deepEquals(props.result, this.props.result) } - render() { + + render(): JSX.Element | null { const { request = {}, query, maxFieldItems } = this.props if (request.status !== 'success') return null const resultJson = JSON.stringify( @@ -123,9 +141,11 @@ export class CodeViewComponent extends Component { } } -export const CodeView = connect((state: GlobalState) => ({ - maxFieldItems: getMaxFieldItems(state) -}))(CodeViewComponent) +export const CodeView: React.ComponentType = connect( + (state: GlobalState) => ({ + maxFieldItems: getMaxFieldItems(state) + }) +)(CodeViewComponent) export const CodeStatusbarComponent = RelatableStatusbarComponent export const CodeStatusbar = RelatableStatusbar diff --git a/src/browser/modules/Stream/CypherFrame/index.test.tsx b/src/browser/modules/Stream/CypherFrame/CypherFrame.test.tsx similarity index 95% rename from src/browser/modules/Stream/CypherFrame/index.test.tsx rename to src/browser/modules/Stream/CypherFrame/CypherFrame.test.tsx index fe510c44343..998ad943bb9 100644 --- a/src/browser/modules/Stream/CypherFrame/index.test.tsx +++ b/src/browser/modules/Stream/CypherFrame/CypherFrame.test.tsx @@ -22,7 +22,7 @@ import React from 'react' import { render } from '@testing-library/react' import { Provider } from 'react-redux' -import { CypherFrame } from './index' +import { CypherFrame } from './CypherFrame' import { Frame } from 'shared/modules/stream/streamDuck' import { BrowserRequest, @@ -41,7 +41,10 @@ const createProps = (status: string, result: BrowserRequestResult) => ({ status, updated: Math.random(), result - } as BrowserRequest + } as BrowserRequest, + autocomplete: false, + setExportItems: () => undefined, + frameHeight: '400px' }) const withProvider = (store: any, children: any) => { return {children} diff --git a/src/browser/modules/Stream/CypherFrame/index.tsx b/src/browser/modules/Stream/CypherFrame/CypherFrame.tsx similarity index 80% rename from src/browser/modules/Stream/CypherFrame/index.tsx rename to src/browser/modules/Stream/CypherFrame/CypherFrame.tsx index ba91e53dbf0..78db176c979 100644 --- a/src/browser/modules/Stream/CypherFrame/index.tsx +++ b/src/browser/modules/Stream/CypherFrame/CypherFrame.tsx @@ -46,7 +46,7 @@ import { import { AsciiView, AsciiStatusbar } from './AsciiView' import { CodeView, CodeStatusbar } from './CodeView' import { ErrorsViewBus as ErrorsView, ErrorsStatusbar } from './ErrorsView' -import { WarningsView, WarningsStatusbar } from './WarningsView' +import { WarningsView } from './WarningsView' import { PlanView, PlanStatusbar } from './PlanView' import { VisualizationConnectedBus } from './VisualizationView' import Render from 'browser-components/Render' @@ -58,7 +58,10 @@ import { resultHasPlan, resultIsError, resultHasNodes, - initialView + initialView, + stringifyResultArray, + transformResultRecordsToResultArray, + recordToJSONMapper } from './helpers' import { SpinnerContainer, StyledStatsBarContainer } from '../styled' import { StyledFrameBody } from 'browser/modules/Frame/styled' @@ -80,6 +83,13 @@ import RelatableView, { } from 'browser/modules/Stream/CypherFrame/relatable-view' import { requestExceedsVisLimits } from 'browser/modules/Stream/CypherFrame/helpers' import { GlobalState } from 'shared/globalState' +import { csvFormat, stringModifier } from 'services/bolt/cypherTypesFormatting' +import { CSVSerializer } from 'services/serializer' +import { map } from 'lodash' +import { stringifyMod } from 'services/utils' +import { downloadPNGFromSVG, downloadSVG } from 'services/exporting/imageUtils' +import { saveAs } from 'file-saver' +import { ExportItem } from '../ExportButtons' type CypherFrameBaseProps = { frame: Frame @@ -92,19 +102,19 @@ type CypherFrameProps = CypherFrameBaseProps & { maxRows: number request: BrowserRequest onRecentViewChanged: (view: viewTypes.FrameView) => void + setExportItems: (exportItems: ExportItem[]) => void + frameHeight: string } export type CypherFrameState = { openView?: viewTypes.FrameView - fullscreen: boolean - collapse: boolean - frameHeight: number hasVis: boolean errors?: unknown _asciiMaxColWidth?: number _asciiSetColWidth?: string - _planExpand?: 'EXPAND' | 'COLLAPSE' + _planExpand: PlanExpand } +export type PlanExpand = 'EXPAND' | 'COLLAPSE' export class CypherFrame extends Component { visElement: null | { @@ -114,10 +124,8 @@ export class CypherFrame extends Component { } = null state: CypherFrameState = { openView: undefined, - fullscreen: false, - collapse: false, - frameHeight: 472, - hasVis: false + hasVis: false, + _planExpand: 'EXPAND' } changeView(view: viewTypes.FrameView): void { @@ -127,28 +135,14 @@ export class CypherFrame extends Component { } } - onResize = ( - fullscreen: boolean, - collapse: boolean, - frameHeight: number - ): void => { - if (frameHeight) { - this.setState({ fullscreen, collapse, frameHeight }) - } else { - this.setState({ fullscreen, collapse }) - } - } - shouldComponentUpdate( props: CypherFrameProps, state: CypherFrameState ): boolean { return ( this.props.request.updated !== props.request.updated || + this.props.frameHeight !== props.frameHeight || this.state.openView !== state.openView || - this.state.fullscreen !== state.fullscreen || - this.state.frameHeight !== state.frameHeight || - this.state.collapse !== state.collapse || this.state._asciiMaxColWidth !== state._asciiMaxColWidth || this.state._asciiSetColWidth !== state._asciiSetColWidth || this.state._planExpand !== state._planExpand || @@ -181,10 +175,24 @@ export class CypherFrame extends Component { }) if (view) this.setState({ openView: view }) } + + const downloadGraphics = [ + { name: 'PNG', download: this.exportPNG }, + { name: 'SVG', download: this.exportSVG } + ] + this.props.setExportItems([ + { name: 'CSV', download: this.exportCSV }, + { name: 'JSON', download: this.exportJSON }, + ...(this.visElement ? downloadGraphics : []) + ]) } componentDidMount(): void { const view = initialView(this.props, this.state) + this.props.setExportItems([ + { name: 'CSV', download: this.exportCSV }, + { name: 'JSON', download: this.exportJSON } + ]) if (view) this.setState({ openView: view }) } @@ -283,6 +291,45 @@ export class CypherFrame extends Component { ) + exportCSV = (): void => { + const records = this.getRecords() + const exportData = stringifyResultArray( + csvFormat, + transformResultRecordsToResultArray(records) + ) + const data = exportData.slice() + const csv = CSVSerializer(data.shift()) + csv.appendRows(data) + const blob = new Blob([csv.output()], { + type: 'text/plain;charset=utf-8' + }) + saveAs(blob, 'export.csv') + } + + exportJSON = (): void => { + const records = this.getRecords() + const exportData = map(records, recordToJSONMapper) + const data = stringifyMod(exportData, stringModifier, true) + const blob = new Blob([data], { + type: 'text/plain;charset=utf-8' + }) + saveAs(blob, 'records.json') + } + + exportPNG = (): void => { + if (this.visElement) { + const { svgElement, graphElement, type } = this.visElement + downloadPNGFromSVG(svgElement, graphElement, type) + } + } + + exportSVG = (): void => { + if (this.visElement) { + const { svgElement, graphElement, type } = this.visElement + downloadSVG(svgElement, graphElement, type) + } + } + getSpinner(): JSX.Element { return ( @@ -299,11 +346,7 @@ export class CypherFrame extends Component { query: string ): JSX.Element { return ( - + { - + - + - + { this.visElement = { svgElement, graphElement, type: 'plan' } this.setState({ hasVis: true }) }} + setPlanExpand={(_planExpand: PlanExpand) => + this.setState({ _planExpand }) + } /> { this.visElement = { svgElement, graphElement, type: 'graph' } this.setState({ hasVis: true }) }} - initialNodeDisplay={this.props.initialNodeDisplay} autoComplete={this.props.autoComplete} + initialNodeDisplay={this.props.initialNodeDisplay} maxNeighbours={this.props.maxNeighbours} + result={result} + updated={this.props.request.updated} /> @@ -398,35 +424,17 @@ export class CypherFrame extends Component { /> - + - - - - + + this.setState({ _planExpand }) + } /> @@ -458,20 +466,9 @@ export class CypherFrame extends Component { return ( ) } diff --git a/src/browser/modules/Stream/CypherFrame/PlanView.test.tsx b/src/browser/modules/Stream/CypherFrame/PlanView.test.tsx index 7c256673a3f..2724a076052 100644 --- a/src/browser/modules/Stream/CypherFrame/PlanView.test.tsx +++ b/src/browser/modules/Stream/CypherFrame/PlanView.test.tsx @@ -39,11 +39,14 @@ describe('PlanViews', () => { identifiers: ['n'] } } - } + }, + updated: 0, + setPlanExpand: () => undefined, + assignVisElement: () => undefined } // When - const { getByText } = render() + const { getByText } = render() // Then expect(getByText('ProduceResults')) @@ -69,7 +72,8 @@ describe('PlanViews', () => { dbHits: 'xx3' } } - } + }, + setPlanExpand: () => undefined } // When diff --git a/src/browser/modules/Stream/CypherFrame/PlanView.tsx b/src/browser/modules/Stream/CypherFrame/PlanView.tsx index 2389ce481be..b39dd50a30b 100644 --- a/src/browser/modules/Stream/CypherFrame/PlanView.tsx +++ b/src/browser/modules/Stream/CypherFrame/PlanView.tsx @@ -20,7 +20,6 @@ import React, { Component } from 'react' import { PlanSVG } from './PlanView.styled' -import { dim } from 'browser-styles/constants' import { deepEquals, shallowEquals } from 'services/utils' import bolt from 'services/bolt/bolt' import { FrameButton } from 'browser-components/buttons' @@ -33,10 +32,18 @@ import { import { StyledFrameTitlebarButtonSection } from 'browser/modules/Frame/styled' import Ellipsis from 'browser-components/Ellipsis' import queryPlan from '../../D3Visualization/lib/visualization/components/queryPlan' +import { PlanExpand } from './CypherFrame' + +type PlanViewState = { extractedPlan: any } +type PlanViewProps = { + _planExpand: PlanExpand + setPlanExpand: (p: PlanExpand) => void + result: any + updated: any + assignVisElement: (a: any, b: any) => void +} -type PlanViewState = any - -export class PlanView extends Component { +export class PlanView extends Component { el: any plan: any constructor(props: any) { @@ -48,7 +55,7 @@ export class PlanView extends Component { componentDidMount() { this.extractPlan(this.props.result) - .then(() => this.props.setParentState({ _planExpand: 'EXPAND' })) + .then(() => this.props.setPlanExpand('EXPAND')) .catch(() => {}) } @@ -70,7 +77,6 @@ export class PlanView extends Component { shouldComponentUpdate(props: any, state: PlanViewState) { if (this.props.result === undefined) return true return ( - props.fullscreen !== this.props.fullscreen || !deepEquals(props.result.summary, this.props.result.summary) || !shallowEquals(state, this.state) || props._planExpand !== this.props._planExpand @@ -79,10 +85,9 @@ export class PlanView extends Component { extractPlan(result: any) { if (result === undefined) return Promise.reject(new Error('No result')) - return new Promise(resolve => { + return new Promise(resolve => { const extractedPlan = bolt.extractPlan(result) - if (extractedPlan) - return this.setState({ extractedPlan }, resolve() as any) + if (extractedPlan) return this.setState({ extractedPlan }, resolve) resolve() }) } @@ -136,25 +141,22 @@ export class PlanView extends Component { render() { if (!this.state.extractedPlan) return null - return ( - - ) + return } } -type PlanStatusbarState = any +type PlanStatusbarState = { extractedPlan: any } +type PlanStatusbarProps = { + result: any + + setPlanExpand: (p: PlanExpand) => void +} -export class PlanStatusbar extends Component { - state = { +export class PlanStatusbar extends Component< + PlanStatusbarProps, + PlanStatusbarState +> { + state: PlanStatusbarState = { extractedPlan: null } @@ -181,7 +183,7 @@ export class PlanStatusbar extends Component { } render() { - const plan: any = this.state.extractedPlan + const plan = this.state.extractedPlan if (!plan) return null const { result = {} } = this.props return ( @@ -203,17 +205,13 @@ export class PlanStatusbar extends Component { - this.props.setParentState({ _planExpand: 'COLLAPSE' }) - } + onClick={() => this.props.setPlanExpand('COLLAPSE')} > - this.props.setParentState({ _planExpand: 'EXPAND' }) - } + onClick={() => this.props.setPlanExpand('EXPAND')} > diff --git a/src/browser/modules/Stream/CypherFrame/VisualizationView.styled.tsx b/src/browser/modules/Stream/CypherFrame/VisualizationView.styled.tsx index b9a3badbdb4..22a9def86f1 100644 --- a/src/browser/modules/Stream/CypherFrame/VisualizationView.styled.tsx +++ b/src/browser/modules/Stream/CypherFrame/VisualizationView.styled.tsx @@ -19,16 +19,11 @@ */ import styled from 'styled-components' -import { dim } from 'browser-styles/constants' -export const StyledVisContainer: any = styled.div` +export const StyledVisContainer = styled.div<{ height: string }>` width: 100%; overflow: hidden; - ${(props: any) => (props.fullscreen ? 'padding-bottom: 39px' : null)}; - height: ${(props: any) => - props.fullscreen - ? '100vh' - : dim.frameBodyHeight - dim.frameTitlebarHeight * 2 + 'px'}; + height: ${props => props.height}; > svg { width: 100%; } diff --git a/src/browser/modules/Stream/CypherFrame/VisualizationView.test.tsx b/src/browser/modules/Stream/CypherFrame/VisualizationView.test.tsx index d2e7ea1e8a1..69e6a918198 100644 --- a/src/browser/modules/Stream/CypherFrame/VisualizationView.test.tsx +++ b/src/browser/modules/Stream/CypherFrame/VisualizationView.test.tsx @@ -22,6 +22,7 @@ import React from 'react' import { render } from '@testing-library/react' import neo4j from 'neo4j-driver' import { Visualization } from './VisualizationView' +import { createBus } from 'suber' const mockEmptyResult = { records: [] @@ -30,16 +31,29 @@ const node = new (neo4j.types.Node as any)('1', ['Person'], { prop1: 'String with HTML in it' }) const mockResult = { - records: [{ keys: ['0'], __fields: [node], get: (_key: any) => node }] + records: [{ keys: ['0'], __fields: [node], get: () => node }] } +const props = { + assignVisElement: () => undefined, + autoComplete: false, + bus: createBus(), + frameHeight: '300px', + graphStyleData: null, + initialNodeDisplay: 10, + maxFieldItems: 100, + maxNeighbours: 100, + updateStyle: () => undefined, + updated: 0 +} test('Visualization renders', () => { - const { container } = render() + const { container } = render( + + ) expect(container).toMatchSnapshot() }) + test('Visualization renders with result and escapes any HTML', () => { - const { container } = render( - {}} autoComplete result={mockResult} /> - ) + const { container } = render() expect(container).toMatchSnapshot() }) diff --git a/src/browser/modules/Stream/CypherFrame/VisualizationView.tsx b/src/browser/modules/Stream/CypherFrame/VisualizationView.tsx index 2f1fb5ca477..9c46c232e63 100644 --- a/src/browser/modules/Stream/CypherFrame/VisualizationView.tsx +++ b/src/browser/modules/Stream/CypherFrame/VisualizationView.tsx @@ -32,15 +32,41 @@ import { CYPHER_REQUEST } from 'shared/modules/cypher/cypherDuck' import { NEO4J_BROWSER_USER_ACTION_QUERY } from 'services/bolt/txMetadata' import { getMaxFieldItems } from 'shared/modules/settings/settingsDuck' import { resultHasTruncatedFields } from 'browser/modules/Stream/CypherFrame/helpers' +import { Bus } from 'suber' -type VisualizationState = any +type VisualizationState = { + updated: number + nodes: any[] + relationships: any[] + hasTruncatedFields: boolean +} + +type VisualizationProps = { + result: any + graphStyleData: any + frameHeight: string + updated: number + autoComplete: boolean + maxNeighbours: number + bus: Bus + maxFieldItems: number + initialNodeDisplay: number + updateStyle: (style: any) => void + assignVisElement: (v: any) => void +} -export class Visualization extends Component { +export class Visualization extends Component< + VisualizationProps, + VisualizationState +> { autoCompleteCallback: any graph: any - state: any = { + + state: VisualizationState = { nodes: [], - relationships: [] + relationships: [], + updated: 0, + hasTruncatedFields: false } componentDidMount() { @@ -186,7 +212,7 @@ export class Visualization extends Component { if (!this.state.nodes.length) return null return ( - + { getNeighbours={this.getNeighbours.bind(this)} nodes={this.state.nodes} relationships={this.state.relationships} - fullscreen={this.props.fullscreen} frameHeight={this.props.frameHeight} assignVisElement={this.props.assignVisElement} getAutoCompleteCallback={(callback: any) => { diff --git a/src/browser/modules/Stream/CypherFrame/WarningsView.tsx b/src/browser/modules/Stream/CypherFrame/WarningsView.tsx index a9a56e42302..f64c6b45c7e 100644 --- a/src/browser/modules/Stream/CypherFrame/WarningsView.tsx +++ b/src/browser/modules/Stream/CypherFrame/WarningsView.tsx @@ -88,13 +88,3 @@ export class WarningsView extends Component { return {notificationsList} } } - -export class WarningsStatusbar extends Component { - shouldComponentUpdate() { - return false - } - - render() { - return null - } -} diff --git a/src/browser/modules/Stream/CypherFrame/__snapshots__/VisualizationView.test.tsx.snap b/src/browser/modules/Stream/CypherFrame/__snapshots__/VisualizationView.test.tsx.snap index 2ef4d565da8..d041683ab26 100644 --- a/src/browser/modules/Stream/CypherFrame/__snapshots__/VisualizationView.test.tsx.snap +++ b/src/browser/modules/Stream/CypherFrame/__snapshots__/VisualizationView.test.tsx.snap @@ -5,7 +5,8 @@ exports[`Visualization renders 1`] = `
`; exports[`Visualization renders with result and escapes any HTML 1`] = `
{ +export const resultHasTruncatedFields = ( + result: any, + maxFieldItems: any +): boolean => { if (!maxFieldItems || !result) { return false } diff --git a/src/browser/modules/Stream/CypherScriptFrame/CypherScriptFrame.tsx b/src/browser/modules/Stream/CypherScriptFrame/CypherScriptFrame.tsx index e53cd841668..6de349550c4 100644 --- a/src/browser/modules/Stream/CypherScriptFrame/CypherScriptFrame.tsx +++ b/src/browser/modules/Stream/CypherScriptFrame/CypherScriptFrame.tsx @@ -102,9 +102,7 @@ function CypherScriptFrame({ ) - return ( - - ) + return } const mapStateToProps = (state: any, ownProps: BaseFrameProps) => { diff --git a/src/browser/modules/Stream/EditFrame.test.tsx b/src/browser/modules/Stream/EditFrame.test.tsx deleted file mode 100644 index 5d3a4ef4988..00000000000 --- a/src/browser/modules/Stream/EditFrame.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Neo4j is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React from 'react' -import { render } from '@testing-library/react' -import { Provider } from 'react-redux' -import configureStore from 'redux-mock-store' - -import EditFrame from './EditFrame' -import { Frame } from 'shared/modules/stream/streamDuck' - -describe('EditFrame', () => { - it('creates a monaco instance with a unique id based on frame id', () => { - const id = 'some-frame-id' - const cmd = ':edit' - const frame = { id, cmd } as Frame - const { container } = render( - - - - ) - expect(container.querySelector(`#monaco-${id}`)).toBeDefined() - }) -}) diff --git a/src/browser/modules/Stream/EditFrame.tsx b/src/browser/modules/Stream/EditFrame.tsx deleted file mode 100644 index ed586fcfe95..00000000000 --- a/src/browser/modules/Stream/EditFrame.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Neo4j is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import React, { Dispatch, useState } from 'react' -import { connect } from 'react-redux' -import { withBus } from 'react-suber' -import { Action } from 'redux' -import styled from 'styled-components' -import { Bus } from 'suber' - -import Monaco from '../Editor/Monaco' -import FrameTemplate from '../Frame/FrameTemplate' -import { StyledFrameBody, StyledFrameContents } from '../Frame/styled' -import { GlobalState } from 'shared/globalState' -import { - commandSources, - executeCommand -} from 'shared/modules/commands/commandsDuck' -import { - codeFontLigatures, - shouldEnableMultiStatementMode -} from 'shared/modules/settings/settingsDuck' -import { Frame } from 'shared/modules/stream/streamDuck' - -interface EditFrameProps { - bus: Bus - codeFontLigatures: boolean - enableMultiStatementMode: boolean - frame: Frame - runQuery: (query: string) => void -} - -const ForceFullSizeFrameContent = styled.div` - ${StyledFrameBody} { - padding: 0; - overflow: unset; - } - ${StyledFrameContents} { - overflow: unset; - } -` - -const EditFrame = (props: EditFrameProps): JSX.Element => { - const [text, setText] = useState(props.frame.query) - - return ( - - { - /* don't allow fullscreening */ - }} - /> - } - header={props.frame} - runQuery={() => { - props.runQuery(text) - }} - /> - - ) -} - -const mapStateToProps = (state: GlobalState) => ({ - enableMultiStatementMode: shouldEnableMultiStatementMode(state), - codeFontLigatures: codeFontLigatures(state) -}) - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - runQuery(query: string) { - dispatch(executeCommand(query, { source: commandSources.playButton })) - } -}) - -export default withBus(connect(mapStateToProps, mapDispatchToProps)(EditFrame)) diff --git a/src/browser/modules/Stream/ErrorFrame.tsx b/src/browser/modules/Stream/ErrorFrame.tsx index 45a9b040fb0..5b161aa5e50 100644 --- a/src/browser/modules/Stream/ErrorFrame.tsx +++ b/src/browser/modules/Stream/ErrorFrame.tsx @@ -73,6 +73,6 @@ export const ErrorView = ({ frame }: any) => { } const ErrorFrame = ({ frame }: any) => { - return } /> + return } /> } export default ErrorFrame diff --git a/src/browser/modules/Stream/ExportButtons.tsx b/src/browser/modules/Stream/ExportButtons.tsx new file mode 100644 index 00000000000..31fc7f888fc --- /dev/null +++ b/src/browser/modules/Stream/ExportButtons.tsx @@ -0,0 +1,76 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import { DownloadIcon } from 'browser-components/icons/Icons' +import React from 'react' +import { DropdownItem } from 'semantic-ui-react' +import { Frame } from 'shared/modules/stream/streamDuck' +import { + DropdownButton, + DropdownContent, + DropDownItemDivider, + DropdownList +} from './styled' + +type ExportButtonProps = { + frame: Frame + isRelateAvailable: boolean + newProjectFile: (name: string) => void + exportItems?: ExportItem[] +} + +export type ExportItem = { name: string; download: () => void } + +export default function ExportButton({ + frame, + isRelateAvailable, + newProjectFile, + exportItems = [] +}: ExportButtonProps): JSX.Element | null { + const canExport: boolean = exportItems.length > 0 || isRelateAvailable + + return canExport ? ( + + + + + {isRelateAvailable && ( + <> + newProjectFile(frame.cmd)}> + Save as project file + + + + )} + + {exportItems.map(({ name, download }) => ( + + Export {name} + + ))} + + + + ) : null +} diff --git a/src/browser/modules/Stream/Extras/SnakeFrame/Snake.tsx b/src/browser/modules/Stream/Extras/SnakeFrame/Snake.tsx index 4d04603d751..72e1f3bbcd9 100644 --- a/src/browser/modules/Stream/Extras/SnakeFrame/Snake.tsx +++ b/src/browser/modules/Stream/Extras/SnakeFrame/Snake.tsx @@ -34,7 +34,7 @@ import { maxSpeed } from './helpers' -const SnakeCanvas: any = styled.canvas` +const SnakeCanvas = styled.canvas` border: 1px solid #787878; &:focus { outline: none; @@ -228,7 +228,7 @@ class SnakeFrame extends React.Component { render() { return ( ` margin: 30px auto; - width: ${props => (props as any).width}px; - height: ${props => (props as any).height + 50}px; + width: ${props => props.width}px; + height: ${props => props.height + 50}px; ` -const SplashScreen = styled(GameDiv)` - background-color: ${props => (props as any).backgroundColor}; +const SplashScreen = styled(GameDiv)<{ backgroundColor: string }>` + background-color: ${props => props.backgroundColor}; ` const SplashContents = styled.div` @@ -56,8 +56,10 @@ const SplashContents = styled.div` } ` -export const InitialStartButton: any = styled(FormButton)` - background-color: ${(props: any) => props.backgroundColor}; +export const InitialStartButton: any = styled(FormButton)<{ + backgroundColor: string +}>` + background-color: ${props => props.backgroundColor}; color: #ffffff; ` @@ -133,8 +135,6 @@ export class SnakeFrame extends React.Component<{}, SnakeFrameState> { } const Frame = (props: any) => { - return ( - } /> - ) + return } /> } export default Frame diff --git a/src/browser/modules/Stream/HelpFrame.tsx b/src/browser/modules/Stream/HelpFrame.tsx index 54b80fafcf4..8627676fefd 100644 --- a/src/browser/modules/Stream/HelpFrame.tsx +++ b/src/browser/modules/Stream/HelpFrame.tsx @@ -73,14 +73,7 @@ const HelpFrame = ({ stack = [] }: any) => { ) : ( main ) - return ( - - ) + return } function generateContent(frame: any) { diff --git a/src/browser/modules/Stream/HistoryFrame.tsx b/src/browser/modules/Stream/HistoryFrame.tsx index a1ace1afbc2..8b1c550d46a 100644 --- a/src/browser/modules/Stream/HistoryFrame.tsx +++ b/src/browser/modules/Stream/HistoryFrame.tsx @@ -17,18 +17,45 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import React from 'react' +import React, { useEffect } from 'react' import { withBus } from 'react-suber' import * as editor from 'shared/modules/editor/editorDuck' import FrameTemplate from '../Frame/FrameTemplate' import { UnstyledList, PaddedDiv } from './styled' import HistoryRow from './HistoryRow' +import { saveAs } from 'file-saver' export const HistoryFrame = (props: any) => { - const { frame, bus } = props + const { frame, bus, setExportItems } = props const onHistoryClick = (cmd: any) => { bus.send(editor.SET_CONTENT, editor.setContent(cmd)) } + useEffect(() => { + setExportItems([ + { + name: 'history', + download: () => { + const txt = frame.result + .map((line: string) => { + const trimmedLine = `${line}`.trim() + + if (trimmedLine.startsWith(':')) { + return trimmedLine + } + + return trimmedLine.endsWith(';') ? trimmedLine : `${trimmedLine};` + }) + .join('\n\n') + const blob = new Blob([txt], { + type: 'text/plain;charset=utf-8' + }) + + saveAs(blob, 'history.txt') + } + } + ]) + }, [setExportItems, frame.result]) + const historyRows = frame.result.length > 0 ? ( frame.result.map((entry: any, index: any) => { @@ -45,12 +72,7 @@ export const HistoryFrame = (props: any) => { Empty history ) - return ( - {historyRows}} - /> - ) + return {historyRows}} /> } export default withBus(HistoryFrame) diff --git a/src/browser/modules/Stream/ParamsFrame.tsx b/src/browser/modules/Stream/ParamsFrame.tsx index 914b084d458..611e7a18c61 100644 --- a/src/browser/modules/Stream/ParamsFrame.tsx +++ b/src/browser/modules/Stream/ParamsFrame.tsx @@ -58,8 +58,6 @@ const ParamsFrame = ({ frame }: any) => { ) - return ( - - ) + return } export default ParamsFrame diff --git a/src/browser/modules/Stream/PlayFrame.tsx b/src/browser/modules/Stream/PlayFrame.tsx index 49985ae9387..090b92f63fd 100644 --- a/src/browser/modules/Stream/PlayFrame.tsx +++ b/src/browser/modules/Stream/PlayFrame.tsx @@ -157,14 +157,6 @@ export function PlayFrame({ stack, bus, showPromotion }: any): JSX.Element { ) - const classNames = ['playFrame'] - if (hasCarousel || stack.length > 1) { - classNames.push('has-carousel') - } - if (isRemote) { - classNames.push('is-remote') - } - let guideAndNav = guide if (stack.length > 1) { guideAndNav = ( @@ -175,14 +167,7 @@ export function PlayFrame({ stack, bus, showPromotion }: any): JSX.Element { ) } - return ( - - ) + return } function generateContent( diff --git a/src/browser/modules/Stream/PreFrame.tsx b/src/browser/modules/Stream/PreFrame.tsx index 3d43102932f..f599d5db309 100644 --- a/src/browser/modules/Stream/PreFrame.tsx +++ b/src/browser/modules/Stream/PreFrame.tsx @@ -24,7 +24,6 @@ import { PaddedDiv } from './styled' const PreFrame = ({ frame }: any) => { return (
{frame.result || frame.contents}
diff --git a/src/browser/modules/Stream/Queries/QueriesFrame.tsx b/src/browser/modules/Stream/Queries/QueriesFrame.tsx index 24ceb84ce1f..1aa63d3942b 100644 --- a/src/browser/modules/Stream/Queries/QueriesFrame.tsx +++ b/src/browser/modules/Stream/Queries/QueriesFrame.tsx @@ -277,7 +277,7 @@ export class QueriesFrame extends Component { const errorRows = errors.map((error: any, i: any) => ( - + Error connecting to: {error.host} @@ -356,7 +356,6 @@ export class QueriesFrame extends Component { } return ( ` text-align: left; height: 30px; vertical-align: top; padding: 5px; - width: ${props => (props as any).width || 'auto'}; + width: ${props => props.width || 'auto'}; ` -export const StyledTd: any = styled.td` +export const StyledTd = styled.td<{ width?: string | number }>` padding: 5px; width: ${props => props.width || 'auto'}; text-overflow: ellipsis; diff --git a/src/browser/modules/Stream/SchemaFrame.tsx b/src/browser/modules/Stream/SchemaFrame.tsx index b39c0505765..cd03740c553 100644 --- a/src/browser/modules/Stream/SchemaFrame.tsx +++ b/src/browser/modules/Stream/SchemaFrame.tsx @@ -230,9 +230,7 @@ export class SchemaFrame extends Component { } const Frame = (props: any) => { - return ( - } /> - ) + return } /> } const mapStateToProps = (state: any) => ({ diff --git a/src/browser/modules/Stream/Stream.tsx b/src/browser/modules/Stream/Stream.tsx index c25239067b5..4eefb7d25c4 100644 --- a/src/browser/modules/Stream/Stream.tsx +++ b/src/browser/modules/Stream/Stream.tsx @@ -19,9 +19,9 @@ */ import { connect } from 'react-redux' -import React, { memo, useRef, useEffect } from 'react' +import React, { memo, useRef, useEffect, useState } from 'react' import { StyledStream, Padding, AnimationContainer } from './styled' -import CypherFrame from './CypherFrame/index' +import CypherFrame from './CypherFrame/CypherFrame' import HistoryFrame from './HistoryFrame' import PlayFrame from './PlayFrame' import DefaultFrame from '../Frame/DefaultFrame' @@ -50,9 +50,13 @@ import { } from 'shared/modules/connections/connectionsDuck' import { getScrollToTop } from 'shared/modules/settings/settingsDuck' import DbsFrame from './Auth/DbsFrame' -import EditFrame from './EditFrame' +import { StyledFrame } from '../Frame/styled' +import FrameTitlebar from '../Frame/FrameTitlebar' +import { dim } from 'browser-styles/constants' +import styled from 'styled-components' +import ExportButton, { ExportItem } from './ExportButtons' -const trans = { +const nameToFrame: Record> = { error: ErrorFrame, cypher: CypherFrame, 'cypher-script': CypherScriptFrame, @@ -78,14 +82,21 @@ const trans = { 'reset-db': UseDbFrame, dbs: DbsFrame, style: StyleFrame, - edit: EditFrame, default: DefaultFrame } -type FrameType = keyof typeof trans +const getFrameComponent = (frameData: FrameStack): React.ComponentType => { + const { cmd, type } = frameData.stack[0] + let MyFrame = nameToFrame[type] || nameToFrame.default -const getFrame = (type: FrameType) => { - return trans[type] || trans.default + if (type === 'error') { + try { + const command = cmd.replace(/^:/, '') + const Frame = command[0].toUpperCase() + command.slice(1) + 'Frame' + MyFrame = require('./Extras/index')[Frame] || nameToFrame['error'] + } catch (e) {} + } + return MyFrame } type StreamProps = { @@ -95,9 +106,10 @@ type StreamProps = { } export interface BaseFrameProps { - frame: Frame & { isPinned: boolean } + frame: Frame activeConnectionData: Connection | null stack: Frame[] + setExportItems: (exportItems: ExportItem[]) => void } function Stream(props: StreamProps): JSX.Element { @@ -119,37 +131,119 @@ function Stream(props: StreamProps): JSX.Element { return ( - {props.frames.map(frameObject => { - const frame = frameObject.stack[0] + {props.frames.map(frameObject => ( + + + + ))} + + + ) +} - const frameProps: BaseFrameProps = { - frame: { ...frame, isPinned: frameObject.isPinned }, - activeConnectionData: props.activeConnectionData, - stack: frameObject.stack - } +type FrameContainerProps = { + frameData: FrameStack + activeConnectionData: Connection | null +} - let MyFrame = getFrame(frame.type as FrameType) - if (frame.type === 'error') { - try { - const cmd = frame.cmd.replace(/^:/, '') - const Frame = cmd[0].toUpperCase() + cmd.slice(1) + 'Frame' - MyFrame = require('./Extras/index')[Frame] - if (!MyFrame) { - MyFrame = getFrame(frame.type) - } - } catch (e) {} +function FrameContainer(props: FrameContainerProps) { + const { + isFullscreen, + toggleFullscreen, + isCollapsed, + toggleCollapse + } = useSizeToggles() + const frame = props.frameData.stack[0] + const [exportItems, setExportItems] = useState([]) + const frameProps: BaseFrameProps = { + frame, + activeConnectionData: props.activeConnectionData, + stack: props.frameData.stack, + setExportItems: a => { + console.log(a) + setExportItems(a) + } + } + const FrameComponent = getFrameComponent(props.frameData) + + return ( + + undefined} + exportItems={exportItems} + /> } - return ( - - - - ) - })} - - + /> + + + {/* side effect of controlling cypher fram height like this. we can + do implement dragable sizing of frames */} + + ) } +const ContentContainer = styled.div<{ + isCollapsed: boolean + isFullscreen: boolean +}>` + overflow: auto; + display: flex; + flex-direction: row; + width: 100%; + max-height: ${props => { + if (props.isCollapsed) { + return 0 + } + if (props.isFullscreen) { + return '100%' + } + return dim.frameBodyHeight - dim.frameStatusbarHeight + 1 + 'px' + }}; +` + +function useSizeToggles() { + const [isCollapsed, setCollapsed] = useState(false) + const [isFullscreen, setFullscreen] = useState(false) + + function toggleCollapse() { + setCollapsed(coll => !coll) + setFullscreen(false) + } + + function toggleFullscreen() { + setFullscreen(full => !full) + setCollapsed(false) + } + + return { + isCollapsed, + isFullscreen, + toggleCollapse, + toggleFullscreen + } +} + const mapStateToProps = (state: GlobalState) => ({ frames: getFrames(state), activeConnectionData: getActiveConnectionData(state), diff --git a/src/browser/modules/Stream/StyleFrame.tsx b/src/browser/modules/Stream/StyleFrame.tsx index 27d02dcd9d3..eeaeba68df9 100644 --- a/src/browser/modules/Stream/StyleFrame.tsx +++ b/src/browser/modules/Stream/StyleFrame.tsx @@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import React from 'react' +import React, { useEffect } from 'react' import { connect } from 'react-redux' import FrameTemplate from '../Frame/FrameTemplate' import { PaddedDiv, StyledOneRowStatsBar, StyledRightPartial } from './styled' @@ -31,9 +31,11 @@ import { } from 'shared/modules/commands/commandsDuck' import { FireExtinguisherIcon } from 'browser-components/icons/Icons' import { InfoView } from './InfoView' +import { saveAs } from 'file-saver' +import { BaseFrameProps } from './Stream' -const StyleFrame = ({ frame }: any) => { - let grass: string | false = '' +const StyleFrame = ({ frame, setExportItems }: BaseFrameProps): JSX.Element => { + let grass = '' let contents = ( { /> ) if (frame.result) { - grass = objToCss(frame.result) + grass = objToCss(frame.result) || '' contents = (
@@ -52,11 +54,23 @@ const StyleFrame = ({ frame }: any) => {
       
     )
   }
+
+  useEffect(() => {
+    setExportItems([
+      {
+        name: 'GraSS',
+        download: () => {
+          const blob = new Blob([grass], {
+            type: 'text/plain;charset=utf-8'
+          })
+          saveAs(blob, 'style.grass')
+        }
+      }
+    ])
+  }, [setExportItems, grass])
+
   return (
      grass}
       contents={contents}
       statusbar={}
     />
diff --git a/src/browser/modules/Stream/SysInfoFrame/index.tsx b/src/browser/modules/Stream/SysInfoFrame/index.tsx
index 3dfd0927a01..e79603d6254 100644
--- a/src/browser/modules/Stream/SysInfoFrame/index.tsx
+++ b/src/browser/modules/Stream/SysInfoFrame/index.tsx
@@ -145,7 +145,6 @@ export class SysInfoFrame extends Component {
 
     return (
       
diff --git a/src/browser/modules/Stream/__snapshots__/SchemaFrame.test.tsx.snap b/src/browser/modules/Stream/__snapshots__/SchemaFrame.test.tsx.snap
index e7c725018de..a2b4f418f87 100644
--- a/src/browser/modules/Stream/__snapshots__/SchemaFrame.test.tsx.snap
+++ b/src/browser/modules/Stream/__snapshots__/SchemaFrame.test.tsx.snap
@@ -10,13 +10,13 @@ exports[`SchemaFrame renders empty 1`] = `
         class="sc-ibxdXY lnvWNA"
       >
         
@@ -24,10 +24,10 @@ exports[`SchemaFrame renders empty 1`] = `
           
@@ -35,13 +35,13 @@ exports[`SchemaFrame renders empty 1`] = `
           
Indexes
None
@@ -49,10 +49,10 @@ exports[`SchemaFrame renders empty 1`] = ` @@ -92,43 +92,43 @@ exports[`SchemaFrame renders empty for Neo4j >= 4.0 1`] = ` class="sc-ibxdXY lnvWNA" >
Constraints
None
@@ -136,42 +136,42 @@ exports[`SchemaFrame renders empty for Neo4j >= 4.0 1`] = `
Index Name Type Uniqueness EntityType LabelsOrTypes Properties State
None
@@ -179,10 +179,10 @@ exports[`SchemaFrame renders empty for Neo4j >= 4.0 1`] = ` @@ -222,13 +222,13 @@ exports[`SchemaFrame renders results for Neo4j < 4.0 1`] = ` class="sc-ibxdXY lnvWNA" >
Constraints
None
@@ -236,10 +236,10 @@ exports[`SchemaFrame renders results for Neo4j < 4.0 1`] = ` @@ -247,13 +247,13 @@ exports[`SchemaFrame renders results for Neo4j < 4.0 1`] = `
Indexes
ON :Movie(released) ONLINE
@@ -261,10 +261,10 @@ exports[`SchemaFrame renders results for Neo4j < 4.0 1`] = ` diff --git a/src/browser/modules/User/UserAdd.tsx b/src/browser/modules/User/UserAdd.tsx index eaa9a71d2a2..124da4b19f3 100644 --- a/src/browser/modules/User/UserAdd.tsx +++ b/src/browser/modules/User/UserAdd.tsx @@ -420,7 +420,6 @@ export class UserAdd extends Component { return ( { : 'No users' frameContents = <>{renderedListOfUsers} } - return ( - - ) + return } } const mapStateToProps = (state: any) => { diff --git a/src/shared/modules/stream/streamDuck.ts b/src/shared/modules/stream/streamDuck.ts index 1934c01e3c8..7217d778dbf 100644 --- a/src/shared/modules/stream/streamDuck.ts +++ b/src/shared/modules/stream/streamDuck.ts @@ -37,6 +37,7 @@ export const FRAME_TYPE_FILTER_UPDATED = 'frames/FRAME_TYPE_FILTER_UPDATED' export const PIN = 'frames/PIN' export const UNPIN = 'frames/UNPIN' export const SET_RECENT_VIEW = 'frames/SET_RECENT_VIEW' +export const UPDATE_FRAME = 'frames/UPDATE_FRAME' export const SET_MAX_FRAMES = 'frames/SET_MAX_FRAMES' export const TRACK_SAVE_AS_PROJECT_FILE = 'frames/TRACK_SAVE_AS_PROJECT_FILE' export const TRACK_FULLSCREEN_TOGGLE = 'frames/TRACK_FULLSCREEN_TOGGLE' @@ -239,6 +240,8 @@ export interface FrameStack { export interface FramesState { allIds: string[] + // note on ids in this file. All Frame:s in a FrameStack.stack share the same id. + // That id is used as a key here in FramesState.byId. Yes this is confusing. byId: { [key: string]: FrameStack } recentView: null | FrameView maxFrames: number
Constraints
ON ( book:Book ) ASSERT book.isbn IS UNIQUE