diff --git a/src/browser/components/Directives.tsx b/src/browser/components/Directives.tsx index 41e0651ae69..e4d714ff2f8 100644 --- a/src/browser/components/Directives.tsx +++ b/src/browser/components/Directives.tsx @@ -28,6 +28,20 @@ import * as editor from 'shared/modules/editor/editorDuck' import { addClass, prependIcon } from 'shared/services/dom-helpers' const directives = [ + { + selector: '[data-exec]', + valueExtractor: (elem: any) => { + return `${elem.getAttribute('data-exec')}` + }, + autoExec: true + }, + { + selector: '[data-populate]', + valueExtractor: (elem: any) => { + return `${elem.getAttribute('data-populate')}` + }, + autoExec: false + }, { selector: '[exec-topic]', valueExtractor: (elem: any) => { @@ -99,15 +113,20 @@ const bindDynamicInputToDom = (element: any) => { } export const Directives = (props: any) => { - const callback = (elem: any) => { + const callback = (elem: HTMLDivElement | null) => { if (elem) { directives.forEach(directive => { const elems = elem.querySelectorAll(directive.selector) - Array.from(elems).forEach((e: any) => { - if (e.firstChild.nodeName !== 'I') { + Array.from(elems).forEach(e => { + if ( + e.firstChild?.nodeName !== 'I' && + !e.classList.contains('remove-play-icon') + ) { prependPlayIcon(e) } + // If we use add event listener we need to remove it afterwards + // @ts-expect-error e.onclick = () => { addClass(e, 'clicked') return props.onItemClick( diff --git a/src/browser/components/ManualLink.test.tsx b/src/browser/components/ManualLink.test.tsx index 7cf16e2b66b..2a10a8c6982 100644 --- a/src/browser/components/ManualLink.test.tsx +++ b/src/browser/components/ManualLink.test.tsx @@ -20,8 +20,7 @@ import React from 'react' import { render } from '@testing-library/react' - -import { ManualLink } from './ManualLink' +import { ManualLink } from 'browser-components/ManualLink' const tests: [Record, string][] = [ [ diff --git a/src/browser/components/ManualLink.tsx b/src/browser/components/ManualLink.tsx index e15ce89ba31..40cfd4e0daf 100644 --- a/src/browser/components/ManualLink.tsx +++ b/src/browser/components/ManualLink.tsx @@ -24,6 +24,7 @@ import semver from 'semver' import { getVersion } from 'shared/modules/dbMeta/dbMetaDuck' import { formatDocVersion } from 'browser/modules/Sidebar/Documents' +import { DrawerExternalLink } from './drawer/drawer-styled' const movedPages: { [key: string]: { oldPage: string; oldContent: string } } = { '/administration/indexes-for-search-performance/': { @@ -52,7 +53,7 @@ export function ManualLink({ children, neo4jVersion, minVersion -}: any) { +}: any): JSX.Element { let cleanPage = page.replace(/^\//, '') let content = children if (isPageMoved(chapter, page, neo4jVersion)) { @@ -70,15 +71,11 @@ export function ManualLink({ const url = `https://neo4j.com/docs/${chapter}/${version}/${cleanPage}` - return ( - - {content} - - ) + return {content} } const mapStateToProps = (state: any) => ({ neo4jVersion: getVersion(state) }) -export default connect(mapStateToProps, null)(ManualLink) +export default connect(mapStateToProps)(ManualLink) diff --git a/src/browser/components/SavedScripts/SavedScriptsFolder.tsx b/src/browser/components/SavedScripts/SavedScriptsFolder.tsx index 4fd7930f9c5..46bea3267c7 100644 --- a/src/browser/components/SavedScripts/SavedScriptsFolder.tsx +++ b/src/browser/components/SavedScripts/SavedScriptsFolder.tsx @@ -51,7 +51,7 @@ interface SavedScriptsFolderProps { forceEdit: boolean onDoneEditing: () => void selectedScriptIds: string[] - children: JSX.Element[] + children: React.ReactNode } function SavedScriptsFolder({ diff --git a/src/browser/components/TabNavigation/Navigation.tsx b/src/browser/components/TabNavigation/Navigation.tsx index f330a12a28f..4443cc09809 100644 --- a/src/browser/components/TabNavigation/Navigation.tsx +++ b/src/browser/components/TabNavigation/Navigation.tsx @@ -32,11 +32,14 @@ import { StyledTopNav, StyledBottomNav } from './styled' +import { GUIDE_DRAWER_ID } from 'shared/modules/sidebar/sidebarDuck' const Closing = 'CLOSING' const Closed = 'CLOSED' const Open = 'OPEN' const Opening = 'OPENING' +export const LARGE_DRAWER_WIDTH = 500 +export const STANDARD_DRAWER_WIDTH = 300 export interface NavItem { name: string @@ -169,6 +172,14 @@ class Navigation extends Component { this.state.drawerContent ) + const drawerWidth = + this.props.openDrawer === GUIDE_DRAWER_ID + ? LARGE_DRAWER_WIDTH + : STANDARD_DRAWER_WIDTH + const useFullWidth = + this.state.transitionState === Open || + this.state.transitionState === Opening + const width = useFullWidth ? drawerWidth : 0 return ( @@ -176,10 +187,7 @@ class Navigation extends Component { {bottomNavItemsList} { if (ref) { // Remove old listeners so we don't get multiple callbacks. diff --git a/src/browser/components/TabNavigation/styled.tsx b/src/browser/components/TabNavigation/styled.tsx index 3b82cd6b3de..915b1876adf 100644 --- a/src/browser/components/TabNavigation/styled.tsx +++ b/src/browser/components/TabNavigation/styled.tsx @@ -28,12 +28,12 @@ export const StyledSidebar = styled.div` color: #fff; ` -export const StyledDrawer: any = styled.div` +export const StyledDrawer = styled.div<{ width: number }>` flex: 0 0 auto; background-color: #31333b; overflow-x: hidden; overflow-y: auto; - width: ${(props: any) => (props.open ? '300px' : '0px')}; + width: ${props => props.width}px; transition: 0.2s ease-out; z-index: 1; ` diff --git a/src/browser/components/drawer/Drawer.tsx b/src/browser/components/drawer/drawer-styled.tsx similarity index 72% rename from src/browser/components/drawer/Drawer.tsx rename to src/browser/components/drawer/drawer-styled.tsx index 2cb306ffc62..b838b932add 100644 --- a/src/browser/components/drawer/Drawer.tsx +++ b/src/browser/components/drawer/drawer-styled.tsx @@ -19,6 +19,7 @@ */ import styled from 'styled-components' +import linkIcon from 'icons/external-link.svg' export const Drawer = styled.div` width: 290px; @@ -43,6 +44,9 @@ export const DrawerHeader = styled.h4` export const DrawerToppedHeader = styled(DrawerHeader)` padding-top: 8px; ` +export const DrawerSeparator = styled.div` + border-bottom: 1px solid #424650; +` export const DrawerSubHeader = styled.h5` color: ${props => props.theme.primaryHeaderText}; @@ -76,3 +80,41 @@ export const DrawerFooter = styled.div` margin-bottom: 20px; text-align: center; ` + +export const DrawerExternalLink = styled.a.attrs({ + target: '_blank', + rel: 'noreferrer' +})` + cursor: pointer; + text-decoration: none; + color: #68bdf4; + + &:active { + text-decoration: none; + } + + &:before { + display: inline-block; + content: ' '; + background-image: url("data:image/svg+xml;utf8,${linkIcon}"); + height: 12px; + width: 12px; + margin-right: 7px; + } +` + +export const DrawerBrowserCommand = styled.span.attrs({ + className: 'remove-play-icon' +})` + background-color: #2a2c33; + border-radius: 2px; + padding: 3px; + + color: #e36962; + font-family: Fira Code; + + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + cursor: pointer; +` diff --git a/src/browser/components/icons/Icons.tsx b/src/browser/components/icons/Icons.tsx index a10b32b4b52..25ee3f1c588 100644 --- a/src/browser/components/icons/Icons.tsx +++ b/src/browser/components/icons/Icons.tsx @@ -26,6 +26,7 @@ import addCircle from 'icons/add-circle.svg' import appWindowCode from 'icons/app-window-code.svg' import arrowLeft from 'icons/arrow-left.svg' import arrowRight from 'icons/arrow-right.svg' +import backArrow from 'icons/back-arrow.svg' import buttonRefreshArrow from 'icons/button-refresh-arrow.svg' import cannyFeedback from 'icons/canny-feedback.svg' import cannyNotifications from 'icons/canny-notifications.svg' @@ -39,6 +40,7 @@ import expand from 'icons/expand.svg' import file from 'icons/file.svg' import folderEmpty from 'icons/folder-empty.svg' import help from 'icons/help.svg' +import monitorPlay from 'icons/monitor-play.svg' import navigationMenuVertical from 'icons/navigation-menu-vertical.svg' import neo4j from 'icons/neo-world.svg' import newFolder from 'icons/folder-add.svg' @@ -135,6 +137,17 @@ export const DatabaseIcon = (props: { ) } +export const GuidesDrawerIcon = (props: { isOpen: boolean }): JSX.Element => ( + +) + interface SidebarIconProps { isOpen: boolean title: string @@ -339,6 +352,7 @@ export const MinusIcon = (): JSX.Element => ( className="sl-minus-circle" /> ) + export const RightArrowIcon = (): JSX.Element => ( ( export const CannyNotificationsIcon = (): JSX.Element => ( ) + +export const BackIcon = ({ width }: { width: number }): JSX.Element => ( + +) diff --git a/src/browser/documentation/index.ts b/src/browser/documentation/index.ts index 421f334de28..8e258f9b770 100644 --- a/src/browser/documentation/index.ts +++ b/src/browser/documentation/index.ts @@ -88,42 +88,192 @@ import helpCypher from './dynamic/cypher' import helpHelp from './dynamic/help' import helpPlay from './dynamic/play' -// Carousels -import guideConcepts from './guides/concepts' -import guideCypher from './guides/cypher' -import guideIntro from './guides/intro' -import guideLearn from './guides/learn' -import guideMovieGraph from './guides/movie-graph' -import guideNorthwindGraph from './guides/northwind-graph' - -// Pages -import guideIconography from './guides/iconography' -import guideStart from './guides/start' -import guideTypography from './guides/typography' -import guideUnfound from './guides/unfound' -import guideWritecode from './guides/write-code' - -export default { +// Play guides +import playConcepts from './play-guides/concepts' +import playCypher from './play-guides/cypher' +import playIntro from './play-guides/intro' +import playLearn from './play-guides/learn' +import playMovieGraph from './play-guides/movie-graph' +import playNorthwindGraph from './play-guides/northwind-graph' +import playIconography from './play-guides/iconography' +import playStart from './play-guides/start' +import playTypography from './play-guides/typography' +import playUnfound from './play-guides/unfound' +import playWritecode from './play-guides/write-code' + +// Migrated sidebar guides +import guideConcepts from './sidebar-guides/concepts' +import guideCypher from './sidebar-guides/cypher' +import guideIndex from './sidebar-guides/guideIndex' +import guideIntro from './sidebar-guides/intro' +import guideMovieGraph from './sidebar-guides/movie-graph' +import guideNorthwindGraph from './sidebar-guides/northwind-graph' +import guideUnfound from './sidebar-guides/unfound' + +type AllDocumentation = { + help: HelpDocs + cypher: CypherDocs + bolt: BoltDocs + play: PlayDocs + guide: GuideDocs +} +type DocItem = { + title: string + subtitle?: string + category?: string + content?: JSX.Element | null + footer?: JSX.Element + slides?: JSX.Element[] +} + +type GuideItem = { + title: string + slides: JSX.Element[] +} + +type GuideDocs = { + title: 'Built-in Browser guides' + chapters: Record +} + +type GuideChapter = + | 'concepts' + | 'cypher' + | 'index' + | 'intro' + | 'movie-graph' + | 'movieGraph' + | 'movies' + | 'northwind' + | 'northwind-graph' + | 'northwindGraph' + | 'unfound' + +// TypeGuard function to ts to understand that a string is a valid key +export function isGuideChapter(name: string): name is GuideChapter { + return name in docs.guide.chapters +} + +type PlayDocs = { + title: 'Guides & Examples' + chapters: Record +} + +export type PlayChapter = + | 'concepts' + | 'cypher' + | 'iconography' + | 'intro' + | 'learn' + | 'movie-graph' + | 'movieGraph' + | 'movies' + | 'northwind' + | 'northwind-graph' + | 'northwindGraph' + | 'start' + | 'typography' + | 'unfound' + | 'writeCode' + +export function isPlayChapter(name: string): name is PlayChapter { + return name in docs.play.chapters +} + +type BoltDocs = { title: 'Bolt'; chapters: Record } +type BoltChapter = 'boltEncryption' | 'boltRouting' +type CypherDocs = { title: 'Cypher'; chapters: Record } +type CypherChapter = + | 'alterUser' + | 'contains' + | 'create' + | 'createConstraintOn' + | 'createDatabase' + | 'createIndexOn' + | 'createRole' + | 'createUser' + | 'delete' + | 'deny' + | 'detachDelete' + | 'dropConstraintOn' + | 'dropDatabase' + | 'dropIndexOn' + | 'dropRole' + | 'dropUser' + | 'endsWith' + | 'explain' + | 'foreach' + | 'grant' + | 'grantRole' + | 'loadCsv' + | 'match' + | 'merge' + | 'param' + | 'params' + | 'profile' + | 'queryPlan' + | 'remove' + | 'rest' + | 'restDelete' + | 'restGet' + | 'restPost' + | 'restPut' + | 'return' + | 'revoke' + | 'revokeRole' + | 'schema' + | 'set' + | 'showDatabases' + | 'showPrivileges' + | 'showRoles' + | 'showUsers' + | 'startsWith' + | 'template' + | 'unwind' + | 'where' + | 'with' + +type HelpDocs = { title: 'Commands'; chapters: Record } +type HelpChapter = + | 'auto' + | 'bolt' + | 'clear' + | 'commands' + | 'cypher' + | 'guides' + | 'help' + | 'history' + | 'historyClear' + | 'keys' + | 'play' + | 'queries' + | 'server' + | 'serverUser' + | 'style' + | 'unfound' + | 'unknown' + +const docs: AllDocumentation = { help: { title: 'Commands', chapters: { auto: helpAuto, - clear: helpClear, - cypher: helpCypher, bolt: helpBolt, + clear: helpClear, commands: helpCommands, + cypher: helpCypher, guides: helpPlay, help: helpHelp, history: helpHistory, historyClear: helpHistoryClear, keys: helpKeys, play: helpPlay, + queries: helpQueries, server: helpServer, serverUser: helpServerUser, style: helpStyle, unfound: helpUnfound, - unknown: helpUnknown, - queries: helpQueries + unknown: helpUnknown } }, cypher: { @@ -131,12 +281,12 @@ export default { chapters: { alterUser: helpAlterUser, contains: helpContains, + create: helpCreate, createConstraintOn: helpCreateConstraintOn, createDatabase: helpCreateDatabase, createIndexOn: helpCreateIndexOn, createRole: helpCreateRole, createUser: helpCreateUser, - create: helpCreate, delete: helpDelete, deny: helpDeny, detachDelete: helpDetachDelete, @@ -188,25 +338,41 @@ export default { }, play: { title: 'Guides & Examples', + chapters: { + concepts: playConcepts, + cypher: playCypher, + iconography: playIconography, + intro: playIntro, + learn: playLearn, + movieGraph: playMovieGraph, + 'movie-graph': playMovieGraph, + movies: playMovieGraph, + northwind: playNorthwindGraph, + 'northwind-graph': playNorthwindGraph, + northwindGraph: playNorthwindGraph, + start: playStart, + typography: playTypography, + unfound: playUnfound, + writeCode: playWritecode + } + }, + // Guides are play-guides but migrated to be viewable in the sidebar + guide: { + title: 'Built-in Browser guides', chapters: { concepts: guideConcepts, cypher: guideCypher, + index: guideIndex, intro: guideIntro, - learn: guideLearn, - movieGraph: guideMovieGraph, movies: guideMovieGraph, - northwindGraph: guideNorthwindGraph, + movieGraph: guideMovieGraph, + 'movie-graph': guideMovieGraph, northwind: guideNorthwindGraph, - iconography: guideIconography, - start: guideStart, - typography: guideTypography, - unfound: guideUnfound, - writeCode: guideWritecode, - // Commands only - 'query-template': { - title: 'Query Templates', - category: 'guides' - } - } as any + northwindGraph: guideNorthwindGraph, + 'northwind-graph': guideNorthwindGraph, + unfound: guideUnfound + } } } + +export default docs diff --git a/src/browser/documentation/guides/concepts.tsx b/src/browser/documentation/play-guides/concepts.tsx similarity index 100% rename from src/browser/documentation/guides/concepts.tsx rename to src/browser/documentation/play-guides/concepts.tsx diff --git a/src/browser/documentation/guides/cypher.tsx b/src/browser/documentation/play-guides/cypher.tsx similarity index 100% rename from src/browser/documentation/guides/cypher.tsx rename to src/browser/documentation/play-guides/cypher.tsx diff --git a/src/browser/documentation/guides/iconography.tsx b/src/browser/documentation/play-guides/iconography.tsx similarity index 100% rename from src/browser/documentation/guides/iconography.tsx rename to src/browser/documentation/play-guides/iconography.tsx diff --git a/src/browser/documentation/guides/intro.tsx b/src/browser/documentation/play-guides/intro.tsx similarity index 98% rename from src/browser/documentation/guides/intro.tsx rename to src/browser/documentation/play-guides/intro.tsx index d4bf88dd456..ab0d0cc839d 100644 --- a/src/browser/documentation/guides/intro.tsx +++ b/src/browser/documentation/play-guides/intro.tsx @@ -20,12 +20,7 @@ import React from 'react' import Slide from '../../modules/Carousel/Slide' -import { - FULLSCREEN_SHORTCUT, - FOCUS_SHORTCUT, - printShortcut, - isMac -} from 'browser/modules/App/keyboardShortcuts' +import { isMac } from 'browser/modules/App/keyboardShortcuts' const title = 'Intro' const slides = [ diff --git a/src/browser/documentation/guides/learn.tsx b/src/browser/documentation/play-guides/learn.tsx similarity index 100% rename from src/browser/documentation/guides/learn.tsx rename to src/browser/documentation/play-guides/learn.tsx diff --git a/src/browser/documentation/guides/movie-graph.tsx b/src/browser/documentation/play-guides/movie-graph.tsx similarity index 99% rename from src/browser/documentation/guides/movie-graph.tsx rename to src/browser/documentation/play-guides/movie-graph.tsx index 4317ad4bc23..545a22c091a 100644 --- a/src/browser/documentation/guides/movie-graph.tsx +++ b/src/browser/documentation/play-guides/movie-graph.tsx @@ -21,7 +21,6 @@ import React from 'react' import ManualLink from 'browser-components/ManualLink' import Slide from '../../modules/Carousel/Slide' -import TextCommand from 'browser/modules/DecoratedText/TextCommand' const title = 'Movie Graph' const category = 'graphExamples' @@ -799,10 +798,6 @@ RETURN tom, m, coActors, m2, cruise`}
           MATCH (n) DETACH DELETE n
         
-

Prove that the Movie Graph is gone

diff --git a/src/browser/documentation/guides/northwind-graph.tsx b/src/browser/documentation/play-guides/northwind-graph.tsx similarity index 100% rename from src/browser/documentation/guides/northwind-graph.tsx rename to src/browser/documentation/play-guides/northwind-graph.tsx diff --git a/src/browser/documentation/guides/start.tsx b/src/browser/documentation/play-guides/start.tsx similarity index 100% rename from src/browser/documentation/guides/start.tsx rename to src/browser/documentation/play-guides/start.tsx diff --git a/src/browser/documentation/guides/typography.tsx b/src/browser/documentation/play-guides/typography.tsx similarity index 100% rename from src/browser/documentation/guides/typography.tsx rename to src/browser/documentation/play-guides/typography.tsx diff --git a/src/browser/documentation/guides/unfound.tsx b/src/browser/documentation/play-guides/unfound.tsx similarity index 100% rename from src/browser/documentation/guides/unfound.tsx rename to src/browser/documentation/play-guides/unfound.tsx diff --git a/src/browser/documentation/guides/write-code.tsx b/src/browser/documentation/play-guides/write-code.tsx similarity index 100% rename from src/browser/documentation/guides/write-code.tsx rename to src/browser/documentation/play-guides/write-code.tsx diff --git a/src/browser/documentation/sidebar-guides/concepts.tsx b/src/browser/documentation/sidebar-guides/concepts.tsx new file mode 100644 index 00000000000..556d6f1853c --- /dev/null +++ b/src/browser/documentation/sidebar-guides/concepts.tsx @@ -0,0 +1,168 @@ +/* + * 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 ManualLink from 'browser-components/ManualLink' +import { SidebarSlide } from '../../modules/Carousel/Slide' + +const title = 'Concepts' +const category = 'guides' +const slides = [ + +

Graph Fundamentals

+

Basic concepts to get you going.

+

+ A graph database can store any kind of data using a few simple concepts: +

+
    +
  1. Nodes - graph data records
  2. +
  3. Relationships - connect nodes
  4. +
  5. Properties - named data values
  6. +
+
, + +

+ A  + Graph Database +

+

+ Neo4j stores data in a Graph, with records called Nodes. +

+

+ The simplest graph has just a single node with some named values called + Properties. Let's draw a social graph of our friends on the Neo4j team: +

+
    +
  1. Start by drawing a circle for the node
  2. +
  3. Add the name Emil
  4. +
  5. Note that he is from Sweden
  6. +
+
    +
  • Nodes are the name for data records in a graph
  • +
  • Data is stored as Properties
  • +
  • Properties are simple name/value pairs
  • +
+ +
, + +

Labels

+

Associate a set of nodes.

+

+ Nodes can be grouped together by applying a Label to each member. In our + social graph, we'll label each node that represents a Person. +

+
    +
  1. Apply the label "Person" to the node we created for Emil
  2. +
  3. Color "Person" nodes red
  4. +
+
    +
  • A node can have zero or more labels
  • +
  • Labels do not have any properties
  • +
+ +
, + +

More Nodes

+

+ Schema-free, nodes can have a mix of common and unique properties. +

+

+ Like any database, storing data in Neo4j can be as simple as adding more + records. We'll add a few more nodes: +

+
    +
  1. Emil has a klout score of 99
  2. +
  3. Johan, from Sweden, who is learning to surf
  4. +
  5. Ian, from England, who is an author
  6. +
  7. Rik, from Belgium, has a cat named Orval
  8. +
  9. Allison, from California, who surfs
  10. +
+
    +
  • Similar nodes can have different properties
  • +
  • Properties can be strings, numbers, or booleans
  • +
  • Neo4j can store billions of nodes
  • +
+ +
, + +

Consider Relationships

+

Connect nodes in the graph

+

+ The real power of Neo4j is in connected data. To associate any two nodes, + add a Relationship which describes how the records are related. +

+

In our social graph, we simply say who KNOWS whom:

+
    +
  1. Emil KNOWS Johan and Ian
  2. +
  3. Johan KNOWS Ian and Rik
  4. +
  5. Rik and Ian KNOWS Allison
  6. +
+
    +
  • Relationships always have direction
  • +
  • Relationships always have a type
  • +
  • Relationships form patterns of data
  • +
+ +
, + +

Relationship properties

+

Store information shared by two nodes.

+

+ In a property graph, relationships are data records that can also contain + properties. Looking more closely at Emil's relationships, note that: +

+
    +
  • Emil has known Johan since 2001
  • +
  • Emil rates Ian 5 (out of 5)
  • +
  • Everyone else can have similar relationship properties
  • +
+ +
, + +

Next steps

+

+ A property graph contains nodes and relationships, with properties on + both. +

+

Keep getting started

+
    +
  • + Intro - a guided tour +
  • +
  • + Cypher - query language +
  • +
  • + + Neo4j Cypher Manual + +
  • +
+
+

Jump into code

+ +
+] + +export default { title, category, slides } diff --git a/src/browser/documentation/sidebar-guides/cypher.tsx b/src/browser/documentation/sidebar-guides/cypher.tsx new file mode 100644 index 00000000000..e8387bcf67d --- /dev/null +++ b/src/browser/documentation/sidebar-guides/cypher.tsx @@ -0,0 +1,222 @@ +/* + * 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 ManualLink from 'browser-components/ManualLink' +import { SidebarSlide } from '../../modules/Carousel/Slide' + +const title = 'Cypher' +const slides = [ + +

Neo4j's graph query language

+

Neo4j's Cypher language is purpose built for working with graph data.

+
    +
  • uses patterns to describe graph data
  • +
  • familiar SQL-like clauses
  • +
  • declarative, describing what to find, not how to find it
  • +
+
, + +

CREATE

+

Create a node

+

Let's use Cypher to generate a small social graph.

+
+
+        {'CREATE (ee:Person { name: "Emil", from: "Sweden", klout: 99 })'}
+      
+
+
    +
  • + CREATE clause to create data +
  • +
  • + () parenthesis to indicate a node +
  • +
  • + ee:Person a variable 'ee' and label 'Person' for the new + node +
  • +
  • + {'{}'} brackets to add properties to the node +
  • +
+
, + +

MATCH

+

Finding nodes

+

Now find the node representing Emil:

+
+
+        MATCH (ee:Person) WHERE ee.name = "Emil" RETURN ee;
+      
+
+
    +
  • + MATCH clause to specify a pattern of nodes and + relationships +
  • +
  • + (ee:Person) a single node pattern with label 'Person' which + will assign matches to the variable 'ee' +
  • +
  • + WHERE clause to constrain the results +
  • +
  • + ee.name = "Emil" compares name property to the value "Emil" +
  • +
  • + RETURN clause used to request particular results +
  • +
+
, + +

CREATE more

+

Nodes and relationships

+

+ CREATE + clauses can create many nodes and relationships at once. +

+
+
+        {`MATCH (ee:Person) WHERE ee.name = "Emil"
+CREATE (js:Person { name: "Johan", from: "Sweden", learn: "surfing" }),
+(ir:Person { name: "Ian", from: "England", title: "author" }),
+(rvb:Person { name: "Rik", from: "Belgium", pet: "Orval" }),
+(ally:Person { name: "Allison", from: "California", hobby: "surfing" }),
+(ee)-[:KNOWS {since: 2001}]->(js),(ee)-[:KNOWS {rating: 5}]->(ir),
+(js)-[:KNOWS]->(ir),(js)-[:KNOWS]->(rvb),
+(ir)-[:KNOWS]->(js),(ir)-[:KNOWS]->(ally),
+(rvb)-[:KNOWS]->(ally)`}
+      
+
+
, + +

Pattern matching

+

Describe what to find in the graph

+

+ For instance, a pattern can be used to find Emil's friends: +

+
+
+        {`MATCH (ee:Person)-[:KNOWS]-(friends)
+WHERE ee.name = "Emil" RETURN ee, friends`}
+      
+
+
    +
  • + MATCH + clause to describe the pattern from known Nodes to found Nodes +
  • +
  • + (ee) + starts the pattern with a Person (qualified by WHERE) +
  • +
  • + -[:KNOWS]- + matches "KNOWS" relationships (in either direction) +
  • +
  • + (friends) + will be bound to Emil's friends +
  • +
+
, + +

Recommend

+

Using patterns

+

+ Pattern matching can be used to make recommendations. Johan is learning to + surf, so he may want to find a new friend who already does: +

+
+
+        {`MATCH (js:Person)-[:KNOWS]-()-[:KNOWS]-(surfer)
+WHERE js.name = "Johan" AND surfer.hobby = "surfing"
+RETURN DISTINCT surfer`}
+      
+
+
    +
  • + () + empty parenthesis to ignore these nodes +
  • +
  • + DISTINCT + because more than one path will match the pattern +
  • +
  • + surfer + will contain Allison, a friend of a friend who surfs +
  • +
+
, + +

Analyze

+

Using the visual query plan

+

+ Understand how your query works by prepending EXPLAIN or{' '} + PROFILE: +

+
+
+        {`PROFILE MATCH (js:Person)-[:KNOWS]-()-[:KNOWS]-(surfer)
+WHERE js.name = "Johan" AND surfer.hobby = "surfing"
+RETURN DISTINCT surfer`}
+      
+
+
, + +

Live Cypher warnings

+

Identify query problems in real time

+

+ As you type, the query editor notifies you about deprecated features and + potentially expensive queries. +

+ +
, + +

Next steps

+

+ Start your application using Cypher to create and query graph data. See{' '} + Help Cypher for more built-in cypher + documentation. +

+

Documentation

+
    +
  • + + Cypher Refcard + +
  • +
  • + + The Cypher chapter + {' '} + of the Neo4j Developer Manual +
  • +
+
+] + +export default { title, slides } diff --git a/src/browser/documentation/sidebar-guides/guideIndex.tsx b/src/browser/documentation/sidebar-guides/guideIndex.tsx new file mode 100644 index 00000000000..32cb1eca8ea --- /dev/null +++ b/src/browser/documentation/sidebar-guides/guideIndex.tsx @@ -0,0 +1,67 @@ +import React from 'react' +import { + DrawerBrowserCommand, + DrawerExternalLink, + DrawerSubHeader +} from 'browser-components/drawer/drawer-styled' +import { SidebarSlide } from 'browser/modules/Carousel/Slide' +import { LinkContainer, MarginTop, MarginTopLi, NoBulletsUl } from './styled' + +const title = '--DEFAULT_INDEX_GUIDE--' +const slides = [ + + You can also access Browser guides by running + + :guide [guide name] + + in the code editor. + + + Built-in guides + + + +
  • + + :guide intro + + Navigating Neo4j Browser +
  • + + + :guide concepts + + Property graph model concepts + + + + :guide cypher + + Cypher basics - create, match, delete + + + + + :guide movie-graph + + + Queries and recommendations with Cypher - movie use case + + + + + + :guide northwind-graph + + Translate and import relation data into graph + +
    + + + More guides + + +
    +] + +export default { title, slides } diff --git a/src/browser/documentation/sidebar-guides/intro.tsx b/src/browser/documentation/sidebar-guides/intro.tsx new file mode 100644 index 00000000000..d147be79424 --- /dev/null +++ b/src/browser/documentation/sidebar-guides/intro.tsx @@ -0,0 +1,194 @@ +/* + * 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 { SidebarSlide } from '../../modules/Carousel/Slide' +import { isMac } from 'browser/modules/App/keyboardShortcuts' + +const title = 'Introduction' +const slides = [ + +

    + Neo4j Browser is a command driven client, like a web-based shell + environment. It is perfect for running ad-hoc graph queries, with just + enough ability to prototype a Neo4j-based application. +

    +
      +
    • + Developer focused, for writing and running graph queries with Cypher +
    • +
    • Exportable tabular results of any query result
    • +
    • + Graph visualization of query results containing nodes and relationships +
    • +
    +
    , + +

    Editor

    +

    Command editing and execution

    +

    + Editor pane is used to edit and run for Cypher statements and Browser + commands. Browser commands begin with :, for example{' '} + :help +

    + + + + + + + + + + + + + + + +
    Execute current command +
    + {isMac ? '' : ''} +
    +
    Previous command in history +
    + {isMac ? '' : ''} +
    +
    Next command in history +
    + {isMac ? '' : ''} +
    +
    +

    +

    + You can view the list of keybinding anytime by running{' '} + :help keys or by pressing F1 in the editor to see all + editor-specific keybindings. +

    +
    + +
    , + +

    Result frame

    +

    Most recently executed command or Cypher query.

    +

    + A result frame is created for each command execution, added to the top of + the stream to create a scrollable collection in reverse chronological + order. +

    +
      +
    • If a frame is pinned it will always stay in the same position.
    • +
    • + You can clear the stream of result frames running :clear{' '} + command. +
    • +
    • + There are maximum 30 result frames displayed in the stream. You can + change this number in the Settings sidebar.{' '} +
    • +
    • + You can bring up the history of the executed commands and queries by + running :history command. +
    • +
    +
    + +
    , + +

    Reusable frame

    +

    Instead of the stream

    +

    + You can also iterate in the same frame instead of generating a scrollable + stream of frames. +

    +

    Adjust your preferences in the Settings sidebar tab.

    +
    + +
    , + +

    Sidebar: Database information

    + When Neo4j is installed, it is initiated with two databases - a{' '} + system database and a default neo4j database. + Launching Neo4j Browser will automatically point us to the{' '} + neo4j default database, shown by the neo4j$ + prompt in the editor. +
    + +
    , + +

    Sidebar: Favorites

    +

    + Favorite queries or commands can be saved in the local storage and + displayed in the sidebar.{' '} +

    +

    + Favorites are global and independent of project or database which means + that you can access your Favorites from Neo4j Browser with different + databases, hosting platforms, and data sets. +

    +
    + +
    , + +

    Sidebar: Project files

    +

    Save cypher files to share with your colleagues

    + Queries and commands can also be saved as Project files. Project + folder can be reached through Neo4j Desktop and your hard disk. Note this + feature is only available when Neo4j Browser is run in Desktop. +
    + +
    , + +

    Next steps

    + +
    +

    Jump into code

    + +
    +] + +export default { title, slides } diff --git a/src/browser/documentation/sidebar-guides/movie-graph.tsx b/src/browser/documentation/sidebar-guides/movie-graph.tsx new file mode 100644 index 00000000000..9e5ec20eaaa --- /dev/null +++ b/src/browser/documentation/sidebar-guides/movie-graph.tsx @@ -0,0 +1,805 @@ +/* + * 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 ManualLink from 'browser-components/ManualLink' +import { SidebarSlide } from '../../modules/Carousel/Slide' +import { BulletsInsideLi } from './styled' +import { DrawerExternalLink } from 'browser-components/drawer/drawer-styled' + +const title = 'Movie Graph' +const category = 'graphExamples' +const slides = [ + +

    + The Movie Graph is a mini graph application containing actors and + directors that are related through the movies they've collaborated on. +

    +

    This guide will show you how to:

    +
      +
    1. + Create: insert movie data into the graph +
    2. +
    3. + Find: retrieve individual movies and actors +
    4. +
    5. + Query: discover related actors and directors +
    6. +
    7. + Solve: the Bacon Path +
    8. +
    +

    +

    + WARNING: This guide will modify the data in the currently active + database.{' '} +

    +
    , + +

    Create

    +

    + Below is a code block containing a single Cypher query statement composed + of multiple CREATE clauses. This will create the movie graph. Keep in mind + this will add data to the database each time it is run. +

    +
      +
    1. Click on the code block
    2. +
    3. Notice it gets copied to the editor to the right
    4. +
    5. Click the editor's play button to execute
    6. +
    7. Wait for the query to finish
    8. +
    +
    +
    +        {`CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'})
    +CREATE (Keanu:Person {name:'Keanu Reeves', born:1964})
    +CREATE (Carrie:Person {name:'Carrie-Anne Moss', born:1967})
    +CREATE (Laurence:Person {name:'Laurence Fishburne', born:1961})
    +CREATE (Hugo:Person {name:'Hugo Weaving', born:1960})
    +CREATE (LillyW:Person {name:'Lilly Wachowski', born:1967})
    +CREATE (LanaW:Person {name:'Lana Wachowski', born:1965})
    +CREATE (JoelS:Person {name:'Joel Silver', born:1952})
    +CREATE
    +(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrix),
    +(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrix),
    +(Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrix),
    +(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrix),
    +(LillyW)-[:DIRECTED]->(TheMatrix),
    +(LanaW)-[:DIRECTED]->(TheMatrix),
    +(JoelS)-[:PRODUCED]->(TheMatrix)
    +
    +CREATE (Emil:Person {name:"Emil Eifrem", born:1978})
    +CREATE (Emil)-[:ACTED_IN {roles:["Emil"]}]->(TheMatrix)
    +
    +CREATE (TheMatrixReloaded:Movie {title:'The Matrix Reloaded', released:2003, tagline:'Free your mind'})
    +CREATE
    +(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixReloaded),
    +(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixReloaded),
    +(Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixReloaded),
    +(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixReloaded),
    +(LillyW)-[:DIRECTED]->(TheMatrixReloaded),
    +(LanaW)-[:DIRECTED]->(TheMatrixReloaded),
    +(JoelS)-[:PRODUCED]->(TheMatrixReloaded)
    +
    +CREATE (TheMatrixRevolutions:Movie {title:'The Matrix Revolutions', released:2003, tagline:'Everything that has a beginning has an end'})
    +CREATE
    +(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixRevolutions),
    +(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixRevolutions),
    +(Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixRevolutions),
    +(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixRevolutions),
    +(LillyW)-[:DIRECTED]->(TheMatrixRevolutions),
    +(LanaW)-[:DIRECTED]->(TheMatrixRevolutions),
    +(JoelS)-[:PRODUCED]->(TheMatrixRevolutions)
    +
    +CREATE (TheDevilsAdvocate:Movie {title:"The Devil's Advocate", released:1997, tagline:'Evil has its winning ways'})
    +CREATE (Charlize:Person {name:'Charlize Theron', born:1975})
    +CREATE (Al:Person {name:'Al Pacino', born:1940})
    +CREATE (Taylor:Person {name:'Taylor Hackford', born:1944})
    +CREATE
    +(Keanu)-[:ACTED_IN {roles:['Kevin Lomax']}]->(TheDevilsAdvocate),
    +(Charlize)-[:ACTED_IN {roles:['Mary Ann Lomax']}]->(TheDevilsAdvocate),
    +(Al)-[:ACTED_IN {roles:['John Milton']}]->(TheDevilsAdvocate),
    +(Taylor)-[:DIRECTED]->(TheDevilsAdvocate)
    +
    +CREATE (AFewGoodMen:Movie {title:"A Few Good Men", released:1992, tagline:"In the heart of the nation's capital, in a courthouse of the U.S. government, one man will stop at nothing to keep his honor, and one will stop at nothing to find the truth."})
    +CREATE (TomC:Person {name:'Tom Cruise', born:1962})
    +CREATE (JackN:Person {name:'Jack Nicholson', born:1937})
    +CREATE (DemiM:Person {name:'Demi Moore', born:1962})
    +CREATE (KevinB:Person {name:'Kevin Bacon', born:1958})
    +CREATE (KieferS:Person {name:'Kiefer Sutherland', born:1966})
    +CREATE (NoahW:Person {name:'Noah Wyle', born:1971})
    +CREATE (CubaG:Person {name:'Cuba Gooding Jr.', born:1968})
    +CREATE (KevinP:Person {name:'Kevin Pollak', born:1957})
    +CREATE (JTW:Person {name:'J.T. Walsh', born:1943})
    +CREATE (JamesM:Person {name:'James Marshall', born:1967})
    +CREATE (ChristopherG:Person {name:'Christopher Guest', born:1948})
    +CREATE (RobR:Person {name:'Rob Reiner', born:1947})
    +CREATE (AaronS:Person {name:'Aaron Sorkin', born:1961})
    +CREATE
    +(TomC)-[:ACTED_IN {roles:['Lt. Daniel Kaffee']}]->(AFewGoodMen),
    +(JackN)-[:ACTED_IN {roles:['Col. Nathan R. Jessup']}]->(AFewGoodMen),
    +(DemiM)-[:ACTED_IN {roles:['Lt. Cdr. JoAnne Galloway']}]->(AFewGoodMen),
    +(KevinB)-[:ACTED_IN {roles:['Capt. Jack Ross']}]->(AFewGoodMen),
    +(KieferS)-[:ACTED_IN {roles:['Lt. Jonathan Kendrick']}]->(AFewGoodMen),
    +(NoahW)-[:ACTED_IN {roles:['Cpl. Jeffrey Barnes']}]->(AFewGoodMen),
    +(CubaG)-[:ACTED_IN {roles:['Cpl. Carl Hammaker']}]->(AFewGoodMen),
    +(KevinP)-[:ACTED_IN {roles:['Lt. Sam Weinberg']}]->(AFewGoodMen),
    +(JTW)-[:ACTED_IN {roles:['Lt. Col. Matthew Andrew Markinson']}]->(AFewGoodMen),
    +(JamesM)-[:ACTED_IN {roles:['Pfc. Louden Downey']}]->(AFewGoodMen),
    +(ChristopherG)-[:ACTED_IN {roles:['Dr. Stone']}]->(AFewGoodMen),
    +(AaronS)-[:ACTED_IN {roles:['Man in Bar']}]->(AFewGoodMen),
    +(RobR)-[:DIRECTED]->(AFewGoodMen),
    +(AaronS)-[:WROTE]->(AFewGoodMen)
    +
    +CREATE (TopGun:Movie {title:"Top Gun", released:1986, tagline:'I feel the need, the need for speed.'})
    +CREATE (KellyM:Person {name:'Kelly McGillis', born:1957})
    +CREATE (ValK:Person {name:'Val Kilmer', born:1959})
    +CREATE (AnthonyE:Person {name:'Anthony Edwards', born:1962})
    +CREATE (TomS:Person {name:'Tom Skerritt', born:1933})
    +CREATE (MegR:Person {name:'Meg Ryan', born:1961})
    +CREATE (TonyS:Person {name:'Tony Scott', born:1944})
    +CREATE (JimC:Person {name:'Jim Cash', born:1941})
    +CREATE
    +(TomC)-[:ACTED_IN {roles:['Maverick']}]->(TopGun),
    +(KellyM)-[:ACTED_IN {roles:['Charlie']}]->(TopGun),
    +(ValK)-[:ACTED_IN {roles:['Iceman']}]->(TopGun),
    +(AnthonyE)-[:ACTED_IN {roles:['Goose']}]->(TopGun),
    +(TomS)-[:ACTED_IN {roles:['Viper']}]->(TopGun),
    +(MegR)-[:ACTED_IN {roles:['Carole']}]->(TopGun),
    +(TonyS)-[:DIRECTED]->(TopGun),
    +(JimC)-[:WROTE]->(TopGun)
    +
    +CREATE (JerryMaguire:Movie {title:'Jerry Maguire', released:2000, tagline:'The rest of his life begins now.'})
    +CREATE (ReneeZ:Person {name:'Renee Zellweger', born:1969})
    +CREATE (KellyP:Person {name:'Kelly Preston', born:1962})
    +CREATE (JerryO:Person {name:"Jerry O'Connell", born:1974})
    +CREATE (JayM:Person {name:'Jay Mohr', born:1970})
    +CREATE (BonnieH:Person {name:'Bonnie Hunt', born:1961})
    +CREATE (ReginaK:Person {name:'Regina King', born:1971})
    +CREATE (JonathanL:Person {name:'Jonathan Lipnicki', born:1996})
    +CREATE (CameronC:Person {name:'Cameron Crowe', born:1957})
    +CREATE
    +(TomC)-[:ACTED_IN {roles:['Jerry Maguire']}]->(JerryMaguire),
    +(CubaG)-[:ACTED_IN {roles:['Rod Tidwell']}]->(JerryMaguire),
    +(ReneeZ)-[:ACTED_IN {roles:['Dorothy Boyd']}]->(JerryMaguire),
    +(KellyP)-[:ACTED_IN {roles:['Avery Bishop']}]->(JerryMaguire),
    +(JerryO)-[:ACTED_IN {roles:['Frank Cushman']}]->(JerryMaguire),
    +(JayM)-[:ACTED_IN {roles:['Bob Sugar']}]->(JerryMaguire),
    +(BonnieH)-[:ACTED_IN {roles:['Laurel Boyd']}]->(JerryMaguire),
    +(ReginaK)-[:ACTED_IN {roles:['Marcee Tidwell']}]->(JerryMaguire),
    +(JonathanL)-[:ACTED_IN {roles:['Ray Boyd']}]->(JerryMaguire),
    +(CameronC)-[:DIRECTED]->(JerryMaguire),
    +(CameronC)-[:PRODUCED]->(JerryMaguire),
    +(CameronC)-[:WROTE]->(JerryMaguire)
    +
    +CREATE (StandByMe:Movie {title:"Stand By Me", released:1986, tagline:"For some, it's the last real taste of innocence, and the first real taste of life. But for everyone, it's the time that memories are made of."})
    +CREATE (RiverP:Person {name:'River Phoenix', born:1970})
    +CREATE (CoreyF:Person {name:'Corey Feldman', born:1971})
    +CREATE (WilW:Person {name:'Wil Wheaton', born:1972})
    +CREATE (JohnC:Person {name:'John Cusack', born:1966})
    +CREATE (MarshallB:Person {name:'Marshall Bell', born:1942})
    +CREATE
    +(WilW)-[:ACTED_IN {roles:['Gordie Lachance']}]->(StandByMe),
    +(RiverP)-[:ACTED_IN {roles:['Chris Chambers']}]->(StandByMe),
    +(JerryO)-[:ACTED_IN {roles:['Vern Tessio']}]->(StandByMe),
    +(CoreyF)-[:ACTED_IN {roles:['Teddy Duchamp']}]->(StandByMe),
    +(JohnC)-[:ACTED_IN {roles:['Denny Lachance']}]->(StandByMe),
    +(KieferS)-[:ACTED_IN {roles:['Ace Merrill']}]->(StandByMe),
    +(MarshallB)-[:ACTED_IN {roles:['Mr. Lachance']}]->(StandByMe),
    +(RobR)-[:DIRECTED]->(StandByMe)
    +
    +CREATE (AsGoodAsItGets:Movie {title:'As Good as It Gets', released:1997, tagline:'A comedy from the heart that goes for the throat.'})
    +CREATE (HelenH:Person {name:'Helen Hunt', born:1963})
    +CREATE (GregK:Person {name:'Greg Kinnear', born:1963})
    +CREATE (JamesB:Person {name:'James L. Brooks', born:1940})
    +CREATE
    +(JackN)-[:ACTED_IN {roles:['Melvin Udall']}]->(AsGoodAsItGets),
    +(HelenH)-[:ACTED_IN {roles:['Carol Connelly']}]->(AsGoodAsItGets),
    +(GregK)-[:ACTED_IN {roles:['Simon Bishop']}]->(AsGoodAsItGets),
    +(CubaG)-[:ACTED_IN {roles:['Frank Sachs']}]->(AsGoodAsItGets),
    +(JamesB)-[:DIRECTED]->(AsGoodAsItGets)
    +
    +CREATE (WhatDreamsMayCome:Movie {title:'What Dreams May Come', released:1998, tagline:'After life there is more. The end is just the beginning.'})
    +CREATE (AnnabellaS:Person {name:'Annabella Sciorra', born:1960})
    +CREATE (MaxS:Person {name:'Max von Sydow', born:1929})
    +CREATE (WernerH:Person {name:'Werner Herzog', born:1942})
    +CREATE (Robin:Person {name:'Robin Williams', born:1951})
    +CREATE (VincentW:Person {name:'Vincent Ward', born:1956})
    +CREATE
    +(Robin)-[:ACTED_IN {roles:['Chris Nielsen']}]->(WhatDreamsMayCome),
    +(CubaG)-[:ACTED_IN {roles:['Albert Lewis']}]->(WhatDreamsMayCome),
    +(AnnabellaS)-[:ACTED_IN {roles:['Annie Collins-Nielsen']}]->(WhatDreamsMayCome),
    +(MaxS)-[:ACTED_IN {roles:['The Tracker']}]->(WhatDreamsMayCome),
    +(WernerH)-[:ACTED_IN {roles:['The Face']}]->(WhatDreamsMayCome),
    +(VincentW)-[:DIRECTED]->(WhatDreamsMayCome)
    +
    +CREATE (SnowFallingonCedars:Movie {title:'Snow Falling on Cedars', released:1999, tagline:'First loves last. Forever.'})
    +CREATE (EthanH:Person {name:'Ethan Hawke', born:1970})
    +CREATE (RickY:Person {name:'Rick Yune', born:1971})
    +CREATE (JamesC:Person {name:'James Cromwell', born:1940})
    +CREATE (ScottH:Person {name:'Scott Hicks', born:1953})
    +CREATE
    +(EthanH)-[:ACTED_IN {roles:['Ishmael Chambers']}]->(SnowFallingonCedars),
    +(RickY)-[:ACTED_IN {roles:['Kazuo Miyamoto']}]->(SnowFallingonCedars),
    +(MaxS)-[:ACTED_IN {roles:['Nels Gudmundsson']}]->(SnowFallingonCedars),
    +(JamesC)-[:ACTED_IN {roles:['Judge Fielding']}]->(SnowFallingonCedars),
    +(ScottH)-[:DIRECTED]->(SnowFallingonCedars)
    +
    +CREATE (YouveGotMail:Movie {title:"You've Got Mail", released:1998, tagline:'At odds in life... in love on-line.'})
    +CREATE (ParkerP:Person {name:'Parker Posey', born:1968})
    +CREATE (DaveC:Person {name:'Dave Chappelle', born:1973})
    +CREATE (SteveZ:Person {name:'Steve Zahn', born:1967})
    +CREATE (TomH:Person {name:'Tom Hanks', born:1956})
    +CREATE (NoraE:Person {name:'Nora Ephron', born:1941})
    +CREATE
    +(TomH)-[:ACTED_IN {roles:['Joe Fox']}]->(YouveGotMail),
    +(MegR)-[:ACTED_IN {roles:['Kathleen Kelly']}]->(YouveGotMail),
    +(GregK)-[:ACTED_IN {roles:['Frank Navasky']}]->(YouveGotMail),
    +(ParkerP)-[:ACTED_IN {roles:['Patricia Eden']}]->(YouveGotMail),
    +(DaveC)-[:ACTED_IN {roles:['Kevin Jackson']}]->(YouveGotMail),
    +(SteveZ)-[:ACTED_IN {roles:['George Pappas']}]->(YouveGotMail),
    +(NoraE)-[:DIRECTED]->(YouveGotMail)
    +
    +CREATE (SleeplessInSeattle:Movie {title:'Sleepless in Seattle', released:1993, tagline:'What if someone you never met, someone you never saw, someone you never knew was the only someone for you?'})
    +CREATE (RitaW:Person {name:'Rita Wilson', born:1956})
    +CREATE (BillPull:Person {name:'Bill Pullman', born:1953})
    +CREATE (VictorG:Person {name:'Victor Garber', born:1949})
    +CREATE (RosieO:Person {name:"Rosie O'Donnell", born:1962})
    +CREATE
    +(TomH)-[:ACTED_IN {roles:['Sam Baldwin']}]->(SleeplessInSeattle),
    +(MegR)-[:ACTED_IN {roles:['Annie Reed']}]->(SleeplessInSeattle),
    +(RitaW)-[:ACTED_IN {roles:['Suzy']}]->(SleeplessInSeattle),
    +(BillPull)-[:ACTED_IN {roles:['Walter']}]->(SleeplessInSeattle),
    +(VictorG)-[:ACTED_IN {roles:['Greg']}]->(SleeplessInSeattle),
    +(RosieO)-[:ACTED_IN {roles:['Becky']}]->(SleeplessInSeattle),
    +(NoraE)-[:DIRECTED]->(SleeplessInSeattle)
    +
    +CREATE (JoeVersustheVolcano:Movie {title:'Joe Versus the Volcano', released:1990, tagline:'A story of love, lava and burning desire.'})
    +CREATE (JohnS:Person {name:'John Patrick Stanley', born:1950})
    +CREATE (Nathan:Person {name:'Nathan Lane', born:1956})
    +CREATE
    +(TomH)-[:ACTED_IN {roles:['Joe Banks']}]->(JoeVersustheVolcano),
    +(MegR)-[:ACTED_IN {roles:['DeDe', 'Angelica Graynamore', 'Patricia Graynamore']}]->(JoeVersustheVolcano),
    +(Nathan)-[:ACTED_IN {roles:['Baw']}]->(JoeVersustheVolcano),
    +(JohnS)-[:DIRECTED]->(JoeVersustheVolcano)
    +
    +CREATE (WhenHarryMetSally:Movie {title:'When Harry Met Sally', released:1998, tagline:'Can two friends sleep together and still love each other in the morning?'})
    +CREATE (BillyC:Person {name:'Billy Crystal', born:1948})
    +CREATE (CarrieF:Person {name:'Carrie Fisher', born:1956})
    +CREATE (BrunoK:Person {name:'Bruno Kirby', born:1949})
    +CREATE
    +(BillyC)-[:ACTED_IN {roles:['Harry Burns']}]->(WhenHarryMetSally),
    +(MegR)-[:ACTED_IN {roles:['Sally Albright']}]->(WhenHarryMetSally),
    +(CarrieF)-[:ACTED_IN {roles:['Marie']}]->(WhenHarryMetSally),
    +(BrunoK)-[:ACTED_IN {roles:['Jess']}]->(WhenHarryMetSally),
    +(RobR)-[:DIRECTED]->(WhenHarryMetSally),
    +(RobR)-[:PRODUCED]->(WhenHarryMetSally),
    +(NoraE)-[:PRODUCED]->(WhenHarryMetSally),
    +(NoraE)-[:WROTE]->(WhenHarryMetSally)
    +
    +CREATE (ThatThingYouDo:Movie {title:'That Thing You Do', released:1996, tagline:'In every life there comes a time when that thing you dream becomes that thing you do'})
    +CREATE (LivT:Person {name:'Liv Tyler', born:1977})
    +CREATE
    +(TomH)-[:ACTED_IN {roles:['Mr. White']}]->(ThatThingYouDo),
    +(LivT)-[:ACTED_IN {roles:['Faye Dolan']}]->(ThatThingYouDo),
    +(Charlize)-[:ACTED_IN {roles:['Tina']}]->(ThatThingYouDo),
    +(TomH)-[:DIRECTED]->(ThatThingYouDo)
    +
    +CREATE (TheReplacements:Movie {title:'The Replacements', released:2000, tagline:'Pain heals, Chicks dig scars... Glory lasts forever'})
    +CREATE (Brooke:Person {name:'Brooke Langton', born:1970})
    +CREATE (Gene:Person {name:'Gene Hackman', born:1930})
    +CREATE (Orlando:Person {name:'Orlando Jones', born:1968})
    +CREATE (Howard:Person {name:'Howard Deutch', born:1950})
    +CREATE
    +(Keanu)-[:ACTED_IN {roles:['Shane Falco']}]->(TheReplacements),
    +(Brooke)-[:ACTED_IN {roles:['Annabelle Farrell']}]->(TheReplacements),
    +(Gene)-[:ACTED_IN {roles:['Jimmy McGinty']}]->(TheReplacements),
    +(Orlando)-[:ACTED_IN {roles:['Clifford Franklin']}]->(TheReplacements),
    +(Howard)-[:DIRECTED]->(TheReplacements)
    +
    +CREATE (RescueDawn:Movie {title:'RescueDawn', released:2006, tagline:"Based on the extraordinary true story of one man's fight for freedom"})
    +CREATE (ChristianB:Person {name:'Christian Bale', born:1974})
    +CREATE (ZachG:Person {name:'Zach Grenier', born:1954})
    +CREATE
    +(MarshallB)-[:ACTED_IN {roles:['Admiral']}]->(RescueDawn),
    +(ChristianB)-[:ACTED_IN {roles:['Dieter Dengler']}]->(RescueDawn),
    +(ZachG)-[:ACTED_IN {roles:['Squad Leader']}]->(RescueDawn),
    +(SteveZ)-[:ACTED_IN {roles:['Duane']}]->(RescueDawn),
    +(WernerH)-[:DIRECTED]->(RescueDawn)
    +
    +CREATE (TheBirdcage:Movie {title:'The Birdcage', released:1996, tagline:'Come as you are'})
    +CREATE (MikeN:Person {name:'Mike Nichols', born:1931})
    +CREATE
    +(Robin)-[:ACTED_IN {roles:['Armand Goldman']}]->(TheBirdcage),
    +(Nathan)-[:ACTED_IN {roles:['Albert Goldman']}]->(TheBirdcage),
    +(Gene)-[:ACTED_IN {roles:['Sen. Kevin Keeley']}]->(TheBirdcage),
    +(MikeN)-[:DIRECTED]->(TheBirdcage)
    +
    +CREATE (Unforgiven:Movie {title:'Unforgiven', released:1992, tagline:"It's a hell of a thing, killing a man"})
    +CREATE (RichardH:Person {name:'Richard Harris', born:1930})
    +CREATE (ClintE:Person {name:'Clint Eastwood', born:1930})
    +CREATE
    +(RichardH)-[:ACTED_IN {roles:['English Bob']}]->(Unforgiven),
    +(ClintE)-[:ACTED_IN {roles:['Bill Munny']}]->(Unforgiven),
    +(Gene)-[:ACTED_IN {roles:['Little Bill Daggett']}]->(Unforgiven),
    +(ClintE)-[:DIRECTED]->(Unforgiven)
    +
    +CREATE (JohnnyMnemonic:Movie {title:'Johnny Mnemonic', released:1995, tagline:'The hottest data on earth. In the coolest head in town'})
    +CREATE (Takeshi:Person {name:'Takeshi Kitano', born:1947})
    +CREATE (Dina:Person {name:'Dina Meyer', born:1968})
    +CREATE (IceT:Person {name:'Ice-T', born:1958})
    +CREATE (RobertL:Person {name:'Robert Longo', born:1953})
    +CREATE
    +(Keanu)-[:ACTED_IN {roles:['Johnny Mnemonic']}]->(JohnnyMnemonic),
    +(Takeshi)-[:ACTED_IN {roles:['Takahashi']}]->(JohnnyMnemonic),
    +(Dina)-[:ACTED_IN {roles:['Jane']}]->(JohnnyMnemonic),
    +(IceT)-[:ACTED_IN {roles:['J-Bone']}]->(JohnnyMnemonic),
    +(RobertL)-[:DIRECTED]->(JohnnyMnemonic)
    +
    +CREATE (CloudAtlas:Movie {title:'Cloud Atlas', released:2012, tagline:'Everything is connected'})
    +CREATE (HalleB:Person {name:'Halle Berry', born:1966})
    +CREATE (JimB:Person {name:'Jim Broadbent', born:1949})
    +CREATE (TomT:Person {name:'Tom Tykwer', born:1965})
    +CREATE (DavidMitchell:Person {name:'David Mitchell', born:1969})
    +CREATE (StefanArndt:Person {name:'Stefan Arndt', born:1961})
    +CREATE
    +(TomH)-[:ACTED_IN {roles:['Zachry', 'Dr. Henry Goose', 'Isaac Sachs', 'Dermot Hoggins']}]->(CloudAtlas),
    +(Hugo)-[:ACTED_IN {roles:['Bill Smoke', 'Haskell Moore', 'Tadeusz Kesselring', 'Nurse Noakes', 'Boardman Mephi', 'Old Georgie']}]->(CloudAtlas),
    +(HalleB)-[:ACTED_IN {roles:['Luisa Rey', 'Jocasta Ayrs', 'Ovid', 'Meronym']}]->(CloudAtlas),
    +(JimB)-[:ACTED_IN {roles:['Vyvyan Ayrs', 'Captain Molyneux', 'Timothy Cavendish']}]->(CloudAtlas),
    +(TomT)-[:DIRECTED]->(CloudAtlas),
    +(LillyW)-[:DIRECTED]->(CloudAtlas),
    +(LanaW)-[:DIRECTED]->(CloudAtlas),
    +(DavidMitchell)-[:WROTE]->(CloudAtlas),
    +(StefanArndt)-[:PRODUCED]->(CloudAtlas)
    +
    +CREATE (TheDaVinciCode:Movie {title:'The Da Vinci Code', released:2006, tagline:'Break The Codes'})
    +CREATE (IanM:Person {name:'Ian McKellen', born:1939})
    +CREATE (AudreyT:Person {name:'Audrey Tautou', born:1976})
    +CREATE (PaulB:Person {name:'Paul Bettany', born:1971})
    +CREATE (RonH:Person {name:'Ron Howard', born:1954})
    +CREATE
    +(TomH)-[:ACTED_IN {roles:['Dr. Robert Langdon']}]->(TheDaVinciCode),
    +(IanM)-[:ACTED_IN {roles:['Sir Leight Teabing']}]->(TheDaVinciCode),
    +(AudreyT)-[:ACTED_IN {roles:['Sophie Neveu']}]->(TheDaVinciCode),
    +(PaulB)-[:ACTED_IN {roles:['Silas']}]->(TheDaVinciCode),
    +(RonH)-[:DIRECTED]->(TheDaVinciCode)
    +
    +CREATE (VforVendetta:Movie {title:'V for Vendetta', released:2006, tagline:'Freedom! Forever!'})
    +CREATE (NatalieP:Person {name:'Natalie Portman', born:1981})
    +CREATE (StephenR:Person {name:'Stephen Rea', born:1946})
    +CREATE (JohnH:Person {name:'John Hurt', born:1940})
    +CREATE (BenM:Person {name: 'Ben Miles', born:1967})
    +CREATE
    +(Hugo)-[:ACTED_IN {roles:['V']}]->(VforVendetta),
    +(NatalieP)-[:ACTED_IN {roles:['Evey Hammond']}]->(VforVendetta),
    +(StephenR)-[:ACTED_IN {roles:['Eric Finch']}]->(VforVendetta),
    +(JohnH)-[:ACTED_IN {roles:['High Chancellor Adam Sutler']}]->(VforVendetta),
    +(BenM)-[:ACTED_IN {roles:['Dascomb']}]->(VforVendetta),
    +(JamesM)-[:DIRECTED]->(VforVendetta),
    +(LillyW)-[:PRODUCED]->(VforVendetta),
    +(LanaW)-[:PRODUCED]->(VforVendetta),
    +(JoelS)-[:PRODUCED]->(VforVendetta),
    +(LillyW)-[:WROTE]->(VforVendetta),
    +(LanaW)-[:WROTE]->(VforVendetta)
    +
    +CREATE (SpeedRacer:Movie {title:'Speed Racer', released:2008, tagline:'Speed has no limits'})
    +CREATE (EmileH:Person {name:'Emile Hirsch', born:1985})
    +CREATE (JohnG:Person {name:'John Goodman', born:1960})
    +CREATE (SusanS:Person {name:'Susan Sarandon', born:1946})
    +CREATE (MatthewF:Person {name:'Matthew Fox', born:1966})
    +CREATE (ChristinaR:Person {name:'Christina Ricci', born:1980})
    +CREATE (Rain:Person {name:'Rain', born:1982})
    +CREATE
    +(EmileH)-[:ACTED_IN {roles:['Speed Racer']}]->(SpeedRacer),
    +(JohnG)-[:ACTED_IN {roles:['Pops']}]->(SpeedRacer),
    +(SusanS)-[:ACTED_IN {roles:['Mom']}]->(SpeedRacer),
    +(MatthewF)-[:ACTED_IN {roles:['Racer X']}]->(SpeedRacer),
    +(ChristinaR)-[:ACTED_IN {roles:['Trixie']}]->(SpeedRacer),
    +(Rain)-[:ACTED_IN {roles:['Taejo Togokahn']}]->(SpeedRacer),
    +(BenM)-[:ACTED_IN {roles:['Cass Jones']}]->(SpeedRacer),
    +(LillyW)-[:DIRECTED]->(SpeedRacer),
    +(LanaW)-[:DIRECTED]->(SpeedRacer),
    +(LillyW)-[:WROTE]->(SpeedRacer),
    +(LanaW)-[:WROTE]->(SpeedRacer),
    +(JoelS)-[:PRODUCED]->(SpeedRacer)
    +
    +CREATE (NinjaAssassin:Movie {title:'Ninja Assassin', released:2009, tagline:'Prepare to enter a secret world of assassins'})
    +CREATE (NaomieH:Person {name:'Naomie Harris'})
    +CREATE
    +(Rain)-[:ACTED_IN {roles:['Raizo']}]->(NinjaAssassin),
    +(NaomieH)-[:ACTED_IN {roles:['Mika Coretti']}]->(NinjaAssassin),
    +(RickY)-[:ACTED_IN {roles:['Takeshi']}]->(NinjaAssassin),
    +(BenM)-[:ACTED_IN {roles:['Ryan Maslow']}]->(NinjaAssassin),
    +(JamesM)-[:DIRECTED]->(NinjaAssassin),
    +(LillyW)-[:PRODUCED]->(NinjaAssassin),
    +(LanaW)-[:PRODUCED]->(NinjaAssassin),
    +(JoelS)-[:PRODUCED]->(NinjaAssassin)
    +
    +CREATE (TheGreenMile:Movie {title:'The Green Mile', released:1999, tagline:"Walk a mile you'll never forget."})
    +CREATE (MichaelD:Person {name:'Michael Clarke Duncan', born:1957})
    +CREATE (DavidM:Person {name:'David Morse', born:1953})
    +CREATE (SamR:Person {name:'Sam Rockwell', born:1968})
    +CREATE (GaryS:Person {name:'Gary Sinise', born:1955})
    +CREATE (PatriciaC:Person {name:'Patricia Clarkson', born:1959})
    +CREATE (FrankD:Person {name:'Frank Darabont', born:1959})
    +CREATE
    +(TomH)-[:ACTED_IN {roles:['Paul Edgecomb']}]->(TheGreenMile),
    +(MichaelD)-[:ACTED_IN {roles:['John Coffey']}]->(TheGreenMile),
    +(DavidM)-[:ACTED_IN {roles:['Brutus "Brutal" Howell']}]->(TheGreenMile),
    +(BonnieH)-[:ACTED_IN {roles:['Jan Edgecomb']}]->(TheGreenMile),
    +(JamesC)-[:ACTED_IN {roles:['Warden Hal Moores']}]->(TheGreenMile),
    +(SamR)-[:ACTED_IN {roles:['"Wild Bill" Wharton']}]->(TheGreenMile),
    +(GaryS)-[:ACTED_IN {roles:['Burt Hammersmith']}]->(TheGreenMile),
    +(PatriciaC)-[:ACTED_IN {roles:['Melinda Moores']}]->(TheGreenMile),
    +(FrankD)-[:DIRECTED]->(TheGreenMile)
    +
    +CREATE (FrostNixon:Movie {title:'Frost/Nixon', released:2008, tagline:'400 million people were waiting for the truth.'})
    +CREATE (FrankL:Person {name:'Frank Langella', born:1938})
    +CREATE (MichaelS:Person {name:'Michael Sheen', born:1969})
    +CREATE (OliverP:Person {name:'Oliver Platt', born:1960})
    +CREATE
    +(FrankL)-[:ACTED_IN {roles:['Richard Nixon']}]->(FrostNixon),
    +(MichaelS)-[:ACTED_IN {roles:['David Frost']}]->(FrostNixon),
    +(KevinB)-[:ACTED_IN {roles:['Jack Brennan']}]->(FrostNixon),
    +(OliverP)-[:ACTED_IN {roles:['Bob Zelnick']}]->(FrostNixon),
    +(SamR)-[:ACTED_IN {roles:['James Reston, Jr.']}]->(FrostNixon),
    +(RonH)-[:DIRECTED]->(FrostNixon)
    +
    +CREATE (Hoffa:Movie {title:'Hoffa', released:1992, tagline:"He didn't want law. He wanted justice."})
    +CREATE (DannyD:Person {name:'Danny DeVito', born:1944})
    +CREATE (JohnR:Person {name:'John C. Reilly', born:1965})
    +CREATE
    +(JackN)-[:ACTED_IN {roles:['Hoffa']}]->(Hoffa),
    +(DannyD)-[:ACTED_IN {roles:['Robert "Bobby" Ciaro']}]->(Hoffa),
    +(JTW)-[:ACTED_IN {roles:['Frank Fitzsimmons']}]->(Hoffa),
    +(JohnR)-[:ACTED_IN {roles:['Peter "Pete" Connelly']}]->(Hoffa),
    +(DannyD)-[:DIRECTED]->(Hoffa)
    +
    +CREATE (Apollo13:Movie {title:'Apollo 13', released:1995, tagline:'Houston, we have a problem.'})
    +CREATE (EdH:Person {name:'Ed Harris', born:1950})
    +CREATE (BillPax:Person {name:'Bill Paxton', born:1955})
    +CREATE
    +(TomH)-[:ACTED_IN {roles:['Jim Lovell']}]->(Apollo13),
    +(KevinB)-[:ACTED_IN {roles:['Jack Swigert']}]->(Apollo13),
    +(EdH)-[:ACTED_IN {roles:['Gene Kranz']}]->(Apollo13),
    +(BillPax)-[:ACTED_IN {roles:['Fred Haise']}]->(Apollo13),
    +(GaryS)-[:ACTED_IN {roles:['Ken Mattingly']}]->(Apollo13),
    +(RonH)-[:DIRECTED]->(Apollo13)
    +
    +CREATE (Twister:Movie {title:'Twister', released:1996, tagline:"Don't Breathe. Don't Look Back."})
    +CREATE (PhilipH:Person {name:'Philip Seymour Hoffman', born:1967})
    +CREATE (JanB:Person {name:'Jan de Bont', born:1943})
    +CREATE
    +(BillPax)-[:ACTED_IN {roles:['Bill Harding']}]->(Twister),
    +(HelenH)-[:ACTED_IN {roles:['Dr. Jo Harding']}]->(Twister),
    +(ZachG)-[:ACTED_IN {roles:['Eddie']}]->(Twister),
    +(PhilipH)-[:ACTED_IN {roles:['Dustin "Dusty" Davis']}]->(Twister),
    +(JanB)-[:DIRECTED]->(Twister)
    +
    +CREATE (CastAway:Movie {title:'Cast Away', released:2000, tagline:'At the edge of the world, his journey begins.'})
    +CREATE (RobertZ:Person {name:'Robert Zemeckis', born:1951})
    +CREATE
    +(TomH)-[:ACTED_IN {roles:['Chuck Noland']}]->(CastAway),
    +(HelenH)-[:ACTED_IN {roles:['Kelly Frears']}]->(CastAway),
    +(RobertZ)-[:DIRECTED]->(CastAway)
    +
    +CREATE (OneFlewOvertheCuckoosNest:Movie {title:"One Flew Over the Cuckoo's Nest", released:1975, tagline:"If he's crazy, what does that make you?"})
    +CREATE (MilosF:Person {name:'Milos Forman', born:1932})
    +CREATE
    +(JackN)-[:ACTED_IN {roles:['Randle McMurphy']}]->(OneFlewOvertheCuckoosNest),
    +(DannyD)-[:ACTED_IN {roles:['Martini']}]->(OneFlewOvertheCuckoosNest),
    +(MilosF)-[:DIRECTED]->(OneFlewOvertheCuckoosNest)
    +
    +CREATE (SomethingsGottaGive:Movie {title:"Something's Gotta Give", released:2003})
    +CREATE (DianeK:Person {name:'Diane Keaton', born:1946})
    +CREATE (NancyM:Person {name:'Nancy Meyers', born:1949})
    +CREATE
    +(JackN)-[:ACTED_IN {roles:['Harry Sanborn']}]->(SomethingsGottaGive),
    +(DianeK)-[:ACTED_IN {roles:['Erica Barry']}]->(SomethingsGottaGive),
    +(Keanu)-[:ACTED_IN {roles:['Julian Mercer']}]->(SomethingsGottaGive),
    +(NancyM)-[:DIRECTED]->(SomethingsGottaGive),
    +(NancyM)-[:PRODUCED]->(SomethingsGottaGive),
    +(NancyM)-[:WROTE]->(SomethingsGottaGive)
    +
    +CREATE (BicentennialMan:Movie {title:'Bicentennial Man', released:1999, tagline:"One robot's 200 year journey to become an ordinary man."})
    +CREATE (ChrisC:Person {name:'Chris Columbus', born:1958})
    +CREATE
    +(Robin)-[:ACTED_IN {roles:['Andrew Marin']}]->(BicentennialMan),
    +(OliverP)-[:ACTED_IN {roles:['Rupert Burns']}]->(BicentennialMan),
    +(ChrisC)-[:DIRECTED]->(BicentennialMan)
    +
    +CREATE (CharlieWilsonsWar:Movie {title:"Charlie Wilson's War", released:2007, tagline:"A stiff drink. A little mascara. A lot of nerve. Who said they couldn't bring down the Soviet empire."})
    +CREATE (JuliaR:Person {name:'Julia Roberts', born:1967})
    +CREATE
    +(TomH)-[:ACTED_IN {roles:['Rep. Charlie Wilson']}]->(CharlieWilsonsWar),
    +(JuliaR)-[:ACTED_IN {roles:['Joanne Herring']}]->(CharlieWilsonsWar),
    +(PhilipH)-[:ACTED_IN {roles:['Gust Avrakotos']}]->(CharlieWilsonsWar),
    +(MikeN)-[:DIRECTED]->(CharlieWilsonsWar)
    +
    +CREATE (ThePolarExpress:Movie {title:'The Polar Express', released:2004, tagline:'This Holiday Season... Believe'})
    +CREATE
    +(TomH)-[:ACTED_IN {roles:['Hero Boy', 'Father', 'Conductor', 'Hobo', 'Scrooge', 'Santa Claus']}]->(ThePolarExpress),
    +(RobertZ)-[:DIRECTED]->(ThePolarExpress)
    +
    +CREATE (ALeagueofTheirOwn:Movie {title:'A League of Their Own', released:1992, tagline:'Once in a lifetime you get a chance to do something different.'})
    +CREATE (Madonna:Person {name:'Madonna', born:1954})
    +CREATE (GeenaD:Person {name:'Geena Davis', born:1956})
    +CREATE (LoriP:Person {name:'Lori Petty', born:1963})
    +CREATE (PennyM:Person {name:'Penny Marshall', born:1943})
    +CREATE
    +(TomH)-[:ACTED_IN {roles:['Jimmy Dugan']}]->(ALeagueofTheirOwn),
    +(GeenaD)-[:ACTED_IN {roles:['Dottie Hinson']}]->(ALeagueofTheirOwn),
    +(LoriP)-[:ACTED_IN {roles:['Kit Keller']}]->(ALeagueofTheirOwn),
    +(RosieO)-[:ACTED_IN {roles:['Doris Murphy']}]->(ALeagueofTheirOwn),
    +(Madonna)-[:ACTED_IN {roles:['"All the Way" Mae Mordabito']}]->(ALeagueofTheirOwn),
    +(BillPax)-[:ACTED_IN {roles:['Bob Hinson']}]->(ALeagueofTheirOwn),
    +(PennyM)-[:DIRECTED]->(ALeagueofTheirOwn)
    +
    +CREATE (PaulBlythe:Person {name:'Paul Blythe'})
    +CREATE (AngelaScope:Person {name:'Angela Scope'})
    +CREATE (JessicaThompson:Person {name:'Jessica Thompson'})
    +CREATE (JamesThompson:Person {name:'James Thompson'})
    +
    +CREATE
    +(JamesThompson)-[:FOLLOWS]->(JessicaThompson),
    +(AngelaScope)-[:FOLLOWS]->(JessicaThompson),
    +(PaulBlythe)-[:FOLLOWS]->(AngelaScope)
    +
    +CREATE
    +(JessicaThompson)-[:REVIEWED {summary:'An amazing journey', rating:95}]->(CloudAtlas),
    +(JessicaThompson)-[:REVIEWED {summary:'Silly, but fun', rating:65}]->(TheReplacements),
    +(JamesThompson)-[:REVIEWED {summary:'The coolest football movie ever', rating:100}]->(TheReplacements),
    +(AngelaScope)-[:REVIEWED {summary:'Pretty funny at times', rating:62}]->(TheReplacements),
    +(JessicaThompson)-[:REVIEWED {summary:'Dark, but compelling', rating:85}]->(Unforgiven),
    +(JessicaThompson)-[:REVIEWED {summary:"Slapstick redeemed only by the Robin Williams and Gene Hackman's stellar performances", rating:45}]->(TheBirdcage),
    +(JessicaThompson)-[:REVIEWED {summary:'A solid romp', rating:68}]->(TheDaVinciCode),
    +(JamesThompson)-[:REVIEWED {summary:'Fun, but a little far fetched', rating:65}]->(TheDaVinciCode),
    +(JessicaThompson)-[:REVIEWED {summary:'You had me at Jerry', rating:92}]->(JerryMaguire)
    +
    +WITH TomH as a
    +MATCH (a)-[:ACTED_IN]->(m)<-[:DIRECTED]-(d) RETURN a,m,d LIMIT 10;`}
    +      
    +
    +
    +

    + :help cypher{' '} + CREATE +

    +
    , + +

    Find

    +

    Example queries for finding individual nodes.

    +
      +
    1. Click on any query example
    2. +
    3. Run the query from the editor
    4. +
    5. Notice the syntax pattern
    6. +
    7. Try looking for other movies or actors
    8. +
    +
    +

    Find the actor named "Tom Hanks"...

    +
    +
    +        {'MATCH (tom {name: "Tom Hanks"}) RETURN tom'}
    +      
    +
    +

    Find the movie with title "Cloud Atlas"...

    +
    +
    +        {'MATCH (cloudAtlas {title: "Cloud Atlas"}) RETURN cloudAtlas'}
    +      
    +
    +

    Find 10 people...

    +
    +
    +        MATCH (people:Person) RETURN people.name LIMIT 10
    +      
    +
    +

    Find movies released in the 1990s...

    +
    +
    +        {
    +          'MATCH (nineties:Movie) WHERE nineties.released >= 1990 AND nineties.released < 2000 RETURN nineties.title'
    +        }
    +      
    +
    +
    +

    + :help MATCH{' '} + WHERE RETURN +

    +
    , + +

    Query

    +

    Finding patterns within the graph.

    +
      +
    1. Actors are people who acted in movies
    2. +
    3. Directors are people who directed a movie
    4. +
    5. What other relationships exist?
    6. +
    +
    +

    List all Tom Hanks movies...

    +
    +
    +        {
    +          'MATCH (tom:Person {name: "Tom Hanks"})-[:ACTED_IN]->(tomHanksMovies) RETURN tom,tomHanksMovies'
    +        }
    +      
    +
    +

    Who directed "Cloud Atlas"?

    +
    +
    +        {
    +          'MATCH (cloudAtlas {title: "Cloud Atlas"})<-[:DIRECTED]-(directors) RETURN directors.name'
    +        }
    +      
    +
    +

    Tom Hanks' co-actors...

    +
    +
    +        {
    +          'MATCH (tom:Person {name:"Tom Hanks"})-[:ACTED_IN]->(m)<-[:ACTED_IN]-(coActors) RETURN coActors.name'
    +        }
    +      
    +
    +

    How people are related to "Cloud Atlas"...

    +
    +
    +        {
    +          'MATCH (people:Person)-[relatedTo]-(:Movie {title: "Cloud Atlas"}) RETURN people.name, Type(relatedTo), relatedTo'
    +        }
    +      
    +
    +
    +

    + :help MATCH +

    +
    , + +

    Solve

    +

    + You've heard of the classic "Six Degrees of Kevin Bacon"? That is simply a + shortest path query called the "Bacon Path". +

    +
      +
    1. Variable length patterns
    2. +
    3. Built-in shortestPath() algorithm
    4. +
    +
    +

    + Movies and actors up to 4 "hops" away from Kevin Bacon +

    +
    +
    +        {`MATCH (bacon:Person {name:"Kevin Bacon"})-[*1..4]-(hollywood)
    +RETURN DISTINCT hollywood`}
    +      
    +
    +

    + Bacon path, the shortest path of any relationships to Meg Ryan +

    +
    +
    +        {`MATCH p=shortestPath(
    +(bacon:Person {name:"Kevin Bacon"})-[*]-(meg:Person {name:"Meg Ryan"})
    +)
    +RETURN p`}
    +      
    + +
    +
    , + +

    Recommend

    +

    + Let's recommend new co-actors for Tom Hanks. A basic recommendation + approach is to find connections past an immediate neighborhood which are + themselves well connected. +

    +

    For Tom Hanks, that means:

    +
      +
    1. + Find actors that Tom Hanks hasn't yet worked with, but his co-actors + have. +
    2. +
    3. Find someone who can introduce Tom to his potential co-actor.
    4. +
    +

    + Extend Tom Hanks co-actors, to find co-co-actors who haven't worked with + Tom Hanks... +

    +
    +
    +        {`MATCH (tom:Person {name:"Tom Hanks"})-[:ACTED_IN]->(m)<-[:ACTED_IN]-(coActors),
    +  (coActors)-[:ACTED_IN]->(m2)<-[:ACTED_IN]-(cocoActors)
    +WHERE NOT (tom)-[:ACTED_IN]->()<-[:ACTED_IN]-(cocoActors) AND tom <> cocoActors
    +RETURN cocoActors.name AS Recommended, count(*) AS Strength ORDER BY Strength DESC`}
    +      
    +
    +

    Find someone to introduce Tom Hanks to Tom Cruise

    +
    +
    +        {`MATCH (tom:Person {name:"Tom Hanks"})-[:ACTED_IN]->(m)<-[:ACTED_IN]-(coActors),
    +  (coActors)-[:ACTED_IN]->(m2)<-[:ACTED_IN]-(cruise:Person {name:"Tom Cruise"})
    +RETURN tom, m, coActors, m2, cruise`}
    +      
    +
    +
    , + +

    Clean up

    +

    When you're done experimenting, you can remove the movie data set.

    +

    + Note: Nodes can't be deleted if they have relationships, so we need to + detach the nodes to delete the nodes. +

    +
    +

    + Delete all Movie and Person nodes, and their relationships +

    +
    +
    +        MATCH (n) DETACH DELETE n
    +      
    +
    +

    Prove that the Movie Graph is gone

    +
    +
    MATCH (n) RETURN n
    +
    +
    +

    + :help DELETE +

    +
    , + +

    Next steps

    +
      +
    • + Northwind Graph- from RDBMS to + graph +
    • +
    • + Cypher- Learn Cypher syntax +
    • +
    • + + Explore more guides: Graph Gists Portal + +
    • +
    +
    +

    Documentation

    +
      +
    • + + Developer resources + +
    • +
    • + + Neo4j Cypher Manual + +
    • +
    +
    +] + +export default { title, category, slides } diff --git a/src/browser/documentation/sidebar-guides/northwind-graph.tsx b/src/browser/documentation/sidebar-guides/northwind-graph.tsx new file mode 100644 index 00000000000..743e4e23406 --- /dev/null +++ b/src/browser/documentation/sidebar-guides/northwind-graph.tsx @@ -0,0 +1,325 @@ +/* + * 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 ManualLink from 'browser-components/ManualLink' +import { SidebarSlide } from '../../modules/Carousel/Slide' +import { DrawerExternalLink } from 'browser-components/drawer/drawer-styled' + +const title = 'Northwind Graph' +const category = 'graphExamples' +const slides = [ + +

    From RDBMS to Graph, using a classic dataset

    +

    + The Northwind Graph demonstrates how to migrate from a relational + database to Neo4j. The transformation is iterative and deliberate, + emphasizing the conceptual shift from relational tables to the nodes and + relationships of a graph. +

    +

    This guide will show you how to:

    +
      +
    1. Load: create data from external CSV files
    2. +
    3. Index: index nodes based on label
    4. +
    5. Relate: transform foreign key references into data relationships
    6. +
    7. Promote: transform join records into relationships
    8. +
    +
    , + +

    Product Catalog

    +

    + Northwind sells food products in a few categories, provided by suppliers. + Let's start by loading the product catalog tables. +

    +

    + The load statements to the right require public internet access. + LOAD CSV will retrieve a CSV file from a valid URL, applying + a Cypher statement to each row using a named map (here we're using the + name `row`). +

    +

    + +

    +

    Load records

    +
    +
    +        {`LOAD CSV WITH HEADERS FROM "http://data.neo4j.com/northwind/products.csv" AS row
    +CREATE (n:Product)
    +SET n = row,
    +n.unitPrice = toFloat(row.unitPrice),
    +n.unitsInStock = toInteger(row.unitsInStock), n.unitsOnOrder = toInteger(row.unitsOnOrder),
    +n.reorderLevel = toInteger(row.reorderLevel), n.discontinued = (row.discontinued <> "0")`}
    +      
    +
    +
    +
    +        {`LOAD CSV WITH HEADERS FROM "http://data.neo4j.com/northwind/categories.csv" AS row
    +CREATE (n:Category)
    +SET n = row`}
    +      
    +
    +
    +
    +        {`LOAD CSV WITH HEADERS FROM "http://data.neo4j.com/northwind/suppliers.csv" AS row
    +CREATE (n:Supplier)
    +SET n = row`}
    +      
    +
    +

    Create indexes

    +
    +
    +        CREATE INDEX ON :Product(productID)
    +      
    +
    +
    +
    +        CREATE INDEX ON :Category(categoryID)
    +      
    +
    +
    +
    +        CREATE INDEX ON :Supplier(supplierID)
    +      
    +
    +
    +

    + :help cypher{' '} + LOAD CSV +

    +
    , + +

    Product Catalog Graph

    +

    + The products, categories and suppliers are related through foreign key + references. Let's promote those to data relationships to realize the + graph. +

    +

    + +

    +

    Create data relationships

    +
    +
    +        {`MATCH (p:Product),(c:Category)
    +WHERE p.categoryID = c.categoryID
    +CREATE (p)-[:PART_OF]->(c)`}
    +      
    + +
    + Calculate join, materialize relationship. (See{' '} + + {' '} + importing guide + {' '} + for more details) +
    +
    +
    +
    +        {`MATCH (p:Product),(s:Supplier)
    +WHERE p.supplierID = s.supplierID
    +CREATE (s)-[:SUPPLIES]->(p)`}
    +      
    + +
    +
    +

    + :help cypher{' '} + MATCH +

    +
    , + +

    Querying Product Catalog Graph

    +

    Lets try some queries using patterns.

    +

    + +

    +

    Query using patterns

    +
    +
    +        {`MATCH (s:Supplier)-->(:Product)-->(c:Category)
    +RETURN s.companyName as Company, collect(distinct c.categoryName) as Categories`}
    +      
    +
    + List the product categories provided by each supplier. +
    +
    +
    +
    +        {`MATCH (c:Category {categoryName:"Produce"})<--(:Product)<--(s:Supplier)
    +RETURN DISTINCT s.companyName as ProduceSuppliers`}
    +      
    +
    Find the produce suppliers.
    +
    +
    +

    + :help cypher{' '} + MATCH +

    +
    , + +

    Customer Orders

    +

    + Northwind customers place orders which may detail multiple products. + +

    +

    Load and index records

    +
    +
    +        {`LOAD CSV WITH HEADERS FROM "http://data.neo4j.com/northwind/customers.csv" AS row
    +CREATE (n:Customer)
    +SET n = row`}
    +      
    +
    +
    +
    +        {`LOAD CSV WITH HEADERS FROM "http://data.neo4j.com/northwind/orders.csv" AS row
    +CREATE (n:Order)
    +SET n = row`}
    +      
    +
    +
    +
    +        CREATE INDEX ON :Customer(customerID)
    +      
    +
    +
    +
    +        CREATE INDEX ON :Order(orderID)
    +      
    +
    +

    Create data relationships

    +
    +
    +        {`MATCH (c:Customer),(o:Order)
    +WHERE c.customerID = o.customerID
    +CREATE (c)-[:PURCHASED]->(o)`}
    +      
    + +
    +
    +

    + :help cypher{' '} + LOAD CSV +

    +
    , + +

    Customer Order Graph

    +

    + Notice that Order Details are always part of an Order and that they{' '} + relate the Order to a Product — they're a join table. Join tables + are always a sign of a data relationship, indicating shared information + between two other records. +

    +

    + Here, we'll directly promote each OrderDetail record into a relationship + in the graph. + +

    +

    Load and index records

    +
    +
    +        {`LOAD CSV WITH HEADERS FROM "http://data.neo4j.com/northwind/order-details.csv" AS row
    +MATCH (p:Product), (o:Order)
    +WHERE p.productID = row.productID AND o.orderID = row.orderID
    +CREATE (o)-[details:ORDERS]->(p)
    +SET details = row,
    +details.quantity = toInteger(row.quantity)`}
    +      
    + +
    +

    Query using patterns

    +
    +
    +        {`MATCH (cust:Customer)-[:PURCHASED]->(:Order)-[o:ORDERS]->(p:Product),
    +  (p)-[:PART_OF]->(c:Category {categoryName:"Produce"})
    +RETURN DISTINCT cust.contactName as CustomerName, SUM(o.quantity) AS TotalProductsPurchased`}
    +      
    +
    +
    +

    + :help cypher{' '} + LOAD CSV +

    +
    , + +

    Next steps

    + +
    +

    Reference

    +
      +
    • + + Full Northwind import example + +
    • +
    • + + Developer resources + +
    • +
    • + + Neo4j Cypher Manual + +
    • +
    +
    +] + +export default { title, category, slides } diff --git a/src/browser/documentation/sidebar-guides/styled.tsx b/src/browser/documentation/sidebar-guides/styled.tsx new file mode 100644 index 00000000000..19015856d08 --- /dev/null +++ b/src/browser/documentation/sidebar-guides/styled.tsx @@ -0,0 +1,22 @@ +import styled from 'styled-components' + +export const NoBulletsUl = styled.div` + list-style-type: none; +` +export const BulletsInsideLi = styled.li` + list-style-position: inside; +` +export const MarginTopLi = styled.li` + margin-top: 15px; +` + +export const MarginTop = styled.div` + margin-top: 5px; +` + +export const LinkContainer = styled.div` + margin-top: 15px; + display: flex; + justify-content: flex-end; + width: 100%; +` diff --git a/src/browser/components/drawer/index.ts b/src/browser/documentation/sidebar-guides/unfound.tsx similarity index 51% rename from src/browser/components/drawer/index.ts rename to src/browser/documentation/sidebar-guides/unfound.tsx index e50ce497f07..9810e16aa08 100644 --- a/src/browser/components/drawer/index.ts +++ b/src/browser/documentation/sidebar-guides/unfound.tsx @@ -18,24 +18,29 @@ * along with this program. If not, see . */ -import { - Drawer, - DrawerBody, - DrawerHeader, - DrawerSubHeader, - DrawerSection, - DrawerSectionBody, - DrawerToppedHeader, - DrawerFooter -} from './Drawer' +import { SidebarSlide } from 'browser/modules/Carousel/Slide' +import React from 'react' +const title = 'Not found' -export { - Drawer, - DrawerBody, - DrawerHeader, - DrawerSubHeader, - DrawerSection, - DrawerSectionBody, - DrawerToppedHeader, - DrawerFooter -} +const slides = [ + +

    Apologies, but there doesn't seem to be any content about that.

    +
    Try:
    +
      +
    • + :help - for general help about using Neo4j + Browser +
    • +
    • + :guide intro - to see a few available + guides +
    • +
    • + Neo4j Documentation - for detailed + information about Neo4j +
    • +
    +
    +] + +export default { title, slides } diff --git a/src/browser/icons/back-arrow.svg b/src/browser/icons/back-arrow.svg new file mode 100644 index 00000000000..5bebe3a6386 --- /dev/null +++ b/src/browser/icons/back-arrow.svg @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/src/browser/icons/external-link.svg b/src/browser/icons/external-link.svg new file mode 100644 index 00000000000..1796a1d0cf8 --- /dev/null +++ b/src/browser/icons/external-link.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/browser/icons/monitor-play.svg b/src/browser/icons/monitor-play.svg new file mode 100644 index 00000000000..794fde86e03 --- /dev/null +++ b/src/browser/icons/monitor-play.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/browser/init.ts b/src/browser/init.ts index 0295669fc1b..adc90185e4e 100644 --- a/src/browser/init.ts +++ b/src/browser/init.ts @@ -26,6 +26,7 @@ import './styles/font-awesome.min.css' import './styles/fira-code.css' import './styles/open-sans.css' import './styles/util-classes.css' +import './styles/workaround-monaco-scroll-bug.css' import '@relate-by-ui/css/semantic/dist/relate-by.min.css' // non web env (just for tests) diff --git a/src/browser/modules/Carousel/Carousel.tsx b/src/browser/modules/Carousel/Carousel.tsx index d3016f8919d..b1b25929c20 100644 --- a/src/browser/modules/Carousel/Carousel.tsx +++ b/src/browser/modules/Carousel/Carousel.tsx @@ -46,7 +46,7 @@ export default function Carousel({ originFrameId, initialSlide = 1, slides = [] -}: any) { +}: any): JSX.Element { const [visibleSlide, setVisibleSlide] = useState(() => { if (initialSlide <= slides.length) { return initialSlide - 1 diff --git a/src/browser/modules/Carousel/Slide.tsx b/src/browser/modules/Carousel/Slide.tsx index 563ec950f5b..91ca4728e38 100644 --- a/src/browser/modules/Carousel/Slide.tsx +++ b/src/browser/modules/Carousel/Slide.tsx @@ -18,39 +18,68 @@ * along with this program. If not, see . */ import React from 'react' +// The reason we have this file is to define classnames +// used in our templates and externally defined guides import styles from './style.less' -import { StyledSlide } from './styled' - -const Slide = React.forwardRef(({ children, content, html }, ref) => { - if (children) { - return ( - - {children} - - ) - } +import { StyledSidebarSlide, StyledSlide } from './styled' - if (content) { - return ( - - {content} - - ) - } +type SlideBaseProps = { + children?: React.ReactNode + content?: JSX.Element + html?: string +} - if (html) { - return ( - - ) - } +type SlideProps = SlideBaseProps & { + isSidebarSlide?: boolean +} + +const Slide = React.forwardRef( + ( + { children, content, html, isSidebarSlide }: SlideProps, + ref: React.Ref + ) => { + const SlideComponent = isSidebarSlide ? StyledSidebarSlide : StyledSlide + + if (children) { + return ( + + {children} + + ) + } + + if (content) { + return ( + + {content} + + ) + } - return null -}) + if (html) { + return ( + + ) + } + + return null + } +) Slide.displayName = 'Slide' export default Slide + +const SidebarSlide = React.forwardRef( + (props: SlideBaseProps, ref: React.Ref) => ( + + ) +) + +SidebarSlide.displayName = 'SidebarSlide' + +export { SidebarSlide } diff --git a/src/browser/modules/Carousel/style.less b/src/browser/modules/Carousel/style.less index cd87d40ad4e..480f68f2c41 100644 --- a/src/browser/modules/Carousel/style.less +++ b/src/browser/modules/Carousel/style.less @@ -85,7 +85,7 @@ } &.runnable { cursor: pointer; - border: transparent; + border: 2px solid transparent; border-radius: 2px; background-color: rgba(0, 0, 0, 0.1); } @@ -309,6 +309,11 @@ font-variant-ligatures: none !important; } } + & a [data-populate], + [data-exec] { + cursor: pointer; + } + & pre { word-break: break-all; word-wrap: break-word; @@ -318,6 +323,7 @@ & code { color: #fd766e; border-radius: 2px; + background-color: transparent; a { color: #fd766e !important; @@ -424,9 +430,18 @@ margin: 6px 0 20px 0; overflow: hidden; } - } - .padding5 {padding: 5px;} - .padding30 {padding-top: 30px;} - .table-padding {padding-top: 10px;} + .padding5 { + padding: 5px; + } + .padding30 { + padding-top: 30px; + } + .table-padding { + padding-top: 10px; + } +} + +.undecorated { + list-style: none; } diff --git a/src/browser/modules/Carousel/styled.tsx b/src/browser/modules/Carousel/styled.tsx index 7937f228a40..d891bb6a48f 100644 --- a/src/browser/modules/Carousel/styled.tsx +++ b/src/browser/modules/Carousel/styled.tsx @@ -20,6 +20,7 @@ import styled from 'styled-components' import { bounceRight } from 'browser-styles/animations' +import { dark } from 'browser-styles/themes' export const StyledCarousel: any = styled.div` padding-bottom: 20px; @@ -48,9 +49,9 @@ export const StyledCarouselButtonContainer = styled.div` justify-content: center; position: absolute; bottom: 0; + left: 0; z-index: 10; border-top: ${props => props.theme.inFrameBorder}; - margin-left: -40px; height: 39px; width: 100%; @@ -289,6 +290,7 @@ export const StyledSlide = styled.div` } &.slide .key { background-color: ${props => props.theme.preBackground}; + color: ${props => props.theme.preText}; border-radius: 3px; font-size: 12px; display: inline-block; @@ -301,4 +303,139 @@ export const StyledSlide = styled.div` &.slide .teaser { background-color: ${props => props.theme.teaserCardBackground} !important; } + + &.slide input { + color: ${props => props.theme.inputText}; + } +` + +export const StyledSidebarSlide = styled.div.attrs({ + className: 'slide' /* added to get styling from less.css */ +})` + color: ${dark.primaryText}; + & p.lead, + .title, + .subtitle, + .content > p, + .table-help { + color: ${dark.primaryText} !important; + line-height: 1.3; + + th { + padding-right: 20px; + text-align: left; + } + + &--header { + th { + border-bottom: ${dark.topicBorder}; + font-size: 2rem; + padding: 15px 0 0 0; + } + + &:first-child { + th { + padding-top: 0; + } + } + } + + &--subheader { + th { + border-bottom: ${dark.topicBorder}; + font-size: 1.5rem; + padding: 10px 0 0 0; + } + + &:first-child { + th { + padding-top: 0; + } + } + } + + &--commands { + margin-top: 2rem; + + td { + padding: 3px 10px 3px 0; + } + } + + &--keys { + th { + border-bottom: ${dark.topicBorder}; + } + td { + padding: 3px 10px 3px 0; + } + } + } + & a { + color: ${dark.link}; + text-decoration: ${props => + props.theme.name === 'dark' ? 'underline' : 'none'}; + } + & kbd { + color: ${dark.primaryBackground} !important; /* inverted */ + background-color: ${dark.primaryText} !important; + } + & .content > pre { + background-color: ${dark.secondaryBackground}; + color: ${dark.preText}; + } + & pre.runnable { + background-color: ${dark.preBackground}; + color: ${dark.preText}; + } + & pre.content { + background-color: ${dark.secondaryBackground}; + color: ${dark.preText}; + } + & a[help-topic], + a[play-topic], + a[server-topic], + a[exec-topic] { + background-color: ${dark.topicBackground} !important; + color: #5ca6d9; + } + & button [help-topic], + button [play-topic], + button [server-topic], + button [exec-topic] { + background-color: ${dark.primaryButtonBackground}; + color: ${dark.primaryButtonText}; + } + &.slide .code { + background-color: transparent; + } + &.slide .key { + background-color: ${dark.preBackground}; + color: ${dark.preText}; + border-radius: 3px; + font-size: 12px; + display: inline-block; + padding: 0 6px; + } + + .has-carousel & { + overflow: visible; + } + &.slide .teaser { + background-color: ${dark.teaserCardBackground} !important; + } + &.slide hr { + border-top: ${dark.drawerSeparator}; + } + + &.slide ul, + &.slide ol { + list-style-position: outside; + padding-left: 1.2em; + margin-top: 0.5em; + } + + &.slide input { + color: ${dark.inputText}; + } ` diff --git a/src/browser/modules/ClickToCode/__snapshots__/index.test.tsx.snap b/src/browser/modules/ClickToCode/__snapshots__/index.test.tsx.snap index 00f13dac53b..75a97d01330 100644 --- a/src/browser/modules/ClickToCode/__snapshots__/index.test.tsx.snap +++ b/src/browser/modules/ClickToCode/__snapshots__/index.test.tsx.snap @@ -5,7 +5,7 @@ exports[`ClickToCode does not render if no children 1`] = `
    `; exports[`ClickToCode renders all children 1`] = `
    @@ -20,7 +20,7 @@ exports[`ClickToCode renders all children 1`] = ` exports[`ClickToCode renders children as code if no code is provided 1`] = `
    hellohi! @@ -30,7 +30,7 @@ exports[`ClickToCode renders children as code if no code is provided 1`] = ` exports[`ClickToCode renders code as the code when code is available 1`] = `
    hello diff --git a/src/browser/modules/DBMSInfo/DBMSInfo.tsx b/src/browser/modules/DBMSInfo/DBMSInfo.tsx index 89b7a86a63a..a63088f3c96 100644 --- a/src/browser/modules/DBMSInfo/DBMSInfo.tsx +++ b/src/browser/modules/DBMSInfo/DBMSInfo.tsx @@ -31,12 +31,16 @@ import { getGraphStyleData } from 'shared/modules/grass/grassDuck' import { LabelItems, RelationshipItems, PropertyItems } from './MetaItems' import { UserDetails } from './UserDetails' import DatabaseKernelInfo from './DatabaseKernelInfo' -import { Drawer, DrawerBody, DrawerHeader } from 'browser-components/drawer' +import { + Drawer, + DrawerBody, + DrawerHeader +} from 'browser-components/drawer/drawer-styled' import { DatabaseSelector } from './DatabaseSelector' import { getUseDb } from 'shared/modules/connections/connectionsDuck' import { getDatabases } from 'shared/modules/dbMeta/dbMetaDuck' -export function DBMSInfo(props: any) { +export function DBMSInfo(props: any): JSX.Element { const moreStep = 50 const [labelsMax, setLabelsMax] = useState(moreStep) const [relationshipsMax, setRelationshipsMax] = useState(moreStep) diff --git a/src/browser/modules/DBMSInfo/DatabaseKernelInfo.tsx b/src/browser/modules/DBMSInfo/DatabaseKernelInfo.tsx index 91ccd1040d1..4608e8fd6c5 100644 --- a/src/browser/modules/DBMSInfo/DatabaseKernelInfo.tsx +++ b/src/browser/modules/DBMSInfo/DatabaseKernelInfo.tsx @@ -39,7 +39,7 @@ import { DrawerSection, DrawerSectionBody, DrawerSubHeader -} from 'browser-components/drawer' +} from 'browser-components/drawer/drawer-styled' import { StyledTable, StyledKey, diff --git a/src/browser/modules/DBMSInfo/DatabaseSelector.tsx b/src/browser/modules/DBMSInfo/DatabaseSelector.tsx index ab62a3028ed..5b80e782004 100644 --- a/src/browser/modules/DBMSInfo/DatabaseSelector.tsx +++ b/src/browser/modules/DBMSInfo/DatabaseSelector.tsx @@ -24,7 +24,7 @@ import { DrawerSection, DrawerSubHeader, DrawerSectionBody -} from 'browser-components/drawer/index' +} from 'browser-components/drawer/drawer-styled' import { uniqBy } from 'lodash-es' import { escapeCypherIdentifier } from 'services/utils' diff --git a/src/browser/modules/DBMSInfo/MetaItems.tsx b/src/browser/modules/DBMSInfo/MetaItems.tsx index 347427a601c..d36622bc8c4 100644 --- a/src/browser/modules/DBMSInfo/MetaItems.tsx +++ b/src/browser/modules/DBMSInfo/MetaItems.tsx @@ -24,7 +24,7 @@ import { DrawerSubHeader, DrawerSection, DrawerSectionBody -} from 'browser-components/drawer' +} from 'browser-components/drawer/drawer-styled' import { StyledLabel, StyledRelationship, diff --git a/src/browser/modules/DBMSInfo/UserDetails.tsx b/src/browser/modules/DBMSInfo/UserDetails.tsx index d61051038b6..b44015caef4 100644 --- a/src/browser/modules/DBMSInfo/UserDetails.tsx +++ b/src/browser/modules/DBMSInfo/UserDetails.tsx @@ -25,7 +25,7 @@ import { DrawerSubHeader, DrawerSection, DrawerSectionBody -} from 'browser-components/drawer' +} from 'browser-components/drawer/drawer-styled' import { StyledTable, StyledKey, StyledValue, Link } from './styled' export class UserDetails extends Component { diff --git a/src/browser/modules/DBMSInfo/__snapshots__/MetaItems.test.tsx.snap b/src/browser/modules/DBMSInfo/__snapshots__/MetaItems.test.tsx.snap index bd4bdb789cd..6a30703065f 100644 --- a/src/browser/modules/DBMSInfo/__snapshots__/MetaItems.test.tsx.snap +++ b/src/browser/modules/DBMSInfo/__snapshots__/MetaItems.test.tsx.snap @@ -3,15 +3,15 @@ exports[`LabelItems renders empty 1`] = `
    Node Labels

    There are no labels in database @@ -24,30 +24,30 @@ exports[`LabelItems renders empty 1`] = ` exports[`LabelItems renders labels 1`] = `

    Node Labels
    *
    MyLabel
    MyLabel2 @@ -60,15 +60,15 @@ exports[`LabelItems renders labels 1`] = ` exports[`PropertyItems renders empty 1`] = `
    Property Keys

    There are no properties in database @@ -81,24 +81,24 @@ exports[`PropertyItems renders empty 1`] = ` exports[`PropertyItems renders properties 1`] = `

    Property Keys
    prop1
    prop2 @@ -111,15 +111,15 @@ exports[`PropertyItems renders properties 1`] = ` exports[`RelationshipItems renders empty 1`] = `
    Relationship Types

    No relationships in database @@ -132,30 +132,30 @@ exports[`RelationshipItems renders empty 1`] = ` exports[`RelationshipItems renders relationshipTypes 1`] = `

    Relationship Types
    *
    MY_TYPE
    MY_TYPE2 diff --git a/src/browser/modules/Docs/Docs.tsx b/src/browser/modules/Docs/Docs.tsx index 331a4f2f391..951a4c826ae 100644 --- a/src/browser/modules/Docs/Docs.tsx +++ b/src/browser/modules/Docs/Docs.tsx @@ -26,17 +26,29 @@ import Slide from '../Carousel/Slide' import MdxSlide from './MDX/MdxSlide' import { splitMdxSlides } from './MDX/splitMdx' +type DocsProps = { + slides?: JSX.Element[] | null + content?: JSX.Element | null + html?: string + mdx?: string + initialSlide?: number + onSlide?: Function + lastUpdate?: number + originFrameId?: string + withDirectives?: true +} + export default function Docs({ slides, content, html, mdx, - withDirectives, initialSlide, onSlide, originFrameId, + withDirectives = true, lastUpdate -}: any) { +}: DocsProps): JSX.Element | null { const [stateSlides, setStateSlides] = useState([]) useEffect(() => { @@ -68,15 +80,13 @@ export default function Docs({ return } - if (withDirectives) { - slide = - } + slide = setStateSlides([slide]) if (onSlide) { onSlide({ hasPrev: false, hasNext: false, slideIndex: 0 }) } - }, [slides, content, html, withDirectives, lastUpdate]) + }, [slides, content, html, lastUpdate]) if (stateSlides.length > 1) { return ( diff --git a/src/browser/modules/Docs/MDX/MdxSlide.tsx b/src/browser/modules/Docs/MDX/MdxSlide.tsx index 739b0ed27ac..68310e0b269 100644 --- a/src/browser/modules/Docs/MDX/MdxSlide.tsx +++ b/src/browser/modules/Docs/MDX/MdxSlide.tsx @@ -55,8 +55,8 @@ const MdxRow = ({ row = '' }) => ( ) -const MdxSlide = ({ mdx = '' }) => ( - +const MdxSlide = ({ mdx = '', isSidebarSlide = false }) => ( + {splitMdxRows(mdx).map((row, index) => ( diff --git a/src/browser/modules/Editor/Monaco.tsx b/src/browser/modules/Editor/Monaco.tsx index f233f414375..f779ceba6d6 100644 --- a/src/browser/modules/Editor/Monaco.tsx +++ b/src/browser/modules/Editor/Monaco.tsx @@ -40,7 +40,7 @@ import { Bus } from 'suber' import { NEO4J_BROWSER_USER_ACTION_QUERY } from 'services/bolt/txMetadata' import { CYPHER_REQUEST } from 'shared/modules/cypher/cypherDuck' -import { NotificationPosition, QueryResult } from 'neo4j-driver' +import { QueryResult } from 'neo4j-driver' const shouldCheckForHints = (code: string) => code.trim().length > 0 && diff --git a/src/browser/modules/GuideCarousel/GuideCarousel.tsx b/src/browser/modules/GuideCarousel/GuideCarousel.tsx new file mode 100644 index 00000000000..18d3a8c82b7 --- /dev/null +++ b/src/browser/modules/GuideCarousel/GuideCarousel.tsx @@ -0,0 +1,145 @@ +/* + * 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, { useEffect } from 'react' +import Directives from 'browser-components/Directives' +import { CarouselButton } from 'browser-components/buttons' +import { + SlidePreviousIcon, + SlideNextIcon +} from 'browser-components/icons/Icons' +import { + GuideButtonContainers, + CarouselIndicatorActive, + CarouselIndicatorInactive, + StyledCarousel, + StyledCarouselButtonContainer, + StyledCarouselButtonContainerInner, + StyledCarouselCount, + StyledUl, + GuideNavButton +} from '../Sidebar/styled' + +type GuideCarouselProps = { + slides: JSX.Element[] + currentSlideIndex: number + scrollToTop?: () => void + gotoSlide: (slideIndex: number) => void +} + +function GuidesCarousel({ + slides, + currentSlideIndex, + gotoSlide, + scrollToTop = () => undefined +}: GuideCarouselProps): JSX.Element { + const currentSlide = slides[currentSlideIndex] + const onFirstSlide = currentSlideIndex === 0 + const onLastSlide = currentSlideIndex === slides.length - 1 + function nextSlide() { + if (!onLastSlide) { + gotoSlide(currentSlideIndex + 1) + } + } + + function prevSlide() { + if (!onFirstSlide) { + gotoSlide(currentSlideIndex - 1) + } + } + + function onKeyUp(e: React.KeyboardEvent) { + if (e.key === 'ArrowLeft') { + prevSlide() + } + if (e.key === 'ArrowRight') { + nextSlide() + } + } + + useEffect(() => { + // As we progress in the slides, scroll to top + scrollToTop() + }, [scrollToTop, currentSlideIndex]) + + const moreThanOneSlide = slides.length > 1 + + return ( + + + {moreThanOneSlide && ( + <> + + + Previous + + + Next + + + + + + {`${currentSlideIndex + 1} / ${slides.length}`} + + + + + + {slides.map((_, i) => + i !== currentSlideIndex ? ( + gotoSlide(i)} + > + + + ) : ( + gotoSlide(i)} + > + + + ) + )} + + + + + + + + )} + + ) +} + +export default GuidesCarousel diff --git a/src/browser/modules/Sidebar/About.tsx b/src/browser/modules/Sidebar/About.tsx index e331c80297e..3e2dcbd5d1a 100644 --- a/src/browser/modules/Sidebar/About.tsx +++ b/src/browser/modules/Sidebar/About.tsx @@ -29,7 +29,7 @@ import { DrawerSection, DrawerSectionBody, DrawerFooter -} from 'browser-components/drawer' +} from 'browser-components/drawer/drawer-styled' import { getVersion, getEdition } from 'shared/modules/dbMeta/dbMetaDuck' function asChangeLogUrl(serverVersion: string): string | undefined { diff --git a/src/browser/modules/Sidebar/DocumentItems.tsx b/src/browser/modules/Sidebar/DocumentItems.tsx index 875da8b2d83..b210b4b6e41 100644 --- a/src/browser/modules/Sidebar/DocumentItems.tsx +++ b/src/browser/modules/Sidebar/DocumentItems.tsx @@ -17,16 +17,17 @@ * 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 { Action } from 'redux' import { DrawerSubHeader, DrawerSection, - DrawerSectionBody -} from 'browser-components/drawer' + DrawerSectionBody, + DrawerExternalLink +} from 'browser-components/drawer/drawer-styled' import { - StyledHelpLink, StyledHelpItem, StyledCommandListItem, StyledCommandNamePair, @@ -43,6 +44,7 @@ import { SavedScriptsExpandMenuRightIcon } from 'browser-components/icons/Icons' import { SavedScriptsFolderCollapseIcon } from 'browser-components/SavedScripts/styled' + const DrawerSubHeaderWithMargin = styled(DrawerSubHeader)` margin: 0 24px 0 24px; ` @@ -78,9 +80,7 @@ export const DocumentItems = ({ const listOfItems = items.map(item => 'url' in item ? ( - - {item.name} - + {item.name} ) : ( { > - - + ) diff --git a/src/browser/modules/Sidebar/GuidesDrawer.tsx b/src/browser/modules/Sidebar/GuidesDrawer.tsx new file mode 100644 index 00000000000..539c56c3e8d --- /dev/null +++ b/src/browser/modules/Sidebar/GuidesDrawer.tsx @@ -0,0 +1,94 @@ +/* + * 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, { useRef } from 'react' +import { connect } from 'react-redux' + +import { + getGuide, + startGuide, + Guide, + isDefaultGuide, + gotoSlide +} from 'shared/modules/guides/guidesDuck' +import { GlobalState } from 'shared/globalState' +import GuideCarousel from '../GuideCarousel/GuideCarousel' +import { BackIcon } from '../../components/icons/Icons' +import { + StyledGuidesDrawer, + GuideTitle, + BackIconContainer, + CarouselWrapper, + StyledGuidesDrawerHeader, + StyledDrawerSeparator +} from './styled' + +type GuidesDrawerProps = { + guide: Guide + backToAllGuides: () => void + gotoSlide: (slideIndex: number) => void +} + +function GuidesDrawer({ + guide, + backToAllGuides, + gotoSlide +}: GuidesDrawerProps): JSX.Element { + const scrollRef = useRef(null) + + return ( + + + {!isDefaultGuide(guide) && ( + + + + )} + Neo4j Browser Guides{' '} + + + {!isDefaultGuide(guide) && ( + {guide.title} + )} + + + scrollRef.current?.scrollIntoView({ block: 'start' }) + } + /> + + + ) +} + +const mapStateToProps = (state: GlobalState) => ({ guide: getGuide(state) }) +const mapDispatchToProps = (dispatch: any) => ({ + backToAllGuides: () => dispatch(startGuide()), + gotoSlide: (slideIndex: number) => dispatch(gotoSlide(slideIndex)) +}) +const ConnectedGuidesDrawer = connect( + mapStateToProps, + mapDispatchToProps +)(GuidesDrawer) + +export default ConnectedGuidesDrawer diff --git a/src/browser/modules/Sidebar/ProjectFiles.tsx b/src/browser/modules/Sidebar/ProjectFiles.tsx index ed3a007a65a..9636ef1ccb6 100644 --- a/src/browser/modules/Sidebar/ProjectFiles.tsx +++ b/src/browser/modules/Sidebar/ProjectFiles.tsx @@ -23,7 +23,7 @@ import { connect } from 'react-redux' import { useMutation } from '@apollo/client' import { getProjectId } from 'shared/modules/app/appDuck' -import { Drawer, DrawerHeader } from 'browser-components/drawer' +import { Drawer, DrawerHeader } from 'browser-components/drawer/drawer-styled' import ProjectFilesScripts, { ProjectFilesError } from '../../components/ProjectFiles/ProjectsFilesScripts' diff --git a/src/browser/modules/Sidebar/Settings.tsx b/src/browser/modules/Sidebar/Settings.tsx index 449283cafef..3236c8ccc36 100644 --- a/src/browser/modules/Sidebar/Settings.tsx +++ b/src/browser/modules/Sidebar/Settings.tsx @@ -27,7 +27,7 @@ import { DrawerSection, DrawerSectionBody, DrawerSubHeader -} from 'browser-components/drawer' +} from 'browser-components/drawer/drawer-styled' import { RadioSelector, CheckboxSelector } from 'browser-components/Form' import { StyledSetting, diff --git a/src/browser/modules/Sidebar/Sidebar.tsx b/src/browser/modules/Sidebar/Sidebar.tsx index 38b8dacab66..45971ac93b7 100644 --- a/src/browser/modules/Sidebar/Sidebar.tsx +++ b/src/browser/modules/Sidebar/Sidebar.tsx @@ -18,17 +18,19 @@ * along with this program. If not, see . */ -import React, { ReactFragment, ReactElement } from 'react' +import React from 'react' import { connect } from 'react-redux' import DatabaseDrawer from '../DBMSInfo/DBMSInfo' import DocumentsDrawer from './Documents' import AboutDrawer from './About' import SettingsDrawer from './Settings' +import GuidesDrawer from './GuidesDrawer' import Favorites from './favorites' import StaticScripts from './static-scripts' import ProjectFilesDrawer from './ProjectFiles' import TabNavigation, { - NavItem + NavItem, + STANDARD_DRAWER_WIDTH } from 'browser-components/TabNavigation/Navigation' import BrowserSync from '../Sync/BrowserSync' import { GlobalState } from 'shared/globalState' @@ -48,10 +50,11 @@ import { CloudSyncIcon, SettingsIcon, AboutIcon, - ProjectFilesIcon + ProjectFilesIcon, + GuidesDrawerIcon } from 'browser-components/icons/Icons' import { getCurrentDraft } from 'shared/modules/sidebar/sidebarDuck' -import { DrawerHeader } from 'browser-components/drawer' +import { DrawerHeader } from 'browser-components/drawer/drawer-styled' interface SidebarProps { openDrawer: string @@ -78,7 +81,7 @@ const Sidebar = ({ { name: 'DBMS', title: 'Database Information', - icon: function dbIcon(isOpen: boolean): ReactElement { + icon: function dbIcon(isOpen: boolean): JSX.Element { return ( }, - content: function FavoritesDrawer(): ReactFragment { + content: function FavoritesDrawer(): JSX.Element { return ( - <> +
    Favorites {showStaticScripts && } - +
    ) } }, @@ -110,7 +113,7 @@ const Sidebar = ({ { name: 'Project Files', title: 'Project Files', - icon: function projectFilesIcon(isOpen: boolean): ReactElement { + icon: function projectFilesIcon(isOpen: boolean): JSX.Element { return }, content: function ProjectDrawer(): JSX.Element { @@ -118,14 +121,22 @@ const Sidebar = ({ } } ] - : []) + : []), + { + name: 'Guides', + title: 'Guides', + icon: function guidesDrawerIcon(isOpen: boolean): JSX.Element { + return + }, + content: GuidesDrawer + } ] const bottomNavItemsList: NavItem[] = [ { name: 'Documents', title: 'Help & Resources', - icon: function docsIcon(isOpen: boolean): ReactElement { + icon: function docsIcon(isOpen: boolean): JSX.Element { return }, content: DocumentsDrawer, @@ -134,7 +145,7 @@ const Sidebar = ({ { name: 'Sync', title: 'Browser Sync', - icon: function syncIcon(isOpen: boolean): ReactElement { + icon: function syncIcon(isOpen: boolean): JSX.Element { return ( }, content: SettingsDrawer @@ -156,7 +167,7 @@ const Sidebar = ({ { name: 'About', title: 'About Neo4j', - icon: function aboutIcon(isOpen: boolean): ReactElement { + icon: function aboutIcon(isOpen: boolean): JSX.Element { return }, content: AboutDrawer diff --git a/src/browser/modules/Sidebar/__snapshots__/Settings.test.tsx.snap b/src/browser/modules/Sidebar/__snapshots__/Settings.test.tsx.snap index 6c305d2bfab..fe6733eeeb9 100644 --- a/src/browser/modules/Sidebar/__snapshots__/Settings.test.tsx.snap +++ b/src/browser/modules/Sidebar/__snapshots__/Settings.test.tsx.snap @@ -12,29 +12,29 @@ exports[`Settings renders with strange characters in display name 1`] = ` Browser Settings
    Test åäö settings
    diff --git a/src/browser/modules/Sidebar/styled.tsx b/src/browser/modules/Sidebar/styled.tsx index abe90af89aa..696389f5efd 100644 --- a/src/browser/modules/Sidebar/styled.tsx +++ b/src/browser/modules/Sidebar/styled.tsx @@ -19,7 +19,12 @@ */ import { Button } from 'semantic-ui-react' import styled from 'styled-components' -import { DrawerBody } from 'browser-components/drawer' +import { + DrawerBody, + DrawerBrowserCommand +} from 'browser-components/drawer/drawer-styled' +import { dark } from 'browser-styles/themes' +import { LARGE_DRAWER_WIDTH } from 'browser-components/TabNavigation/Navigation' export const StyledSetting = styled.div` padding-bottom: 15px; @@ -45,24 +50,6 @@ export const StyledSettingTextInput = styled.input` width: 192px; ` -export const StyledHelpLink = styled.a` - cursor: pointer; - text-decoration: none; - color: #68bdf4; - - &:active { - text-decoration: none; - } - - &:before { - display: inline-block; - content: ' '; - background-image: url("data:image/svg+xml;utf8, "); - height: 12px; - width: 12px; - margin-right: 7px; - } -` export const StyledHelpItem = styled.li` list-style-type: none; margin: 8px 24px 0 24px; @@ -100,21 +87,6 @@ export const StyledName = styled.div` margin-right: 5%; ` -export const StyledCommand = styled.div` - background-color: #2a2c33; - border-radius: 2px; - padding: 3px; - - color: #e36962; - font-family: Fira Code; - - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - - max-width: 45%; -` - export const StyledFullSizeDrawerBody = styled(DrawerBody)` padding: 0; ` @@ -149,3 +121,202 @@ export const StyledFeedbackButton = styled(Button)` margin: 0 0 25px 25px !important; min-height: fit-content !important; ` + +export const StyledCommand = styled(DrawerBrowserCommand)` + max-width: 45%; +` + +export const StyledCarousel = styled.div` + height: 100%; + padding-bottom: 20px; + width: 100%; + outline: none; + + .row { + margin-left: 0; + margin-right: 0; + } +` + +export const SlideContainer = styled.div` + padding: 0; + width: 100%; + display: inline-block; +` + +export const StyledCarouselButtonContainer = styled.div` + color: ${props => props.theme.secondaryButtonText}; + background-color: ${dark.secondaryBackground}; + display: flex; + align-items: center; + justify-content: center; + position: fixed; + bottom: 0; + left: 60px; + width: 500px; + + z-index: 10; + border-top: ${props => props.theme.drawerSeparator}; + height: 40px; +` + +export const StyledCarouselButtonContainerInner = styled.div` + display: flex; + align-items: center; + justify-content: center; + border-radius: 3px; + position: relative; +` + +export const StyledCarouselCount = styled.div` + display: flex; + align-items: center; + font-size: 10px; + font-weight: bold; + justify-content: flex-end; + border-radius: 3px; + min-width: 44px; + position: absolute; + right: 100%; + padding: 0; + margin-right: 10px; +` + +export const CarouselIndicator = styled.li` + margin: 0; + cursor: pointer; + border-radius: 50%; + border: 3px solid transparent; + position: relative; + z-index: 1; + + > span { + background-color: ${props => props.theme.secondaryButtonText}; + display: block; + border-radius: 3px; + width: 6px; + height: 6px; + opacity: 0.4; + transition: opacity 0.1s ease-in-out; + } + + &::before { + border-radius: 2px; + content: attr(aria-label); + color: ${props => props.theme.primaryBackground}; + background-color: ${props => props.theme.primaryText}; + position: absolute; + font-size: 12px; + font-weight: bold; + left: 50%; + min-width: 24px; + bottom: calc(100% + 5px); + pointer-events: none; + transform: translateX(-50%); + padding: 5px; + line-height: 1; + text-align: center; + z-index: 100; + visibility: hidden; + } + + &::after { + border: solid; + border-color: ${props => props.theme.primaryText} transparent; + border-width: 6px 6px 0 6px; + bottom: 5px; + content: ''; + left: 50%; + pointer-events: none; + position: absolute; + transform: translateX(-50%); + z-index: 100; + visibility: hidden; + } + + &:hover::before, + &:hover::after { + visibility: visible; + } +` +export const CarouselIndicatorInactive = styled(CarouselIndicator)` + &:hover > span { + opacity: 1; + } +` +export const CarouselIndicatorActive = styled(CarouselIndicator)` + > span { + opacity: 1; + } +` + +export const StyledUl = styled.ul` + list-style: none; + display: flex; + align-items: center; + justify-content: center; + margin: 0 !important; + padding-left: 0 !important; +` + +export const StyledGuidesDrawer = styled.div` + min-height: 100vh; + position: relative; + width: ${LARGE_DRAWER_WIDTH}px; + /* width is set to avoid squashing during opening animation */ +` + +export const StyledGuidesDrawerHeader = styled.h4` + color: ${props => props.theme.primaryHeaderText}; + background-color: ${props => props.theme.drawerBackground}; + font-size: 18px; + padding: 25px 0 0 25px; + font-weight: bold; + -webkit-font-smoothing: antialiased; + text-shadow: rgba(0, 0, 0, 0.4) 0px 1px 0px; + font-family: ${props => props.theme.drawerHeaderFontFamily}; + cursor: pointer; +` + +export const GuideTitle = styled.div` + overflow: hidden; + text-overflow: ellipsis; + font-size: 2em; + line-height: normal; + padding: 0 18px; + margin-bottom: 10px; +` + +export const BackIconContainer = styled.span` + cursor: pointer; + margin-right: 5px; +` +export const CarouselWrapper = styled.div` + padding: 0 18px; +` + +export const GuideButtonContainers = styled.div` + margin-top: 10px; + display: flex; + justify-content: space-between; + padding-right: 18px; + margin-bottom: 40px; +` + +export const GuideNavButton = styled.button` + border: none; + background-color: inherit; + font-size: 1.2em; + color: ${props => props.theme.link}; + outline: none; + padding: 5px 0; + + :disabled { + opacity: 0; + } +` + +export const StyledDrawerSeparator = styled.div` + margin: 0 18px 18px 18px; + border-bottom: 1px solid #424650; +` diff --git a/src/browser/modules/Stream/CypherFrame/__snapshots__/ErrorsView.test.tsx.snap b/src/browser/modules/Stream/CypherFrame/__snapshots__/ErrorsView.test.tsx.snap index e0292585916..7bc9d63da6d 100644 --- a/src/browser/modules/Stream/CypherFrame/__snapshots__/ErrorsView.test.tsx.snap +++ b/src/browser/modules/Stream/CypherFrame/__snapshots__/ErrorsView.test.tsx.snap @@ -6,11 +6,11 @@ exports[`ErrorsViews ErrorsStatusbar displays error 1`] = ` class="sc-bdVaJa kwzqrW" > Test.Error: Test error description @@ -26,42 +26,42 @@ exports[`ErrorsViews ErrorsView displays nothing if no errors 1`] = `
    `; exports[`ErrorsViews ErrorsView displays procedure link if unknown procedure 1`] = `
    ERROR

    Neo.ClientError.Procedure.ProcedureNotFound

               not found
             
     List available procedures @@ -74,30 +74,30 @@ exports[`ErrorsViews ErrorsView displays procedure link if unknown procedure 1`] exports[`ErrorsViews ErrorsView does displays an error 1`] = `
    ERROR

    Test.Error

               Test error description
             
    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 bfb2316fbee..dba6bf4211a 100644 --- a/src/browser/modules/Stream/CypherFrame/__snapshots__/VisualizationView.test.tsx.snap +++ b/src/browser/modules/Stream/CypherFrame/__snapshots__/VisualizationView.test.tsx.snap @@ -137,7 +137,7 @@ exports[`Visualization renders with result and escapes any HTML 1`] = ` class="sc-cvbbAY ZoyPP faded zoom-in" > @@ -145,7 +145,7 @@ exports[`Visualization renders with result and escapes any HTML 1`] = ` class="sc-cvbbAY ZoyPP zoom-out" > diff --git a/src/browser/modules/Stream/HelpFrame.tsx b/src/browser/modules/Stream/HelpFrame.tsx index 7c73db4fb51..54b80fafcf4 100644 --- a/src/browser/modules/Stream/HelpFrame.tsx +++ b/src/browser/modules/Stream/HelpFrame.tsx @@ -46,7 +46,7 @@ const HelpFrame = ({ stack = [] }: any) => { setCurrentFrameIndex(currentFrameIndex + 1)} + onClick={() => setCurrentFrameIndex(index => index + 1)} > @@ -57,7 +57,7 @@ const HelpFrame = ({ stack = [] }: any) => { setCurrentFrameIndex(currentFrameIndex - 1)} + onClick={() => setCurrentFrameIndex(index => index - 1)} > diff --git a/src/browser/modules/Stream/PlayFrame.tsx b/src/browser/modules/Stream/PlayFrame.tsx index b8d6c45818c..2bb42f2f2cf 100644 --- a/src/browser/modules/Stream/PlayFrame.tsx +++ b/src/browser/modules/Stream/PlayFrame.tsx @@ -23,7 +23,7 @@ import { withBus } from 'react-suber' import { fetchGuideFromAllowlistAction } from 'shared/modules/commands/commandsDuck' import Docs from '../Docs/Docs' -import docs from '../../documentation' +import docs, { isPlayChapter } from '../../documentation' import FrameTemplate from '../Frame/FrameTemplate' import FrameAside from '../Frame/FrameAside' import { @@ -189,12 +189,12 @@ function generateContent( return { guide: ( ), hasCarousel: checkHtmlForSlides(stackFrame.result), @@ -239,10 +239,9 @@ function generateContent( stackFrame.cmd.trim() === ':play' ? ':play start' : stackFrame.cmd ) - const guide = chapters[guideName] || {} // Check if content exists locally - if (Object.keys(guide).length) { - const { content, title, subtitle, slides = null } = guide + if (isPlayChapter(guideName)) { + const { content, title, subtitle, slides = null } = chapters[guideName] return { guide: (
    @@ -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`] = ` @@ -89,46 +89,46 @@ exports[`SchemaFrame renders empty for Neo4j >= 4.0 1`] = ` >
    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`] = ` @@ -219,16 +219,16 @@ exports[`SchemaFrame renders results for Neo4j < 4.0 1`] = ` >
    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/Sync/BrowserSync.tsx b/src/browser/modules/Sync/BrowserSync.tsx index a12179dad8b..4c2eb4a60eb 100644 --- a/src/browser/modules/Sync/BrowserSync.tsx +++ b/src/browser/modules/Sync/BrowserSync.tsx @@ -52,7 +52,7 @@ import { DrawerSubHeader, DrawerSectionBody, DrawerToppedHeader -} from 'browser-components/drawer' +} from 'browser-components/drawer/drawer-styled' import { FormButton, SyncSignInButton } from 'browser-components/buttons' import { BinIcon } from 'browser-components/icons/Icons' import { diff --git a/src/browser/styles/fonts/neo4j-world.svg b/src/browser/styles/fonts/neo4j-world.svg index 570ece74002..151a19fee90 100644 --- a/src/browser/styles/fonts/neo4j-world.svg +++ b/src/browser/styles/fonts/neo4j-world.svg @@ -1,45 +1,82 @@ - + -Generated by IcoMoon - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/browser/styles/themes.ts b/src/browser/styles/themes.ts index 9c62ae376b9..fd7ab87e874 100644 --- a/src/browser/styles/themes.ts +++ b/src/browser/styles/themes.ts @@ -90,6 +90,7 @@ export const base = { frameBorder: 'none', inFrameBorder: '1px solid #e6e9ef', topicBorder: '1px solid #dadada', + drawerSeparator: '1px solid #424650', // Frame frameSidebarBackground: '#F8F9FB', diff --git a/src/browser/styles/workaround-monaco-scroll-bug.css b/src/browser/styles/workaround-monaco-scroll-bug.css new file mode 100644 index 00000000000..f92b8adf641 --- /dev/null +++ b/src/browser/styles/workaround-monaco-scroll-bug.css @@ -0,0 +1,3 @@ +body .monaco-aria-container { + margin: -1px; +} diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts index b2e417c0027..b38911b2f25 100644 --- a/src/shared/globalState.ts +++ b/src/shared/globalState.ts @@ -57,6 +57,7 @@ import { NAME as folders, Folder } from './modules/favorites/foldersDuck' import { NAME as commands } from './modules/commands/commandsDuck' import { NAME as udc, udcState } from './modules/udc/udcDuck' import { NAME as app } from './modules/app/appDuck' +import { NAME as guides, GuideState } from './modules/guides/guidesDuck' import { NAME as experimentalFeatures, initialState as experimentalFeaturesInitialState @@ -83,4 +84,5 @@ export interface GlobalState { [udc]: udcState [app]: Record [experimentalFeatures]: typeof experimentalFeaturesInitialState + [guides]: GuideState } diff --git a/src/shared/modules/commands/helpers/play.ts b/src/shared/modules/commands/helpers/play.ts index e412f74349b..a5a73a71dce 100644 --- a/src/shared/modules/commands/helpers/play.ts +++ b/src/shared/modules/commands/helpers/play.ts @@ -23,7 +23,7 @@ import { cleanHtml } from 'services/remoteUtils' import remote from 'services/remote' export const fetchRemoteGuide = (url: any, allowlist = null) => { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { if (!hostIsAllowed(url, allowlist)) { return reject( new Error('Hostname is not allowed according to server allowlist') diff --git a/src/shared/modules/currentUser/currentUserDuck.ts b/src/shared/modules/currentUser/currentUserDuck.ts index f93d44ddc25..ac1f3793e9c 100644 --- a/src/shared/modules/currentUser/currentUserDuck.ts +++ b/src/shared/modules/currentUser/currentUserDuck.ts @@ -56,7 +56,7 @@ export function getCurrentUser(state: any) { /** * Reducer */ -export default function user(state = initialState, action: any) { +export default function reducer(state = initialState, action: any) { switch (action.type) { case APP_START: return { ...initialState, ...state } diff --git a/src/shared/modules/editor/editorDuck.ts b/src/shared/modules/editor/editorDuck.ts index d4151892ebe..22666a709cf 100644 --- a/src/shared/modules/editor/editorDuck.ts +++ b/src/shared/modules/editor/editorDuck.ts @@ -65,6 +65,7 @@ export const NOT_SUPPORTED_URL_PARAM_COMMAND = // Supported commands const validCommandTypes: { [key: string]: (args: string[]) => string } = { play: args => `:play ${args.join(' ')}`, + guide: args => `:guide ${args.join(' ')}`, edit: args => args.join('\n'), param: args => `:param ${args.join(' ')}`, params: args => `:params ${args.join(' ')}` @@ -150,7 +151,8 @@ export const populateEditorFromUrlEpic: Epic = some$ => { // Play command is considered safe and can run automatically // When running the explicit command, also set flag to skip any implicit init commands - if (commandType === 'play') { + + if (['play', 'guide'].includes(commandType)) { return [ executeCommand(fullCommand, { source: commandSources.url }), { type: DISABLE_IMPLICIT_INIT_COMMANDS } diff --git a/src/shared/modules/guides/guidesDuck.ts b/src/shared/modules/guides/guidesDuck.ts new file mode 100644 index 00000000000..82b1b7e1423 --- /dev/null +++ b/src/shared/modules/guides/guidesDuck.ts @@ -0,0 +1,84 @@ +/* + * 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 docs from 'browser/documentation' +import { GlobalState } from 'shared/globalState' + +export const NAME = 'guides' +export const START_GUIDE = 'sidebar/START_GUIDE' +export const GOTO_SLIDE = 'sidebar/GOTO_SLIDE' + +export const isDefaultGuide = (guide: Guide): boolean => + guide.title === defaultGuide.title + +export const getGuide = (state: GlobalState): Guide => state[NAME].guide +export type Guide = { + currentSlide: number + title: string + slides: JSX.Element[] +} + +const defaultGuide: Guide = { + ...docs.guide.chapters.index, + currentSlide: 0 +} +export interface GuideState { + guide: Guide +} +const initialState: GuideState = { + guide: defaultGuide +} + +type GuideAction = StartAction | GotoSlideAction + +interface StartAction { + type: typeof START_GUIDE + guide: Guide +} + +interface GotoSlideAction { + type: typeof GOTO_SLIDE + slideIndex: number +} + +export default function reducer( + state = initialState, + action: GuideAction +): GuideState { + switch (action.type) { + case START_GUIDE: + return { ...state, guide: action.guide } + case GOTO_SLIDE: + return { + ...state, + guide: { ...state.guide, currentSlide: action.slideIndex } + } + default: + return state + } +} + +export function startGuide(guide: Guide = defaultGuide): StartAction { + return { type: START_GUIDE, guide } +} + +export function gotoSlide(slideIndex: number): GotoSlideAction { + return { type: GOTO_SLIDE, slideIndex } +} diff --git a/src/shared/modules/history/historyDuck.test.ts b/src/shared/modules/history/historyDuck.test.ts index 7486e556198..bb28a77dafb 100644 --- a/src/shared/modules/history/historyDuck.test.ts +++ b/src/shared/modules/history/historyDuck.test.ts @@ -35,10 +35,10 @@ describe('editor reducer', () => { // Given const helpAction = actions.addHistory(':help', 20) const historyAction = actions.addHistory(':history', 20) - const initalState = [':help'] + const initialState = [':help'] // When - const nextState = reducer(initalState, helpAction) + const nextState = reducer(initialState, helpAction) // Then expect(nextState).toEqual([':help']) @@ -51,18 +51,18 @@ describe('editor reducer', () => { }) test('takes editor.actionTypes.SET_MAX_HISTORY into account', () => { - const initalState = [':help', ':help', ':help'] + const initialState = [':help', ':help', ':help'] const helpAction = actions.addHistory(':history', 3) - const nextState = reducer(initalState, helpAction) + const nextState = reducer(initialState, helpAction) expect(nextState).toEqual([':history', ':help', ':help']) }) test('handles editor.actionTypes.CLEAR_HISTORY', () => { // Given - const initalState = [':emily'] + const initialState = [':emily'] const anAction = actions.addHistory(':elliot', 3) - const state = reducer(initalState, anAction) + const state = reducer(initialState, anAction) // When const nextState = reducer(state, actions.clearHistory()) diff --git a/src/shared/modules/sidebar/sidebarDuck.ts b/src/shared/modules/sidebar/sidebarDuck.ts index 6a23b62490d..7d982881164 100644 --- a/src/shared/modules/sidebar/sidebarDuck.ts +++ b/src/shared/modules/sidebar/sidebarDuck.ts @@ -41,6 +41,8 @@ export function getScriptDraftId(state: GlobalState): string | null { return state[NAME].scriptId || null } +export const GUIDE_DRAWER_ID = 'guides' + // SIDEBAR type DrawerId = | 'dbms' @@ -51,6 +53,7 @@ type DrawerId = | 'about' | 'project files' | 'settings' + | typeof GUIDE_DRAWER_ID | null export interface SidebarState { drawer: DrawerId | null diff --git a/src/shared/rootReducer.ts b/src/shared/rootReducer.ts index 65c4fe96dfb..27c363d8262 100644 --- a/src/shared/rootReducer.ts +++ b/src/shared/rootReducer.ts @@ -62,6 +62,7 @@ import commandsReducer, { } from 'shared/modules/commands/commandsDuck' import udcReducer, { NAME as udc } from 'shared/modules/udc/udcDuck' import appReducer, { NAME as app } from 'shared/modules/app/appDuck' +import guideReducer, { NAME as guides } from 'shared/modules/guides/guidesDuck' import experimentalFeaturesReducer, { NAME as experimentalFeatures } from 'shared/modules/experimentalFeatures/experimentalFeaturesDuck' @@ -86,5 +87,6 @@ export default { [commands]: commandsReducer, [udc]: udcReducer, [app]: appReducer, + [guides]: guideReducer, [experimentalFeatures]: experimentalFeaturesReducer } diff --git a/src/shared/services/commandInterpreterHelper.ts b/src/shared/services/commandInterpreterHelper.ts index af894228239..45e04771b47 100644 --- a/src/shared/services/commandInterpreterHelper.ts +++ b/src/shared/services/commandInterpreterHelper.ts @@ -17,6 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + import * as Sentry from '@sentry/react' import bolt from 'services/bolt/bolt' import * as frames from 'shared/modules/stream/streamDuck' @@ -35,6 +36,8 @@ import { useDb, getUseDb } from 'shared/modules/connections/connectionsDuck' +import { open } from 'shared/modules/sidebar/sidebarDuck' +import { startGuide } from 'shared/modules/guides/guidesDuck' import { getParams } from 'shared/modules/params/paramsDuck' import { getUserCapabilities } from 'shared/modules/features/featuresDuck' import { @@ -99,6 +102,7 @@ import { } from './commandUtils' import { unescapeCypherIdentifier } from './utils' import { getLatestFromFrameStack } from 'browser/modules/Stream/stream.utils' +import { resolveGuide } from './guideResolverHelper' const PLAY_FRAME_TYPES = ['play', 'play-remote'] @@ -467,6 +471,31 @@ const availableCommands = [ } } }, + { + name: 'guide', + match: (cmd: any) => /^guide(\s|$)/.test(cmd), + exec(action: any, put: any, store: any) { + const guideName = action.cmd.substr(':guide'.length).trim() + if (!guideName) { + put(startGuide()) + put(open('guides')) + return + } + + const initialSlide = tryGetRemoteInitialSlideFromUrl(action.cmd) + resolveGuide(guideName, store.getState()).then(({ slides, title }) => { + put( + startGuide({ + currentSlide: initialSlide, + title, + slides + }) + ) + + put(open('guides')) + }) + } + }, { name: 'play-remote', match: (cmd: any) => /^play(\s|$)https?/.test(cmd), diff --git a/src/shared/services/commandUtils.ts b/src/shared/services/commandUtils.ts index b2051d2ad4f..d994812ca73 100644 --- a/src/shared/services/commandUtils.ts +++ b/src/shared/services/commandUtils.ts @@ -103,7 +103,7 @@ const camelToSnake = (name: any, separator: any) => { .toLowerCase() } -export const transformCommandToHelpTopic = (inputStr: any) => +export const transformCommandToHelpTopic = (inputStr?: string): string => [inputStr || ''] .map(stripPound) .map(getHelpTopic) diff --git a/src/shared/services/exceptions.ts b/src/shared/services/exceptions.ts index 7d03eaa19b2..9ed68c99445 100644 --- a/src/shared/services/exceptions.ts +++ b/src/shared/services/exceptions.ts @@ -140,7 +140,7 @@ export function DatabaseUnavailableError({ return { type, code: type, - message: `Database "${dbName}" is unavailable, its status is "${dbMeta.status}."` + message: `Database "${dbName}" is unavailable, its status is "${dbMeta.status}".` } } diff --git a/src/shared/services/guideResolverHelper.tsx b/src/shared/services/guideResolverHelper.tsx new file mode 100644 index 00000000000..c865cf9d533 --- /dev/null +++ b/src/shared/services/guideResolverHelper.tsx @@ -0,0 +1,172 @@ +/* + * 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 MdxSlide from 'browser/modules/Docs/MDX/MdxSlide' +import Slide from 'browser/modules/Carousel/Slide' +import docs, { isGuideChapter } from 'browser/documentation' +import guideUnfound from 'browser/documentation/sidebar-guides/unfound' +import { + addProtocolsToUrlList, + extractAllowlistFromConfigString, + resolveAllowlistWildcard +} from './utils' +import { fetchRemoteGuide } from 'shared/modules/commands/helpers/play' +import { + getDefaultRemoteContentHostnameAllowlist, + getRemoteContentHostnameAllowlist +} from 'shared/modules/dbMeta/dbMetaDuck' +import { splitMdxSlides } from 'browser/modules/Docs/MDX/splitMdx' +import { + StyledCypherErrorMessage, + StyledDiv, + StyledErrorH4, + StyledHelpContent, + StyledHelpDescription, + StyledHelpFrame, + StyledPreformattedArea +} from 'browser/modules/Stream/styled' + +const { chapters } = docs.guide + +export async function resolveGuide( + guideName: string, + state: any +): Promise<{ slides: JSX.Element[]; title: string }> { + const isUrl = guideName.startsWith('http') + if (isUrl) { + return await resolveRemoteGuideFromURL(guideName, state) + } + + if (isGuideChapter(guideName)) { + return chapters[guideName] + } + + try { + return await resolveRemoteGuideFromName(guideName, state) + } catch (e) {} + + return guideUnfound +} + +function mdxTextToSlides(mdx: string): JSX.Element[] { + return splitMdxSlides(mdx).map((slide, index) => ( + // index is fine since we'll never move or delete slides + + )) +} + +function htmlTextToSlides(html: string): JSX.Element[] { + const tmpDiv = document.createElement('div') + tmpDiv.innerHTML = html + const htmlSlides = tmpDiv.getElementsByTagName('slide') + if (htmlSlides && htmlSlides.length) { + return Array.from(htmlSlides).map((slide, index) => ( + + )) + } + return [] +} + +async function resolveRemoteGuideFromURL( + guideName: string, + state: any +): Promise<{ slides: JSX.Element[]; title: string }> { + const url = guideName + const urlObject = new URL(url) + urlObject.href = url + const filenameExtension = + (urlObject.pathname.includes('.') && urlObject.pathname.split('.').pop()) || + 'html' + const allowlist = getRemoteContentHostnameAllowlist(state) + + try { + const remoteGuide = await fetchRemoteGuide(url, allowlist) + const titleRegexMatch = remoteGuide.match(/(.*?)<\/title>/) + const title = (titleRegexMatch && titleRegexMatch[1])?.trim() || guideName + if (['md', 'mdx'].includes(filenameExtension)) { + return { + slides: mdxTextToSlides(remoteGuide), + title + } + } else { + return { + slides: htmlTextToSlides(remoteGuide), + title + } + } + } catch (e) { + if (e.response && e.response.status === 404) { + return guideUnfound + } + return { + title: 'Error', + slides: [ + <Slide key="first" isSidebarSlide> + <StyledHelpFrame> + <StyledHelpContent> + <StyledHelpDescription> + <StyledCypherErrorMessage>ERROR</StyledCypherErrorMessage> + <StyledErrorH4>Remote guide error</StyledErrorH4> + </StyledHelpDescription> + <StyledDiv> + <StyledPreformattedArea> + {e.name}: {e.message} + </StyledPreformattedArea> + </StyledDiv> + </StyledHelpContent> + </StyledHelpFrame> + </Slide> + ] + } + } +} + +async function resolveRemoteGuideFromName( + guideName: string, + state: any +): Promise<{ slides: JSX.Element[]; title: string }> { + const allowlistStr = getRemoteContentHostnameAllowlist(state) + const allowlist = extractAllowlistFromConfigString(allowlistStr) + const defaultAllowlist = extractAllowlistFromConfigString( + getDefaultRemoteContentHostnameAllowlist(state) + ) + const resolvedWildcardAllowlist = resolveAllowlistWildcard( + allowlist, + defaultAllowlist + ) + const urlAllowlist = addProtocolsToUrlList(resolvedWildcardAllowlist) + const possibleGuidesUrls: string[] = urlAllowlist.map( + (url: string) => `${url}/${guideName}` + ) + + return possibleGuidesUrls + .reduce( + (promiseChain: Promise<any>, currentUrl: string) => + promiseChain + .catch(() => fetchRemoteGuide(currentUrl, allowlistStr)) + .then(r => Promise.resolve(r)), + Promise.reject(new Error()) + ) + .then(text => ({ + slides: htmlTextToSlides(text), + title: guideName + })) +} diff --git a/src/shared/services/remoteUtils.ts b/src/shared/services/remoteUtils.ts index 73e7880678f..e1fee32eccc 100644 --- a/src/shared/services/remoteUtils.ts +++ b/src/shared/services/remoteUtils.ts @@ -33,7 +33,7 @@ const removeOnHandlersFromHtml = (string: any) => '' ) -export function cleanHtml(string: any) { +export function cleanHtml(string: string): string { if (typeof string !== 'string') return string const stringWithoutHandlers = removeOnHandlersFromHtml(string) const stringWithoutScript = removeScriptTags(stringWithoutHandlers)
    Constraints
    ON ( book:Book ) ASSERT book.isbn IS UNIQUE