From 704abbb47a94980160c2054124229afc3b269cde Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Thu, 6 Apr 2023 18:35:26 +0200 Subject: [PATCH] change(ui) - onboarding (#1124) * change(ui) - preferences header text change * change(ui) - onboarding - wip * change(ui) - onboarding - wip * change(ui) - onboarding - wip * change(ui) - onboarding - wip * change(ui) - onboarding - wip * change(ui) - onboarding - wip * change(ui) - onboarding - wip * change(ui) - onboarding - wip * change(ui) - onboarding - wip --- .../Client/CustomFields/CustomFieldForm.js | 113 ++++----- .../Client/CustomFields/CustomFields.js | 1 + .../Client/Integrations/Integrations.tsx | 95 ++++--- .../components/Client/Sites/NewSiteForm.js | 232 +++++++++--------- .../Users/components/UserForm/UserForm.tsx | 2 +- .../PreferencesView/PreferencesView.tsx | 2 +- .../app/components/Onboarding/Onboarding.js | 53 ---- .../app/components/Onboarding/Onboarding.tsx | 63 +++++ .../CircleNumber/circleNumber.module.css | 1 + .../IdentifyUsersTab/IdentifyUsersTab.js | 62 ----- .../IdentifyUsersTab/IdentifyUsersTab.tsx | 99 ++++++++ .../IdentifyUsersTab/{index.js => index.ts} | 0 .../InstallOpenReplayTab.js | 19 -- .../InstallOpenReplayTab.tsx | 49 ++++ .../{index.js => index.ts} | 0 .../IntegrationsTab/IntegrationsTab.js | 84 ------- .../IntegrationsTab/IntegrationsTab.tsx | 40 +++ .../IntegrationsTab/{index.js => index.ts} | 0 .../ManageUsersTab/ManageUsersTab.js | 23 -- .../ManageUsersTab/ManageUsersTab.tsx | 53 ++++ .../ManageUsersTab/{index.js => index.ts} | 0 .../components/MetadataList/MetadataList.js | 84 ++++--- .../OnboardingMenu/OnboardingMenu.js | 117 +++++---- .../OnboardingTabs/InstallDocs/InstallDocs.js | 7 +- .../OnboardingTabs/OnboardingTabs.js | 49 ---- .../OnboardingTabs/OnboardingTabs.tsx | 87 +++++++ .../ProjectCodeSnippet/ProjectCodeSnippet.js | 196 ++++++--------- .../ProjectFormButton/ProjectFormButton.js | 52 ++-- .../Onboarding/components/SideMenu.js | 39 --- .../Onboarding/components/SideMenu.tsx | 71 ++++++ .../Onboarding/components/withOnboarding.tsx | 57 +++++ .../shared/CodeSnippet/CodeSnippet.tsx | 5 +- .../app/components/shared/DocCard/DocCard.tsx | 37 +++ .../app/components/shared/DocCard/index.ts | 1 + frontend/app/components/ui/SVG.tsx | 6 +- frontend/app/components/ui/Toggler/Toggler.js | 2 +- frontend/app/styles/general.css | 6 + frontend/app/svg/icons/people.svg | 3 + frontend/app/svg/icons/person-border.svg | 4 + frontend/app/svg/icons/plug.svg | 3 + 40 files changed, 1040 insertions(+), 777 deletions(-) delete mode 100644 frontend/app/components/Onboarding/Onboarding.js create mode 100644 frontend/app/components/Onboarding/Onboarding.tsx delete mode 100644 frontend/app/components/Onboarding/components/IdentifyUsersTab/IdentifyUsersTab.js create mode 100644 frontend/app/components/Onboarding/components/IdentifyUsersTab/IdentifyUsersTab.tsx rename frontend/app/components/Onboarding/components/IdentifyUsersTab/{index.js => index.ts} (100%) delete mode 100644 frontend/app/components/Onboarding/components/InstallOpenReplayTab/InstallOpenReplayTab.js create mode 100644 frontend/app/components/Onboarding/components/InstallOpenReplayTab/InstallOpenReplayTab.tsx rename frontend/app/components/Onboarding/components/InstallOpenReplayTab/{index.js => index.ts} (100%) delete mode 100644 frontend/app/components/Onboarding/components/IntegrationsTab/IntegrationsTab.js create mode 100644 frontend/app/components/Onboarding/components/IntegrationsTab/IntegrationsTab.tsx rename frontend/app/components/Onboarding/components/IntegrationsTab/{index.js => index.ts} (100%) delete mode 100644 frontend/app/components/Onboarding/components/ManageUsersTab/ManageUsersTab.js create mode 100644 frontend/app/components/Onboarding/components/ManageUsersTab/ManageUsersTab.tsx rename frontend/app/components/Onboarding/components/ManageUsersTab/{index.js => index.ts} (100%) delete mode 100644 frontend/app/components/Onboarding/components/OnboardingTabs/OnboardingTabs.js create mode 100644 frontend/app/components/Onboarding/components/OnboardingTabs/OnboardingTabs.tsx delete mode 100644 frontend/app/components/Onboarding/components/SideMenu.js create mode 100644 frontend/app/components/Onboarding/components/SideMenu.tsx create mode 100644 frontend/app/components/Onboarding/components/withOnboarding.tsx create mode 100644 frontend/app/components/shared/DocCard/DocCard.tsx create mode 100644 frontend/app/components/shared/DocCard/index.ts create mode 100644 frontend/app/svg/icons/people.svg create mode 100644 frontend/app/svg/icons/person-border.svg create mode 100644 frontend/app/svg/icons/plug.svg diff --git a/frontend/app/components/Client/CustomFields/CustomFieldForm.js b/frontend/app/components/Client/CustomFields/CustomFieldForm.js index 1a8a582cd..4f2d1e278 100644 --- a/frontend/app/components/Client/CustomFields/CustomFieldForm.js +++ b/frontend/app/components/Client/CustomFields/CustomFieldForm.js @@ -1,68 +1,61 @@ -import React from 'react'; +import React, { useRef } from 'react'; import { connect } from 'react-redux'; import { edit, save } from 'Duck/customField'; -import { Form, Input, Button, Message } from 'UI'; +import { Form, Input, Button } from 'UI'; import styles from './customFieldForm.module.css'; -@connect( - (state) => ({ - field: state.getIn(['customFields', 'instance']), - saving: state.getIn(['customFields', 'saveRequest', 'loading']), - errors: state.getIn(['customFields', 'saveRequest', 'errors']), - }), - { - edit, - save, - } -) -class CustomFieldForm extends React.PureComponent { - setFocus = () => this.focusElement.focus(); - onChangeSelect = (event, { name, value }) => this.props.edit({ [name]: value }); - write = ({ target: { value, name } }) => this.props.edit({ [name]: value }); +const CustomFieldForm = ({ field, saving, errors, edit, save, onSave, onClose, onDelete }) => { + const focusElementRef = useRef(null); - render() { - const { field, errors } = this.props; - const exists = field.exists(); - return ( -
-

{exists ? 'Update' : 'Add'} Metadata Field

-
- - - { - this.focusElement = ref; - }} - name="key" - value={field.key} - onChange={this.write} - placeholder="Field Name" - maxLength={50} - /> - + const setFocus = () => focusElementRef.current.focus(); + const onChangeSelect = (event, { name, value }) => edit({ [name]: value }); + const write = ({ target: { value, name } }) => edit({ [name]: value }); -
-
- - -
+ const exists = field.exists(); - -
-
-
- ); - } -} + return ( +
+

{exists ? 'Update' : 'Add'} Metadata Field

+
+ + + + -export default CustomFieldForm; +
+
+ + +
+ + +
+
+
+ ); +}; + +const mapStateToProps = (state) => ({ + field: state.getIn(['customFields', 'instance']), + saving: state.getIn(['customFields', 'saveRequest', 'loading']), + errors: state.getIn(['customFields', 'saveRequest', 'errors']), +}); + +export default connect(mapStateToProps, { edit, save })(CustomFieldForm); diff --git a/frontend/app/components/Client/CustomFields/CustomFields.js b/frontend/app/components/Client/CustomFields/CustomFields.js index d5843425a..04c143e9f 100644 --- a/frontend/app/components/Client/CustomFields/CustomFields.js +++ b/frontend/app/components/Client/CustomFields/CustomFields.js @@ -29,6 +29,7 @@ function CustomFields(props) { props.save(currentSite.id, field).then((response) => { if (!response || !response.errors || response.errors.size === 0) { hideModal(); + toast.success('Metadata added successfully!'); } else { toast.error(response.errors[0]); } diff --git a/frontend/app/components/Client/Integrations/Integrations.tsx b/frontend/app/components/Client/Integrations/Integrations.tsx index 30d901dcf..96f3be099 100644 --- a/frontend/app/components/Client/Integrations/Integrations.tsx +++ b/frontend/app/components/Client/Integrations/Integrations.tsx @@ -30,6 +30,8 @@ import withPageTitle from 'HOCs/withPageTitle'; import PiniaDoc from './PiniaDoc'; import ZustandDoc from './ZustandDoc'; import MSTeams from './Teams'; +import DocCard from 'Shared/DocCard/DocCard'; +import cn from 'classnames'; interface Props { fetch: (name: string, siteId: string) => void; @@ -85,42 +87,48 @@ function Integrations(props: Props) {
{!hideHeader && Integrations
} />} {integrations.map((cat: any) => ( -
-
-

{cat.title}

- {cat.isProject && ( -
-
- +
+
+
+

{cat.title}

+ {cat.isProject && ( +
+
+ +
+ {loading && cat.isProject && }
- {loading && cat.isProject && } -
- )} -
-
{cat.description}
+ )} +
+
{cat.description}
-
- {cat.integrations.map((integration: any) => ( - - - onClick(integration, cat.title === 'Plugins' ? 500 : 350)} - hide={ - (integration.slug === 'github' && integratedList.includes('jira')) || - (integration.slug === 'jira' && integratedList.includes('github')) - } - /> - - - ))} +
+ {cat.integrations.map((integration: any) => ( + + + onClick(integration, cat.title === 'Plugins' ? 500 : 350)} + hide={ + (integration.slug === 'github' && integratedList.includes('jira')) || + (integration.slug === 'jira' && integratedList.includes('github')) + } + /> + + + ))} +
+ {cat.docs &&
{cat.docs()}
}
))}
@@ -182,6 +190,16 @@ const integrations = [ isProject: true, description: 'Sync your backend errors with sessions replays and see what happened front-to-back.', + docs: () => ( + + Sync your backend errors with sessions replays and see what happened front-to-back. + + ), integrations: [ { title: 'Sentry', slug: 'sentry', icon: 'integrations/sentry', component: }, { @@ -238,6 +256,17 @@ const integrations = [ title: 'Plugins', key: 3, isProject: true, + docs: () => ( + + Plugins capture your application’s store, monitor queries, track performance issues and even + assist your end user through live sessions. + + ), description: "Reproduce issues as if they happened in your own browser. Plugins help capture your application's store, HTTP requeets, GraphQL queries, and more.", integrations: [ diff --git a/frontend/app/components/Client/Sites/NewSiteForm.js b/frontend/app/components/Client/Sites/NewSiteForm.js index 0527668c3..d49577041 100644 --- a/frontend/app/components/Client/Sites/NewSiteForm.js +++ b/frontend/app/components/Client/Sites/NewSiteForm.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { connect } from 'react-redux'; import { Form, Input, Button, Icon } from 'UI'; import { save, edit, update, fetchList, remove } from 'Duck/site'; @@ -12,121 +12,129 @@ import { clearSearch as clearSearchLive } from 'Duck/liveSearch'; import { withStore } from 'App/mstore'; import { toast } from 'react-toastify'; -@connect( - (state) => ({ - site: state.getIn(['site', 'instance']), - sites: state.getIn(['site', 'list']), - siteList: state.getIn(['site', 'list']), - loading: state.getIn(['site', 'save', 'loading']) || state.getIn(['site', 'remove', 'loading']), - }), - { - save, - remove, - edit, - update, - pushNewSite, - fetchList, - setSiteId, - clearSearch, - clearSearchLive, - } -) -@withRouter -@withStore -export default class NewSiteForm extends React.PureComponent { - state = { - existsError: false, - }; - +const NewSiteForm = ({ + site, + loading, + save, + remove, + edit, + update, + pushNewSite, + fetchList, + setSiteId, + clearSearch, + clearSearchLive, + location: { pathname }, + onClose, + mstore, +}) => { + const [existsError, setExistsError] = useState(false); - componentDidMount() { - const { - location: { pathname }, - match: { - params: { siteId }, - }, - } = this.props; - if (pathname.includes('onboarding')) { - this.props.setSiteId(siteId); - } + useEffect(() => { + if (pathname.includes('onboarding')) { + setSiteId(site.id); } + }, []); - onSubmit = (e) => { - e.preventDefault(); - const { - site, - siteList, - location: { pathname }, - } = this.props; - - if (site.exists()) { - this.props.update(this.props.site, this.props.site.id).then((response) => { - if (!response || !response.errors || response.errors.size === 0) { - this.props.onClose(null); - this.props.fetchList(); - toast.success('Project updated successfully'); - } else { - toast.error(response.errors[0]); - } - }); + const onSubmit = (e) => { + e.preventDefault(); + + if (site.exists()) { + update(site, site.id).then((response) => { + if (!response || !response.errors || response.errors.size === 0) { + onClose(null); + fetchList(); + toast.success('Project updated successfully'); } else { - this.props.save(this.props.site).then((response) => { - if (!response || !response.errors || response.errors.size === 0) { - this.props.onClose(null); - this.props.clearSearch(); - this.props.clearSearchLive(); - this.props.mstore.initClient(); - toast.success('Project added successfully'); - } else { - toast.error(response.errors[0]); - } - }); + toast.error(response.errors[0]); } - }; - - remove = async (site) => { - if ( - await confirm({ - header: 'Projects', - confirmation: `Are you sure you want to delete this Project? We won't be able to record anymore sessions.`, - }) - ) { - this.props.remove(site.id).then(() => { - this.props.onClose(null); - }); + }); + } else { + save(site).then((response) => { + if (!response || !response.errors || response.errors.size === 0) { + onClose(null); + clearSearch(); + clearSearchLive(); + mstore.initClient(); + toast.success('Project added successfully'); + } else { + toast.error(response.errors[0]); } - }; - - edit = ({ target: { name, value } }) => { - this.setState({ existsError: false }); - this.props.edit({ [name]: value }); - }; - - render() { - const { site, loading } = this.props; - return ( -
-

{site.exists() ? 'Edit Project' : 'New Project'}

-
-
- - - - -
- - {site.exists() && ( - - )} -
- {this.state.existsError &&
{'Project exists already.'}
} -
-
-
- ); + }); } -} + }; + + const handleRemove = async () => { + if ( + await confirm({ + header: 'Projects', + confirmation: `Are you sure you want to delete this Project? We won't be able to record anymore sessions.`, + }) + ) { + remove(site.id).then(() => { + onClose(null); + }); + } + }; + + const handleEdit = ({ target: { name, value } }) => { + setExistsError(false); + edit({ [name]: value }); + }; + + return ( +
+

{site.exists() ? 'Edit Project' : 'New Project'}

+
+
+ + + + +
+ + {site.exists() && ( + + )} +
+ {existsError &&
{'Project exists already.'}
} +
+
+
+ ); +}; + +const mapStateToProps = (state) => ({ + site: state.getIn(['site', 'instance']), + siteList: state.getIn(['site', 'list']), + loading: state.getIn(['site', 'save', 'loading']) || state.getIn(['site', 'remove', 'loading']), +}); + +export default connect(mapStateToProps, { + save, + remove, + edit, + update, + pushNewSite, + fetchList, + setSiteId, + clearSearch, + clearSearchLive, +})(withRouter(withStore(NewSiteForm))); diff --git a/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx b/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx index 3cf44bb7f..c0e050a7c 100644 --- a/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx +++ b/frontend/app/components/Client/Users/components/UserForm/UserForm.tsx @@ -17,7 +17,7 @@ function UserForm(props: Props) { const { hideModal } = useModal(); const { userStore, roleStore } = useStore(); const isSaving = useObserver(() => userStore.saving); - const user: any = useObserver(() => userStore.instance); + const user: any = useObserver(() => userStore.instance || userStore.initUser()); const roles = useObserver(() => roleStore.list.filter(r => r.isProtected ? user.isSuperAdmin : true).map(r => ({ label: r.name, value: r.roleId }))); const onChangeCheckbox = (e: any) => { diff --git a/frontend/app/components/Header/PreferencesView/PreferencesView.tsx b/frontend/app/components/Header/PreferencesView/PreferencesView.tsx index 735fdeaa9..4087475c0 100644 --- a/frontend/app/components/Header/PreferencesView/PreferencesView.tsx +++ b/frontend/app/components/Header/PreferencesView/PreferencesView.tsx @@ -18,7 +18,7 @@ function PreferencesView(props: Props) {
- Updates are be applied at organization level. + Any changes will be put into effect across your organization.
); diff --git a/frontend/app/components/Onboarding/Onboarding.js b/frontend/app/components/Onboarding/Onboarding.js deleted file mode 100644 index e0f4fd973..000000000 --- a/frontend/app/components/Onboarding/Onboarding.js +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react' -import SideMenu from './components/SideMenu' -import { withRouter } from 'react-router-dom' -import { Switch, Route, Redirect } from 'react-router' -import { OB_TABS, onboarding as onboardingRoute } from 'App/routes' -import InstallOpenReplayTab from './components/InstallOpenReplayTab' -import IdentifyUsersTab from './components/IdentifyUsersTab' -import IntegrationsTab from './components/IntegrationsTab' -import ManageUsersTab from './components/ManageUsersTab' -import OnboardingNavButton from './components/OnboardingNavButton' -import * as routes from '../../routes' - -const withSiteId = routes.withSiteId; - -const Onboarding = (props) => { - const { match: { params: { activeTab } } } = props; - - const route = path => { - return withSiteId(onboardingRoute(path)); - } - - const renderActiveTab = () => ( - - } /> - } /> - } /> - } /> - - - ) - - return ( -
-
-
-
- -
-
- { activeTab && renderActiveTab()} -
-
-
-
-
- -
-
-
- ) -} - -export default withRouter(Onboarding); \ No newline at end of file diff --git a/frontend/app/components/Onboarding/Onboarding.tsx b/frontend/app/components/Onboarding/Onboarding.tsx new file mode 100644 index 000000000..f6b508446 --- /dev/null +++ b/frontend/app/components/Onboarding/Onboarding.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import SideMenu from './components/SideMenu'; +import { withRouter } from 'react-router-dom'; +import { Switch, Route, Redirect, RouteComponentProps } from 'react-router'; +import { OB_TABS, onboarding as onboardingRoute } from 'App/routes'; +import InstallOpenReplayTab from './components/InstallOpenReplayTab'; +import IdentifyUsersTab from './components/IdentifyUsersTab'; +import IntegrationsTab from './components/IntegrationsTab'; +import ManageUsersTab from './components/ManageUsersTab'; +import { withSiteId } from 'App/routes'; +interface Props { + match: { + params: { + activeTab: string; + siteId: string; + }; + }; + history: RouteComponentProps['history']; +} +const Onboarding = (props: Props) => { + const { + match: { + params: { activeTab, siteId }, + }, + } = props; + + const route = (path: string) => { + return withSiteId(onboardingRoute(path)); + }; + + const onMenuItemClick = (tab: string) => { + props.history.push(withSiteId(onboardingRoute(tab), siteId)); + } + + return ( +
+
+ +
+
+
+ + + + + + + +
+
+ {/*
+
+ +
+
*/} +
+ ); +}; + +export default withRouter(Onboarding); diff --git a/frontend/app/components/Onboarding/components/CircleNumber/circleNumber.module.css b/frontend/app/components/Onboarding/components/CircleNumber/circleNumber.module.css index aaea1eb41..09ea8aae2 100644 --- a/frontend/app/components/Onboarding/components/CircleNumber/circleNumber.module.css +++ b/frontend/app/components/Onboarding/components/CircleNumber/circleNumber.module.css @@ -9,4 +9,5 @@ color: white; font-size: 12px; margin-right: 10px; + flex-shrink: 0; } \ No newline at end of file diff --git a/frontend/app/components/Onboarding/components/IdentifyUsersTab/IdentifyUsersTab.js b/frontend/app/components/Onboarding/components/IdentifyUsersTab/IdentifyUsersTab.js deleted file mode 100644 index b4ba5b0a9..000000000 --- a/frontend/app/components/Onboarding/components/IdentifyUsersTab/IdentifyUsersTab.js +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react' -import CircleNumber from '../CircleNumber' -import MetadataList from '../MetadataList/MetadataList' -import { HighlightCode } from 'UI' - -export default function IdentifyUsersTab() { - return ( -
-
-

- 🕵️‍♂️ -
Identify Users
-

-
-
By User ID
-
- Call setUserID to identify your users when recording a session. The identity of the user can be changed, but OpenReplay will only keep the last communicated user ID. -
- -
- -
-
-
By adding metadata
-
- -
- Explicitly specify the metadata -
You can add up to 10 keys.
-
- - -
-
- -
-
- -
- Inject metadata when recording sessions -
Use the setMetadata method in your code to inject custom user data in the form of a key/value pair (string).
- -
-
-
-
- -
-
-
-
Why Identify Users?
-
Make it easy to search and filter replays by user id. OpenReplay allows you to associate your internal-user-id with the recording.
-
- -
-
What is Metadata?
-
Additional information about users can be provided with metadata (also known as traits or user variables). They take the form of key/value pairs, and are useful for filtering and searching for specific session replays.
-
-
-
- ) -} diff --git a/frontend/app/components/Onboarding/components/IdentifyUsersTab/IdentifyUsersTab.tsx b/frontend/app/components/Onboarding/components/IdentifyUsersTab/IdentifyUsersTab.tsx new file mode 100644 index 000000000..6c6083f03 --- /dev/null +++ b/frontend/app/components/Onboarding/components/IdentifyUsersTab/IdentifyUsersTab.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import CircleNumber from '../CircleNumber'; +import MetadataList from '../MetadataList/MetadataList'; +import { HighlightCode, Icon, Button } from 'UI'; +import DocCard from 'Shared/DocCard/DocCard'; +import withOnboarding, { WithOnboardingProps } from '../withOnboarding'; +import { OB_TABS } from 'App/routes'; + +interface Props extends WithOnboardingProps {} + +function IdentifyUsersTab(props: Props) { + return ( + <> +

+ 🕵️‍♂️ +
Identify Users
+

+
+
+
+
Identify users by user ID
+
+ Call setUserID to identify your users when + recording a session. +
+
+ +
+ + OpenReplay keeps the last communicated user ID. +
+ + +
+ +
+
+
Identify users by adding metadata
+

+ To identify users through metadata, you will have to explicitly specify your user + metadata so it can be injected during sessions. Follow the below steps +

+
+ + +
+ +
+
+ +
+ Inject metadata when recording sessions +
+ Use the setMetadata method in your code to + inject custom user data in the form of a key/value pair (string). +
+ +
+
+
+
+ +
+ + Make it easy to search and filter replays by user id. OpenReplay allows you to associate + your internal-user-id with the recording. + + + + Additional information about users can be provided with metadata (also known as traits + or user variables). They take the form of key/value pairs, and are useful for filtering + and searching for specific session replays. + +
+
+ +
+ + +
+ + ); +} + +export default withOnboarding(IdentifyUsersTab); diff --git a/frontend/app/components/Onboarding/components/IdentifyUsersTab/index.js b/frontend/app/components/Onboarding/components/IdentifyUsersTab/index.ts similarity index 100% rename from frontend/app/components/Onboarding/components/IdentifyUsersTab/index.js rename to frontend/app/components/Onboarding/components/IdentifyUsersTab/index.ts diff --git a/frontend/app/components/Onboarding/components/InstallOpenReplayTab/InstallOpenReplayTab.js b/frontend/app/components/Onboarding/components/InstallOpenReplayTab/InstallOpenReplayTab.js deleted file mode 100644 index 1d6288c4d..000000000 --- a/frontend/app/components/Onboarding/components/InstallOpenReplayTab/InstallOpenReplayTab.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' -import OnboardingTabs from '../OnboardingTabs' -import ProjectFormButton from '../ProjectFormButton' - -export default function InstallOpenReplayTab() { - return ( -
-

- 👋 -
- Hey there! Setup - -
-

-
OpenReplay can be installed via script or NPM package (recommended).
- -
- ) -} diff --git a/frontend/app/components/Onboarding/components/InstallOpenReplayTab/InstallOpenReplayTab.tsx b/frontend/app/components/Onboarding/components/InstallOpenReplayTab/InstallOpenReplayTab.tsx new file mode 100644 index 000000000..c09506a28 --- /dev/null +++ b/frontend/app/components/Onboarding/components/InstallOpenReplayTab/InstallOpenReplayTab.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import OnboardingTabs from '../OnboardingTabs'; +import ProjectFormButton from '../ProjectFormButton'; +import { Button, Icon } from 'UI'; +import withOnboarding from '../withOnboarding'; +import { WithOnboardingProps } from '../withOnboarding'; +import { OB_TABS } from 'App/routes'; + +interface Props extends WithOnboardingProps {} + +function InstallOpenReplayTab(props: Props) { + const { site } = props; + return ( + <> +

+
+ 👋 +
+ Hey there! Setup + +
+
+ + + Setup Guide + +

+
+
+ Setup OpenReplay through NPM package (recommended) or + script. +
+ +
+
+ +
+ + ); +} + +export default withOnboarding(InstallOpenReplayTab); diff --git a/frontend/app/components/Onboarding/components/InstallOpenReplayTab/index.js b/frontend/app/components/Onboarding/components/InstallOpenReplayTab/index.ts similarity index 100% rename from frontend/app/components/Onboarding/components/InstallOpenReplayTab/index.js rename to frontend/app/components/Onboarding/components/InstallOpenReplayTab/index.ts diff --git a/frontend/app/components/Onboarding/components/IntegrationsTab/IntegrationsTab.js b/frontend/app/components/Onboarding/components/IntegrationsTab/IntegrationsTab.js deleted file mode 100644 index db679f220..000000000 --- a/frontend/app/components/Onboarding/components/IntegrationsTab/IntegrationsTab.js +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react' -import { Icon } from 'UI' -import Integrations from '../../../Client/Integrations' - -function IntegrationItem({ icon, title, onClick= () => null }) { - return ( -
- -
{title}
-
- ) -} - -function IntegrationsTab() { - return ( -
-
-

- 🔌 -
Integrations
-

- - - {/*
-

- 🔌 -
Integrations
-

- - */} - - {/*
-
How are you handling store management?
-
- - - - -
-
- -
- -
-
How are you monitoring errors and crash reporting?
-
- - - - -
-
- -
- -
-
How are you logging backend errors?
-
- - - - - -
-
- -
*/} -
-
-
-
Why Use Plugins?
-
Reproduce issues as if they happened in your own browser. Plugins help capture your application’s store, HTTP requests, GraphQL queries and more.
-
- -
-
Why Use Integrations?
-
Sync your backend errors with sessions replays and see what happened front-to-back.
-
-
-
- ) -} - -export default IntegrationsTab diff --git a/frontend/app/components/Onboarding/components/IntegrationsTab/IntegrationsTab.tsx b/frontend/app/components/Onboarding/components/IntegrationsTab/IntegrationsTab.tsx new file mode 100644 index 000000000..dc549e74f --- /dev/null +++ b/frontend/app/components/Onboarding/components/IntegrationsTab/IntegrationsTab.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Button } from 'UI'; +import Integrations from 'App/components/Client/Integrations/Integrations'; +import withOnboarding, { WithOnboardingProps } from '../withOnboarding'; + +interface Props extends WithOnboardingProps {} +function IntegrationsTab(props: Props) { + return ( + <> +

+ 🔌 +
Integrations
+

+ + {/*
+
+
Why Use Plugins?
+
+ Reproduce issues as if they happened in your own browser. Plugins help capture your + application’s store, HTTP requests, GraphQL queries and more. +
+
+ +
+
Why Use Integrations?
+
+ Sync your backend errors with sessions replays and see what happened front-to-back. +
+
+
*/} +
+ +
+ + ); +} + +export default withOnboarding(IntegrationsTab); diff --git a/frontend/app/components/Onboarding/components/IntegrationsTab/index.js b/frontend/app/components/Onboarding/components/IntegrationsTab/index.ts similarity index 100% rename from frontend/app/components/Onboarding/components/IntegrationsTab/index.js rename to frontend/app/components/Onboarding/components/IntegrationsTab/index.ts diff --git a/frontend/app/components/Onboarding/components/ManageUsersTab/ManageUsersTab.js b/frontend/app/components/Onboarding/components/ManageUsersTab/ManageUsersTab.js deleted file mode 100644 index 1f4763247..000000000 --- a/frontend/app/components/Onboarding/components/ManageUsersTab/ManageUsersTab.js +++ /dev/null @@ -1,23 +0,0 @@ -import UsersView from 'App/components/Client/Users/UsersView' -import React from 'react' - -export default function ManageUsersTab() { - return ( -
-
-

- 👨‍💻 -
Invite Collaborators
-

- - -
-
-
-
Why Invite Collaborators?
-
Session replay is useful for all team members, from developers, testers and product managers to design, support and marketing folks. Invite them all and start improving your app now.
-
-
-
- ) -} diff --git a/frontend/app/components/Onboarding/components/ManageUsersTab/ManageUsersTab.tsx b/frontend/app/components/Onboarding/components/ManageUsersTab/ManageUsersTab.tsx new file mode 100644 index 000000000..3f888bea5 --- /dev/null +++ b/frontend/app/components/Onboarding/components/ManageUsersTab/ManageUsersTab.tsx @@ -0,0 +1,53 @@ +import UsersView from 'App/components/Client/Users/UsersView'; +import DocCard from 'Shared/DocCard/DocCard'; +import React from 'react'; +import { Button, Icon } from 'UI'; +import withOnboarding, { WithOnboardingProps } from '../withOnboarding'; +import { OB_TABS } from 'App/routes'; + +interface Props extends WithOnboardingProps {} + +function ManageUsersTab(props: Props) { + return ( + <> +

+ 👨‍💻 +
Invite Collaborators
+

+
+
+ +
+
+ +

Come together and unlock the potential collaborative improvements!

+

+ Session replays are useful to developers, designers, product managers and to everyone + on the product team. +

+
+
+
+
+ + +
+ + ); +} + +export default withOnboarding(ManageUsersTab); diff --git a/frontend/app/components/Onboarding/components/ManageUsersTab/index.js b/frontend/app/components/Onboarding/components/ManageUsersTab/index.ts similarity index 100% rename from frontend/app/components/Onboarding/components/ManageUsersTab/index.js rename to frontend/app/components/Onboarding/components/ManageUsersTab/index.ts diff --git a/frontend/app/components/Onboarding/components/MetadataList/MetadataList.js b/frontend/app/components/Onboarding/components/MetadataList/MetadataList.js index 76068fddf..154cf2aba 100644 --- a/frontend/app/components/Onboarding/components/MetadataList/MetadataList.js +++ b/frontend/app/components/Onboarding/components/MetadataList/MetadataList.js @@ -1,69 +1,67 @@ -import React, { useState, useEffect } from 'react' -import { Button, SlideModal, TagBadge } from 'UI' -import { connect } from 'react-redux' +import React, { useEffect } from 'react'; +import { Button, TagBadge } from 'UI'; +import { connect } from 'react-redux'; import { fetchList, save, remove } from 'Duck/customField'; import CustomFieldForm from '../../../Client/CustomFields/CustomFieldForm'; import { confirm } from 'UI'; +import { useModal } from 'App/components/Modal'; +import { toast } from 'react-toastify'; const MetadataList = (props) => { const { site, fields } = props; - const [showModal, setShowModal] = useState(false) + + const { showModal, hideModal } = useModal(); useEffect(() => { props.fetchList(site.id); - }, []) + }, []); const save = (field) => { - props.save(site.id, field).then(() => { - setShowModal(false) + props.save(site.id, field).then((response) => { + if (!response || !response.errors || response.errors.size === 0) { + hideModal(); + toast.success('Metadata added successfully!'); + } else { + toast.error(response.errors[0]); + } }); }; - + const openModal = () => { - setShowModal(!showModal); - } + showModal(, { right: true }); + }; const removeMetadata = async (field) => { - if (await confirm({ - header: 'Metadata', - confirmation: `Are you sure you want to remove?` - })) { + if ( + await confirm({ + header: 'Metadata', + confirmation: `Are you sure you want to remove?`, + }) + ) { props.remove(site.id, field.index); } - } + }; return (
- +
- { fields.map((f, index) => ( - removeMetadata(f) } - outline - /> - //
{f.key}
+ {fields.map((f, index) => ( + removeMetadata(f)} outline /> ))}
- - setShowModal(false) } onSave={save} /> - )} - onClose={ () => setShowModal(false) } - />
- ) -} + ); +}; -export default connect(state => ({ - site: state.getIn([ 'site', 'instance' ]), - fields: state.getIn(['customFields', 'list']).sortBy(i => i.index), - field: state.getIn(['customFields', 'instance']), - loading: state.getIn(['customFields', 'fetchRequest', 'loading']), -}), { fetchList, save, remove })(MetadataList) \ No newline at end of file +export default connect( + (state) => ({ + site: state.getIn(['site', 'instance']), + fields: state.getIn(['customFields', 'list']).sortBy((i) => i.index), + field: state.getIn(['customFields', 'instance']), + loading: state.getIn(['customFields', 'fetchRequest', 'loading']), + }), + { fetchList, save, remove } +)(MetadataList); diff --git a/frontend/app/components/Onboarding/components/OnboardingMenu/OnboardingMenu.js b/frontend/app/components/Onboarding/components/OnboardingMenu/OnboardingMenu.js index f7a6179f8..9e5bfa049 100644 --- a/frontend/app/components/Onboarding/components/OnboardingMenu/OnboardingMenu.js +++ b/frontend/app/components/Onboarding/components/OnboardingMenu/OnboardingMenu.js @@ -1,73 +1,96 @@ -import React from 'react' -import { Icon, SideMenuitem } from 'UI' -import cn from 'classnames' -import stl from './onboardingMenu.module.css' -import { OB_TABS, onboarding as onboardingRoute } from 'App/routes' -import { withRouter } from 'react-router-dom' -import * as routes from '../../../../routes' +import React from 'react'; +import { Icon, SideMenuitem } from 'UI'; +import cn from 'classnames'; +import stl from './onboardingMenu.module.css'; +import { OB_TABS, onboarding as onboardingRoute } from 'App/routes'; +import { withRouter } from 'react-router-dom'; +import * as routes from '../../../../routes'; const withSiteId = routes.withSiteId; -const MENU_ITEMS = [OB_TABS.INSTALLING, OB_TABS.IDENTIFY_USERS, OB_TABS.MANAGE_USERS, OB_TABS.INTEGRATIONS] +const MENU_ITEMS = [ + OB_TABS.INSTALLING, + OB_TABS.IDENTIFY_USERS, + OB_TABS.MANAGE_USERS, + OB_TABS.INTEGRATIONS, +]; const Item = ({ icon, text, completed, active, onClick }) => ( -
-
+
-
- { completed && - - } + {/* {completed && } */}
{text}
-) +); const OnboardingMenu = (props) => { - const { match: { params: { activeTab, siteId } }, history } = props; - const activeIndex = MENU_ITEMS.findIndex(i => i === activeTab); + const { + match: { + params: { activeTab, siteId }, + }, + history, + } = props; + const activeIndex = MENU_ITEMS.findIndex((i) => i === activeTab); const setTab = (tab) => { history.push(withSiteId(onboardingRoute(tab), siteId)); - } + }; return (
- { activeIndex === 0 && ( - - )} - { activeIndex > 0 && ( + + + + + <> - = 0} active={activeIndex === 0} onClick={() => setTab(MENU_ITEMS[0])} /> - = 1} active={activeIndex === 1} onClick={() => setTab(MENU_ITEMS[1])} /> - = 2} active={activeIndex === 2} onClick={() => setTab(MENU_ITEMS[2])} /> - = 3} active={activeIndex === 3} onClick={() => setTab(MENU_ITEMS[3])} /> + = 0} + active={activeIndex === 0} + onClick={() => setTab(MENU_ITEMS[0])} + /> + = 1} + active={activeIndex === 1} + onClick={() => setTab(MENU_ITEMS[1])} + /> + = 2} + active={activeIndex === 2} + onClick={() => setTab(MENU_ITEMS[2])} + /> + = 3} + active={activeIndex === 3} + onClick={() => setTab(MENU_ITEMS[3])} + /> - )}
- ) -} + ); +}; -export default withRouter(OnboardingMenu) +export default withRouter(OnboardingMenu); diff --git a/frontend/app/components/Onboarding/components/OnboardingTabs/InstallDocs/InstallDocs.js b/frontend/app/components/Onboarding/components/OnboardingTabs/InstallDocs/InstallDocs.js index d999397f1..ad1307888 100644 --- a/frontend/app/components/Onboarding/components/OnboardingTabs/InstallDocs/InstallDocs.js +++ b/frontend/app/components/Onboarding/components/OnboardingTabs/InstallDocs/InstallDocs.js @@ -41,7 +41,7 @@ function InstallDocs({ site }) { Install the npm package.
-
+
{installationCommand} @@ -67,7 +67,7 @@ function InstallDocs({ site }) {
{isSpa && ( -
+
If your website is a Single Page Application (SPA) use the below code:
@@ -79,7 +79,7 @@ function InstallDocs({ site }) { )} {!isSpa && ( -
+
Otherwise, if your web app is Server-Side-Rendered (SSR) (i.e. NextJS, NuxtJS) use this snippet:
@@ -92,7 +92,6 @@ function InstallDocs({ site }) {
-
See Documentation for the list of available options.
) } diff --git a/frontend/app/components/Onboarding/components/OnboardingTabs/OnboardingTabs.js b/frontend/app/components/Onboarding/components/OnboardingTabs/OnboardingTabs.js deleted file mode 100644 index 398b7d240..000000000 --- a/frontend/app/components/Onboarding/components/OnboardingTabs/OnboardingTabs.js +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import { Tabs } from 'UI'; -import ProjectCodeSnippet from './ProjectCodeSnippet'; -import InstallDocs from './InstallDocs'; - -const PROJECT = 'SCRIPT'; -const DOCUMENTATION = 'NPM'; -// const SEGMENT = 'SEGMENT'; -// const GOOGLE_TAG = 'GOOGLE TAG'; -const TABS = [ - { key: DOCUMENTATION, text: DOCUMENTATION }, - { key: PROJECT, text: PROJECT }, - // { key: SEGMENT, text: SEGMENT }, - // { key: GOOGLE_TAG, text: GOOGLE_TAG } -]; - -class TrackingCodeModal extends React.PureComponent { - state = { copied: false, changed: false, activeTab: DOCUMENTATION }; - - setActiveTab = (tab) => { - this.setState({ activeTab: tab }); - } - - renderActiveTab = () => { - switch (this.state.activeTab) { - case PROJECT: - return - case DOCUMENTATION: - return - } - return null; - } - - render() { - const { activeTab } = this.state; - return ( - <> - -
- { this.renderActiveTab() } -
- - ); - } -} - -export default TrackingCodeModal; \ No newline at end of file diff --git a/frontend/app/components/Onboarding/components/OnboardingTabs/OnboardingTabs.tsx b/frontend/app/components/Onboarding/components/OnboardingTabs/OnboardingTabs.tsx new file mode 100644 index 000000000..ff2c2a590 --- /dev/null +++ b/frontend/app/components/Onboarding/components/OnboardingTabs/OnboardingTabs.tsx @@ -0,0 +1,87 @@ +import React, { useState } from 'react'; +import { Tabs, Icon, CopyButton } from 'UI'; +import ProjectCodeSnippet from './ProjectCodeSnippet'; +import InstallDocs from './InstallDocs'; +import DocCard from 'Shared/DocCard/DocCard'; +import { useModal } from 'App/components/Modal'; +import UserForm from 'App/components/Client/Users/components/UserForm/UserForm'; + +const PROJECT = 'SCRIPT'; +const DOCUMENTATION = 'NPM'; +const TABS = [ + { key: DOCUMENTATION, text: DOCUMENTATION }, + { key: PROJECT, text: PROJECT }, +]; + +interface Props { + site: any; +} +const TrackingCodeModal = (props: Props) => { + const { site } = props; + const [activeTab, setActiveTab] = useState(DOCUMENTATION); + const { showModal } = useModal(); + + const showUserModal = () => { + showModal(, { right: true }); + }; + + const renderActiveTab = () => { + switch (activeTab) { + case PROJECT: + return ( +
+
+ +
+ +
+ + + Invite and Collaborate + + + +
+ {site.projectKey} + +
+
+ + + Google Tag Manager (GTM) + + + +
+
+ ); + case DOCUMENTATION: + return ( +
+
+ +
+ +
+ Invite and Collaborate +
+
+ ); + default: + return null; + } + }; + + return ( + <> + +
{renderActiveTab()}
+ + ); +}; + +export default TrackingCodeModal; diff --git a/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js b/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js index 837281e8f..5204d9c5d 100644 --- a/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js +++ b/frontend/app/components/Onboarding/components/OnboardingTabs/ProjectCodeSnippet/ProjectCodeSnippet.js @@ -1,13 +1,12 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react'; import { connect } from 'react-redux'; import { editGDPR, saveGDPR, init } from 'Duck/site'; -import copy from 'copy-to-clipboard'; -import { Checkbox } from 'UI'; +import { Checkbox, Toggler } from 'UI'; import GDPR from 'Types/site/gdpr'; -import cn from 'classnames' -import stl from './projectCodeSnippet.module.css' +import cn from 'classnames'; +import stl from './projectCodeSnippet.module.css'; import CircleNumber from '../../CircleNumber'; -import Select from 'Shared/Select' +import Select from 'Shared/Select'; import CodeSnippet from 'Shared/CodeSnippet'; const inputModeOptions = [ @@ -16,117 +15,67 @@ const inputModeOptions = [ { label: 'Obscure all inputs', value: 'hidden' }, ]; -const inputModeOptionsMap = {} -inputModeOptions.forEach((o, i) => inputModeOptionsMap[o.value] = i) +const inputModeOptionsMap = {}; +inputModeOptions.forEach((o, i) => (inputModeOptionsMap[o.value] = i)); -const ProjectCodeSnippet = props => { - // const site = props.sites.find(s => s.id === props.siteId); +const ProjectCodeSnippet = (props) => { const { site } = props; const { gdpr } = props.site; - const [changed, setChanged] = useState(false) - const [copied, setCopied] = useState(false) + const [changed, setChanged] = useState(false); + const [isAssistEnabled, setAssistEnabled] = useState(false); useEffect(() => { - const site = props.sites.find(s => s.id === props.siteId); + const site = props.sites.find((s) => s.id === props.siteId); if (site) { - props.init(site) + props.init(site); } - }, []) - - const codeSnippet = ` -`; + }, []); const saveGDPR = (value) => { - setChanged(true) - props.saveGDPR(site.id, GDPR({...value})); - } + setChanged(true); + props.saveGDPR(site.id, GDPR({ ...value })); + }; const onChangeSelect = ({ name, value }) => { const _gdpr = { ...gdpr.toData() }; _gdpr[name] = value; - props.editGDPR({ [ name ]: value }); - saveGDPR(_gdpr) + props.editGDPR({ [name]: value }); + saveGDPR(_gdpr); }; const onChangeOption = ({ target: { name, checked } }) => { const _gdpr = { ...gdpr.toData() }; _gdpr[name] = checked; - props.editGDPR({ [ name ]: checked }); - saveGDPR(_gdpr) - } + props.editGDPR({ [name]: checked }); + saveGDPR(_gdpr); + }; - const getOptionValues = () => { - // const { gdpr } = site; - return (!!gdpr.maskEmails)|(!!gdpr.maskNumbers << 1)|(['plain' , 'obscured', 'hidden'].indexOf(gdpr.defaultInputMode) << 5)|28 - } - - - const getCodeSnippet = site => { - let snippet = codeSnippet; - if (site && site.id) { - snippet = snippet.replace('PROJECT_KEY', site.projectKey); - } - return snippet - .replace('XXX', getOptionValues()) - .replace('HOST', site && site.host); - } - - const copyHandler = (code) => { - setCopied(true); - copy(code); - setTimeout(() => { - setCopied(false); - }, 1000); - }; - - const _snippet = getCodeSnippet(site); - - // console.log('gdpr.defaultInputMode', gdpr.defaultInputMode) - return (
Choose data recording options
-
+ +
diff --git a/frontend/app/styles/general.css b/frontend/app/styles/general.css index e86178033..9eae2bb8d 100644 --- a/frontend/app/styles/general.css +++ b/frontend/app/styles/general.css @@ -256,6 +256,12 @@ padding: 1px 2px; } +.highlight-blue { + background-color: $active-blue; + border-radius: 3px; + padding: 1px 3px; +} + .hljs { padding: 10px !important; border-radius: 6px !important; diff --git a/frontend/app/svg/icons/people.svg b/frontend/app/svg/icons/people.svg new file mode 100644 index 000000000..2bddc8df8 --- /dev/null +++ b/frontend/app/svg/icons/people.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/person-border.svg b/frontend/app/svg/icons/person-border.svg new file mode 100644 index 000000000..2b67697ca --- /dev/null +++ b/frontend/app/svg/icons/person-border.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/plug.svg b/frontend/app/svg/icons/plug.svg new file mode 100644 index 000000000..407636048 --- /dev/null +++ b/frontend/app/svg/icons/plug.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file