diff --git a/cypress/e2e/notebooks.cy.ts b/cypress/e2e/notebooks.cy.ts index d44555d42294e..4761c29d56827 100644 --- a/cypress/e2e/notebooks.cy.ts +++ b/cypress/e2e/notebooks.cy.ts @@ -7,16 +7,23 @@ describe('Notebooks', () => { 'loadSessionRecordingsList' ) }) + cy.fixture('api/session-recordings/recording.json').then((recording) => { cy.intercept('GET', /api\/projects\/\d+\/session_recordings\/.*\?.*/, { body: recording }).as( 'loadSessionRecording' ) }) + cy.fixture('api/notebooks/notebooks.json').then((notebook) => { cy.intercept('GET', /api\/projects\/\d+\/notebooks\//, { body: notebook }).as('loadNotebooksList') }) + cy.fixture('api/notebooks/notebook.json').then((notebook) => { cy.intercept('GET', /api\/projects\/\d+\/notebooks\/.*\//, { body: notebook }).as('loadNotebook') + // this means saving doesn't work but so what? + cy.intercept('PATCH', /api\/projects\/\d+\/notebooks\/.*\//, (req, res) => { + res.reply(req.body) + }).as('patchNotebook') }) cy.clickNavMenu('dashboards') @@ -53,4 +60,45 @@ describe('Notebooks', () => { cy.get('.ph-recording.NotebookNode').should('be.visible') cy.get('.NotebookRecordingTimestamp').should('contain.text', '0:00') }) + + it('Can add a number list', () => { + cy.get('li').contains('Notebooks').should('exist').click() + cy.get('[data-attr="new-notebook"]').click() + // we don't actually get a new notebook because the API is mocked + // so, press enter twice to "exit" the timestamp block we start in + cy.get('.NotebookEditor').type('{enter}{enter}') + cy.get('.NotebookEditor').type('{enter}') + cy.get('.NotebookEditor').type('1. the first') + cy.get('.NotebookEditor').type('{enter}') + // no need to type the number now. it should be inserted automatically + cy.get('.NotebookEditor').type('the second') + cy.get('.NotebookEditor').type('{enter}') + cy.get('ol').should('contain.text', 'the first') + cy.get('ol').should('contain.text', 'the second') + // the numbered list auto inserts the next list item + cy.get('.NotebookEditor ol li').should('have.length', 3) + }) + + it('Can add bold', () => { + cy.get('li').contains('Notebooks').should('exist').click() + cy.get('[data-attr="new-notebook"]').click() + // we don't actually get a new notebook because the API is mocked + // so, press enter twice to "exit" the timestamp block we start in + cy.get('.NotebookEditor').type('{enter}{enter}') + cy.get('.NotebookEditor').type('**bold**') + cy.get('.NotebookEditor p').last().should('contain.html', 'bold') + }) + + it('Can add bullet list', () => { + cy.get('li').contains('Notebooks').should('exist').click() + cy.get('[data-attr="new-notebook"]').click() + // we don't actually get a new notebook because the API is mocked + // so, press enter twice to "exit" the timestamp block we start in + cy.get('.NotebookEditor').type('{enter}{enter}') + cy.get('.NotebookEditor').type('* the first{enter}the second{enter}') + cy.get('ul').should('contain.text', 'the first') + cy.get('ul').should('contain.text', 'the second') + // the list auto inserts the next list item + cy.get('.NotebookEditor ul li').should('have.length', 3) + }) }) diff --git a/frontend/__snapshots__/scenes-app-notebooks--bullet-list.png b/frontend/__snapshots__/scenes-app-notebooks--bullet-list.png new file mode 100644 index 0000000000000..00ac16d82c920 Binary files /dev/null and b/frontend/__snapshots__/scenes-app-notebooks--bullet-list.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--headings.png b/frontend/__snapshots__/scenes-app-notebooks--headings.png new file mode 100644 index 0000000000000..4ddf6731b71b5 Binary files /dev/null and b/frontend/__snapshots__/scenes-app-notebooks--headings.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--notebooks-template-introduction.png b/frontend/__snapshots__/scenes-app-notebooks--notebooks-template-introduction.png new file mode 100644 index 0000000000000..b6466dd921cf7 Binary files /dev/null and b/frontend/__snapshots__/scenes-app-notebooks--notebooks-template-introduction.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--numbered-list.png b/frontend/__snapshots__/scenes-app-notebooks--numbered-list.png new file mode 100644 index 0000000000000..31e099837847c Binary files /dev/null and b/frontend/__snapshots__/scenes-app-notebooks--numbered-list.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist.png b/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist.png new file mode 100644 index 0000000000000..50e0e859358e0 Binary files /dev/null and b/frontend/__snapshots__/scenes-app-notebooks--recordings-playlist.png differ diff --git a/frontend/__snapshots__/scenes-app-notebooks--text-formats.png b/frontend/__snapshots__/scenes-app-notebooks--text-formats.png new file mode 100644 index 0000000000000..f1fea5b24cf66 Binary files /dev/null and b/frontend/__snapshots__/scenes-app-notebooks--text-formats.png differ diff --git a/frontend/src/lib/components/Cards/TextCard/TextCard.scss b/frontend/src/lib/components/Cards/TextCard/TextCard.scss index 0652dd7fa64bb..f88af17286e05 100644 --- a/frontend/src/lib/components/Cards/TextCard/TextCard.scss +++ b/frontend/src/lib/components/Cards/TextCard/TextCard.scss @@ -9,13 +9,13 @@ overflow-y: auto; ul { - list-style: disc; - padding-inline-start: 1.5em; + list-style-type: disc; + list-style-position: inside; } ol { - list-style: numeric; - padding-inline-start: 1.5em; + list-style-type: numeric; + list-style-position: inside; } img { diff --git a/frontend/src/lib/components/Cards/TextCard/TextCard.tsx b/frontend/src/lib/components/Cards/TextCard/TextCard.tsx index 9b2a9d8705ddf..5c54f515dbea8 100644 --- a/frontend/src/lib/components/Cards/TextCard/TextCard.tsx +++ b/frontend/src/lib/components/Cards/TextCard/TextCard.tsx @@ -24,15 +24,15 @@ interface TextCardProps extends React.HTMLAttributes, Resizeable showEditingControls?: boolean } -interface TextCardBodyProps extends Pick, 'style'> { +interface TextCardBodyProps extends Pick, 'style' | 'className'> { text: string closeDetails?: () => void } -export function TextContent({ text, closeDetails, style }: TextCardBodyProps): JSX.Element { +export function TextContent({ text, closeDetails, style, className }: TextCardBodyProps): JSX.Element { return ( // eslint-disable-next-line react/forbid-dom-props -
closeDetails?.()} style={style}> +
closeDetails?.()} style={style}> {text}
) diff --git a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.scss b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.scss index d15a0b710a0d5..389975e57915a 100644 --- a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.scss +++ b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.scss @@ -34,3 +34,15 @@ border: 1px solid var(--danger); } } + +.LemonTextArea--preview { + ul { + list-style-type: disc; + list-style-position: inside; + } + + ol { + list-style-type: decimal; + list-style-position: inside; + } +} diff --git a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.tsx b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.tsx index 9a85e20ba4829..4cfbf6bd7648f 100644 --- a/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.tsx +++ b/frontend/src/lib/lemon-ui/LemonTextArea/LemonTextArea.tsx @@ -138,7 +138,11 @@ export function LemonTextMarkdown({ value, onChange, ...editAreaProps }: LemonTe { key: 'preview', label: 'Preview', - content: value ? : Nothing to preview, + content: value ? ( + + ) : ( + Nothing to preview + ), }, ]} /> diff --git a/frontend/src/scenes/notebooks/Notebook/Notebook.scss b/frontend/src/scenes/notebooks/Notebook/Notebook.scss index c6906e0a76f8a..f272cacab53be 100644 --- a/frontend/src/scenes/notebooks/Notebook/Notebook.scss +++ b/frontend/src/scenes/notebooks/Notebook/Notebook.scss @@ -32,7 +32,15 @@ height: 0; } - > ul, + ul { + list-style-type: disc; + } + + ol { + list-style-type: decimal; + } + + ul, ol { padding-left: 1rem; @@ -40,11 +48,11 @@ p { margin-bottom: 0.2rem; } - } - } - > ul { - list-style: initial; + > p { + display: inline-block; + } + } } > pre { diff --git a/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx b/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx index b19845fe0b917..ecceb26e1ec93 100644 --- a/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx +++ b/frontend/src/scenes/notebooks/Notebook/Notebook.stories.tsx @@ -5,6 +5,193 @@ import { router } from 'kea-router' import { urls } from 'scenes/urls' import { App } from 'scenes/App' import notebook12345Json from './__mocks__/notebook-12345.json' +import { notebookTestTemplate } from './__mocks__/notebook-template-for-snapshot' +import { NotebookType } from '~/types' + +// a list of test cases to run, showing different types of content in notebooks +const testCases: Record = { + 'api/projects/:team_id/notebooks/text-formats': notebookTestTemplate('text-formats', [ + { + type: 'paragraph', + content: [ + { + type: 'text', + marks: [ + { + type: 'bold', + }, + ], + text: ' bold ', + }, + ], + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + marks: [ + { + type: 'italic', + }, + ], + text: 'italic', + }, + ], + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + marks: [ + { + type: 'bold', + }, + { + type: 'italic', + }, + ], + text: 'bold _and_ italic', + }, + ], + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + marks: [ + { + type: 'code', + }, + ], + text: 'code', + }, + ], + }, + ]), + 'api/projects/:team_id/notebooks/headings': notebookTestTemplate('headings', [ + { + type: 'heading', + attrs: { + level: 1, + }, + content: [ + { + type: 'text', + text: 'Heading 1', + }, + ], + }, + { + type: 'heading', + attrs: { + level: 2, + }, + content: [ + { + type: 'text', + text: 'Heading 2', + }, + ], + }, + { + type: 'heading', + attrs: { + level: 3, + }, + content: [ + { + type: 'text', + text: 'Heading 3', + }, + ], + }, + ]), + 'api/projects/:team_id/notebooks/numbered-list': notebookTestTemplate('numbered-list', [ + { + type: 'orderedList', + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'first item', + }, + ], + }, + ], + }, + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'second item', + }, + ], + }, + ], + }, + ], + }, + ]), + 'api/projects/:team_id/notebooks/bullet-list': notebookTestTemplate('bullet-list', [ + { + type: 'bulletList', + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'first item', + }, + ], + }, + ], + }, + { + type: 'listItem', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'second item', + }, + ], + }, + ], + }, + ], + }, + ]), + 'api/projects/:team_id/notebooks/recordings-playlist': notebookTestTemplate('recordings-playlist', [ + { + type: 'ph-recording-playlist', + attrs: { + height: null, + title: 'Session replays', + nodeId: '41faad12-499f-4a4b-95f7-3a36601317cc', + filters: + '{"session_recording_duration":{"type":"recording","key":"duration","value":3600,"operator":"gt"},"properties":[],"events":[],"actions":[],"date_from":"-7d","date_to":null}', + }, + }, + ]), +} const meta: Meta = { title: 'Scenes-App/Notebooks', @@ -15,6 +202,25 @@ const meta: Meta = { }, decorators: [ mswDecorator({ + post: { + 'api/projects/:team_id/query': { + clickhouse: + "SELECT nullIf(nullIf(events.`$session_id`, ''), 'null') AS session_id, any(events.properties) AS properties FROM events WHERE and(equals(events.team_id, 1), in(events.event, [%(hogql_val_0)s, %(hogql_val_1)s]), ifNull(in(session_id, [%(hogql_val_2)s]), 0), ifNull(greaterOrEquals(toTimeZone(events.timestamp, %(hogql_val_3)s), %(hogql_val_4)s), 0), ifNull(lessOrEquals(toTimeZone(events.timestamp, %(hogql_val_5)s), %(hogql_val_6)s), 0)) GROUP BY session_id LIMIT 100 SETTINGS readonly=2, max_execution_time=60, allow_experimental_object_type=True", + columns: ['session_id', 'properties'], + hogql: "SELECT properties.$session_id AS session_id, any(properties) AS properties FROM events WHERE and(in(event, ['$pageview', '$autocapture']), in(session_id, ['018a8a51-a39d-7b18-897f-94054eec5f61']), greaterOrEquals(timestamp, '2023-09-11 16:55:36'), lessOrEquals(timestamp, '2023-09-13 18:07:40')) GROUP BY session_id LIMIT 100", + query: "SELECT properties.$session_id as session_id, any(properties) as properties\n FROM events\n WHERE event IN ['$pageview', '$autocapture']\n AND session_id IN ['018a8a51-a39d-7b18-897f-94054eec5f61']\n -- the timestamp range here is only to avoid querying too much of the events table\n -- we don't really care about the absolute value, \n -- but we do care about whether timezones have an odd impact\n -- so, we extend the range by a day on each side so that timezones don't cause issues\n AND timestamp >= '2023-09-11 16:55:36'\n AND timestamp <= '2023-09-13 18:07:40'\n GROUP BY session_id", + results: [ + [ + '018a8a51-a39d-7b18-897f-94054eec5f61', + '{"$os":"Mac OS X","$os_version":"10.15.7","$browser":"Chrome","$device_type":"Desktop","$current_url":"http://localhost:8000/ingestion/platform","$host":"localhost:8000","$pathname":"/ingestion/platform","$browser_version":116,"$browser_language":"en-GB","$screen_height":982,"$screen_width":1512,"$viewport_height":827,"$viewport_width":1498,"$lib":"web","$lib_version":"1.78.2","$insert_id":"249xj40dkv7x9knp","$time":1694537723.201,"distinct_id":"uLI7S0z6rWQIKAjgXhdUBplxPYymuQqxH5QbJKe2wqr","$device_id":"018a8a51-a39c-78f9-a4e4-1183f059f7cc","$user_id":"uLI7S0z6rWQIKAjgXhdUBplxPYymuQqxH5QbJKe2wqr","is_demo_project":false,"$groups":{"project":"018a8a51-9ee3-0000-0369-ff1924dcba89","organization":"018a8a51-988e-0000-d3e6-477c7cc111f1","instance":"http://localhost:8000"},"$autocapture_disabled_server_side":false,"$active_feature_flags":[],"$feature_flag_payloads":{},"realm":"hosted-clickhouse","email_service_available":false,"slack_service_available":false,"$referrer":"http://localhost:8000/signup","$referring_domain":"localhost:8000","$event_type":"click","$ce_version":1,"token":"phc_awewGgfgakHbaSbprHllKajqoa6iP2nz7OAUou763ie","$session_id":"018a8a51-a39d-7b18-897f-94054eec5f61","$window_id":"018a8a51-a39d-7b18-897f-940673bea28c","$set_once":{"$initial_os":"Mac OS X","$initial_browser":"Chrome","$initial_device_type":"Desktop","$initial_current_url":"http://localhost:8000/ingestion/platform","$initial_pathname":"/ingestion/platform","$initial_browser_version":116,"$initial_referrer":"http://localhost:8000/signup","$initial_referring_domain":"localhost:8000"},"$sent_at":"2023-09-12T16:55:23.743000+00:00","$ip":"127.0.0.1","$group_0":"018a8a51-9ee3-0000-0369-ff1924dcba89","$group_1":"018a8a51-988e-0000-d3e6-477c7cc111f1","$group_2":"http://localhost:8000"}', + ], + ], + types: [ + ['session_id', 'Nullable(String)'], + ['properties', 'String'], + ], + }, + }, get: { 'api/projects/:team_id/notebooks': { count: 1, @@ -66,6 +272,76 @@ const meta: Meta = { ], }, 'api/projects/:team_id/notebooks/12345': notebook12345Json, + 'api/projects/:team_id/session_recordings': { + results: [ + { + id: '018a8a51-a39d-7b18-897f-94054eec5f61', + distinct_id: 'uLI7S0z6rWQIKAjgXhdUBplxPYymuQqxH5QbJKe2wqr', + viewed: true, + recording_duration: 4324, + active_seconds: 21, + inactive_seconds: 4302, + start_time: '2023-09-12T16:55:36.404000Z', + end_time: '2023-09-12T18:07:40.147000Z', + click_count: 3, + keypress_count: 0, + mouse_activity_count: 924, + console_log_count: 37, + console_warn_count: 7, + console_error_count: 9, + start_url: 'http://localhost:8000/replay/recent', + person: { + id: 1, + name: 'paul@posthog.com', + distinct_ids: [ + 'uLI7S0z6rWQIKAjgXhdUBplxPYymuQqxH5QbJKe2wqr', + '018a8a51-a39c-78f9-a4e4-1183f059f7cc', + ], + properties: { + email: 'paul@posthog.com', + $initial_os: 'Mac OS X', + $geoip_latitude: -33.8715, + $geoip_city_name: 'Sydney', + $geoip_longitude: 151.2006, + $geoip_time_zone: 'Australia/Sydney', + $initial_browser: 'Chrome', + $initial_pathname: '/', + $initial_referrer: 'http://localhost:8000/signup', + $geoip_postal_code: '2000', + $creator_event_uuid: '018a8a51-a39d-7b18-897f-9407e795547b', + $geoip_country_code: 'AU', + $geoip_country_name: 'Australia', + $initial_current_url: 'http://localhost:8000/', + $initial_device_type: 'Desktop', + $geoip_continent_code: 'OC', + $geoip_continent_name: 'Oceania', + $initial_geoip_latitude: -33.8715, + $initial_browser_version: 116, + $initial_geoip_city_name: 'Sydney', + $initial_geoip_longitude: 151.2006, + $initial_geoip_time_zone: 'Australia/Sydney', + $geoip_subdivision_1_code: 'NSW', + $geoip_subdivision_1_name: 'New South Wales', + $initial_referring_domain: 'localhost:8000', + $initial_geoip_postal_code: '2000', + $initial_geoip_country_code: 'AU', + $initial_geoip_country_name: 'Australia', + $initial_geoip_continent_code: 'OC', + $initial_geoip_continent_name: 'Oceania', + $initial_geoip_subdivision_1_code: 'NSW', + $initial_geoip_subdivision_1_name: 'New South Wales', + }, + created_at: '2023-09-12T16:55:20.736000Z', + uuid: '018a8a51-a3d3-0000-e8fa-94621f9ddd48', + }, + storage: 'clickhouse', + pinned_count: 0, + }, + ], + has_next: false, + version: 3, + }, + ...testCases, }, }), ], @@ -78,6 +354,41 @@ export function NotebooksList(): JSX.Element { return } +export function Headings(): JSX.Element { + useEffect(() => { + router.actions.push(urls.notebook('headings')) + }, []) + return +} + +export function TextFormats(): JSX.Element { + useEffect(() => { + router.actions.push(urls.notebook('text-formats')) + }, []) + return +} + +export function NumberedList(): JSX.Element { + useEffect(() => { + router.actions.push(urls.notebook('numbered-list')) + }, []) + return +} + +export function BulletList(): JSX.Element { + useEffect(() => { + router.actions.push(urls.notebook('bullet-list')) + }, []) + return +} + +export function RecordingsPlaylist(): JSX.Element { + useEffect(() => { + router.actions.push(urls.notebook('recordings-playlist')) + }, []) + return +} + export function TextOnlyNotebook(): JSX.Element { useEffect(() => { router.actions.push(urls.notebook('12345')) diff --git a/frontend/src/scenes/notebooks/Notebook/__mocks__/notebook-template-for-snapshot.ts b/frontend/src/scenes/notebooks/Notebook/__mocks__/notebook-template-for-snapshot.ts new file mode 100644 index 0000000000000..b87917836a5db --- /dev/null +++ b/frontend/src/scenes/notebooks/Notebook/__mocks__/notebook-template-for-snapshot.ts @@ -0,0 +1,34 @@ +import { NotebookType } from '~/types' +import { MOCK_DEFAULT_BASIC_USER } from 'lib/api.mock' +import { JSONContent } from 'scenes/notebooks/Notebook/utils' + +export const notebookTestTemplate = ( + title: string = 'Notebook for snapshots', + notebookJson: JSONContent[] +): NotebookType => ({ + short_id: 'template-introduction', + title: title, + created_at: '2023-06-02T00:00:00Z', + last_modified_at: '2023-06-02T00:00:00Z', + created_by: MOCK_DEFAULT_BASIC_USER, + last_modified_by: MOCK_DEFAULT_BASIC_USER, + version: 1, + content: { + type: 'doc', + content: [ + { + type: 'heading', + attrs: { + level: 1, + }, + content: [ + { + type: 'text', + text: title, + }, + ], + }, + ...notebookJson, + ], + }, +}) diff --git a/frontend/src/styles/utilities.scss b/frontend/src/styles/utilities.scss index 126d981427e89..745375f1c3f57 100644 --- a/frontend/src/styles/utilities.scss +++ b/frontend/src/styles/utilities.scss @@ -919,6 +919,13 @@ $decorations: underline, overline, line-through, no-underline; } } +.list-inside { + list-style-position: inside; +} +.list-outside { + list-style-position: outside; +} + .shadow { box-shadow: var(--shadow-elevation); }