diff --git a/frontend/src/lib/components/AppEditorLink/AppEditorLink.js b/frontend/src/lib/components/AppEditorLink/AppEditorLink.js index f869a8dd577d5..a44b885452627 100644 --- a/frontend/src/lib/components/AppEditorLink/AppEditorLink.js +++ b/frontend/src/lib/components/AppEditorLink/AppEditorLink.js @@ -1,9 +1,10 @@ import React, { useState } from 'react' import { useValues } from 'kea' -import { ChooseURLModal } from './ChooseURLModal' +import { EditAppUrls } from './EditAppUrls' import { appEditorUrl } from './utils' -import { userLogic } from '../../../scenes/userLogic' +import { userLogic } from 'scenes/userLogic' +import { Modal, Button } from 'antd' export function AppEditorLink({ actionId, style, className, children }) { const [modalOpen, setModalOpen] = useState(false) @@ -23,12 +24,18 @@ export function AppEditorLink({ actionId, style, className, children }) { > {children} - {modalOpen && ( - setModalOpen(false)} - /> - )} + setModalOpen(false)}>Close} + onCancel={() => setModalOpen(false)} + > + setModalOpen(false)} /> + ) } diff --git a/frontend/src/lib/components/AppEditorLink/ChooseURLModal.js b/frontend/src/lib/components/AppEditorLink/ChooseURLModal.js deleted file mode 100644 index e0db18623ab7c..0000000000000 --- a/frontend/src/lib/components/AppEditorLink/ChooseURLModal.js +++ /dev/null @@ -1,134 +0,0 @@ -import React, { useState, useEffect } from 'react' -import { useActions, useValues } from 'kea' -import { Modal } from '../Modal' -import api from '../../api' -import { userLogic } from '../../../scenes/userLogic' -import { UrlRow } from './UrlRow' -import { appEditorUrl, defaultUrl } from './utils' - -export function ChooseURLModal({ actionId, dismissModal }) { - const { user } = useValues(userLogic) - const { setUser, loadUser, userUpdateRequest } = useActions(userLogic) - const appUrls = user.team.app_urls - - const [newValue, setNewValue] = useState(defaultUrl) - const [addingNew, setAddingNew] = useState(false) - - // We run this effect so that the URLs are the latest ones from the database. - // Otherwise if you edit/add an URL, click to it and then click back, you will - // see state urls (i.e. without the one you just added) - useEffect(() => { - loadUser() - }, []) // run just once - - function saveUrl({ index, value, callback }) { - const newUrls = - typeof index === 'undefined' - ? appUrls.concat([value]) - : appUrls.map((url, i) => (i === index ? value : url)) - - const willRedirect = - appUrls.length === 0 && typeof index === 'undefined' - - api.update('api/user', { team: { app_urls: newUrls } }).then(user => { - callback(newUrls) - - // Do not set the app urls when redirecting. - // Doing so is bad UX as the screen will flash from the "add first url" dialog to - // the "here are all the urls" dialog before the user is redirected away - if (!willRedirect) { - setUser(user) - } - if (!index) { - setAddingNew(false) - } - }) - } - - function deleteUrl({ index }) { - const newUrls = appUrls.filter((v, i) => i !== index) - userUpdateRequest({ team: { app_urls: newUrls } }) - } - - return ( - 0 && - !addingNew && ( -
- -
- ) - } - onDismiss={dismissModal} - > - {appUrls.length === 0 ? ( -
- setNewValue(e.target.value)} - autoFocus - style={{ maxWidth: 400 }} - type="url" - className="form-control" - name="url" - placeholder={defaultUrl} - /> -
- -
- ) : ( - - )} -
- ) -} diff --git a/frontend/src/lib/components/AppEditorLink/EditAppUrls.js b/frontend/src/lib/components/AppEditorLink/EditAppUrls.js new file mode 100644 index 0000000000000..af5e0366218a3 --- /dev/null +++ b/frontend/src/lib/components/AppEditorLink/EditAppUrls.js @@ -0,0 +1,169 @@ +import React, { useState } from 'react' +import { kea, useActions, useValues } from 'kea' +import { Spin, Button, List } from 'antd' +import { PlusOutlined } from '@ant-design/icons' + +import { userLogic } from 'scenes/userLogic' +import api from 'lib/api' +import { toParams } from 'lib/utils' +import { UrlRow } from './UrlRow' +import { toast } from 'react-toastify' + +const defaultValue = 'https://' + +const appUrlsLogic = kea({ + actions: () => ({ + addUrl: value => ({ value }), + removeUrl: index => ({ index }), + updateUrl: (index, value) => ({ index, value }), + }), + + loaders: ({ values }) => ({ + suggestions: { + loadSuggestions: async () => { + let params = { + events: [{ id: '$pageview', name: '$pageview', type: 'events' }], + breakdown: '$current_url', + } + let data = await api.get('api/action/trends/?' + toParams(params)) + let domainsSeen = [] + return data + .filter(item => { + let domain = new URL(item.breakdown_value).hostname + if (domainsSeen.indexOf(domain) > -1) return + if (values.appUrls.filter(url => url.indexOf(domain) > -1).length > 0) return + domainsSeen.push(domain) + return true + }) + .map(item => item.breakdown_value) + .slice(0, 20) + }, + }, + }), + + events: ({ actions }) => ({ + afterMount: actions.loadSuggestions, + }), + + defaults: () => ({ + appUrls: state => userLogic.selectors.user(state).team.app_urls || [defaultValue], + }), + + allURLs: ({ selectors }) => ({ + recordsForSelectedMonth: [ + () => [selectors.appUrls, selectors.suggestions], + (appUrls, suggestions) => { + return appUrls + suggestions + }, + ], + }), + + reducers: ({ actions }) => ({ + appUrls: [ + [defaultValue], + { + [actions.addUrl]: (state, { value }) => state.concat([value || defaultValue]), + [actions.updateUrl]: (state, { index, value }) => Object.assign([...state], { [index]: value }), + [actions.removeUrl]: (state, { index }) => { + const newAppUrls = [...state] + newAppUrls.splice(index, 1) + return newAppUrls + }, + }, + ], + suggestions: [ + [], + { + [actions.addUrl]: (state, { value }) => [...state].filter(item => value !== item), + }, + ], + isSaved: [ + false, + { + [actions.addUrl]: () => false, + [actions.removeUrl]: () => false, + [actions.updateUrl]: () => false, + [userLogic.actions.userUpdateSuccess]: (state, { updateKey }) => updateKey === 'SetupAppUrls' || state, + }, + ], + }), + + listeners: ({ sharedListeners }) => ({ + addUrl: sharedListeners.saveAppUrls, + removeUrl: sharedListeners.saveAppUrls, + updateUrl: sharedListeners.saveAppUrls, + }), + + sharedListeners: ({ values }) => ({ + saveAppUrls: () => { + toast('URLs saved', { toastId: 'EditAppUrls' }) + userLogic.actions.userUpdateRequest({ team: { app_urls: values.appUrls } }, 'SetupAppUrls') + }, + }), +}) + +export function EditAppUrls({ actionId, allowNavigation }) { + const { appUrls, suggestions, suggestionsLoading, isSaved } = useValues(appUrlsLogic) + const { addUrl, removeUrl, updateUrl } = useActions(appUrlsLogic) + const [loadMore, setLoadMore] = useState() + + return ( +
+ + {appUrls.map((url, index) => ( + updateUrl(index, value)} + deleteUrl={() => removeUrl(index)} + /> + ))} + {appUrls.length === 0 && No url set yet.} + + Suggestions: {suggestionsLoading && }{' '} + {!suggestionsLoading && suggestions.length === 0 && 'No suggestions found.'} + + {suggestions && + suggestions.slice(0, loadMore ? suggestions.length : 5).map(url => ( + addUrl(url)} + style={{ cursor: 'pointer', justifyContent: 'space-between' }} + > + e.preventDefault()}> + {url} + + + + ))} + {!loadMore && suggestions && suggestions.length > 5 && ( +
+ +
+ )} +
+ + {isSaved && ( + + URLs saved. + + )} + +
+ ) +} diff --git a/frontend/src/lib/components/AppEditorLink/UrlRow.js b/frontend/src/lib/components/AppEditorLink/UrlRow.js index 764d2a15a4dea..9f6bc95391ff2 100644 --- a/frontend/src/lib/components/AppEditorLink/UrlRow.js +++ b/frontend/src/lib/components/AppEditorLink/UrlRow.js @@ -1,43 +1,46 @@ import React, { useState } from 'react' import { appEditorUrl, defaultUrl } from './utils' +import { Input, Button, List } from 'antd' +import { DeleteOutlined, EditOutlined } from '@ant-design/icons' -export function UrlRow({ actionId, url, saveUrl, deleteUrl }) { +export function UrlRow({ actionId, url, saveUrl, deleteUrl, allowNavigation }) { const [isEditing, setIsEditing] = useState(url === defaultUrl) const [savedValue, setSavedValue] = useState(url || defaultUrl) const [editedValue, setEditedValue] = useState(url || defaultUrl) return ( -
  • + {isEditing ? ( -
    - { + e.preventDefault() + if (editedValue === defaultUrl) { + deleteUrl() + } else { + saveUrl(editedValue) + setIsEditing(false) + setSavedValue(editedValue) + } + }} + > + setEditedValue(e.target.value)} autoFocus + required style={{ flex: '1' }} type="url" - className="form-control" placeholder={defaultUrl} /> - - -
    + + ) : typeof url === 'undefined' ? (
    ) : ( -
    -
    - - -
    -
    +
    + !allowNavigation && e.preventDefault()}> {editedValue} + + setIsEditing(true)} + style={{ color: 'var(--primary)', marginLeft: 8 }} + /> + +
    )} -
  • + ) } diff --git a/frontend/src/scenes/setup/Setup.js b/frontend/src/scenes/setup/Setup.js index fea000e826253..301d6604e4128 100644 --- a/frontend/src/scenes/setup/Setup.js +++ b/frontend/src/scenes/setup/Setup.js @@ -1,13 +1,13 @@ import React from 'react' import { useValues } from 'kea' import { Divider } from 'antd' -import { JSSnippet } from '../../lib/components/JSSnippet' -import { InviteTeam } from '../../lib/components/InviteTeam' +import { JSSnippet } from 'lib/components/JSSnippet' +import { InviteTeam } from 'lib/components/InviteTeam' import { OptOutCapture } from './OptOutCapture' import { UpdateEmailPreferences } from './UpdateEmailPreferences' -import { SetupAppUrls } from './SetupAppUrls' +import { EditAppUrls } from 'lib/components/AppEditorLink/EditAppUrls' -import { userLogic } from '../userLogic' +import { userLogic } from 'scenes/userLogic' import { DeleteDemoData } from './DeleteDemoData' import { SlackIntegration } from 'scenes/setup/SlackIntegration' import { ChangePassword } from './ChangePassword' @@ -22,9 +22,6 @@ export function Setup() { return (
    -

    Setup your PostHog account

    - -

    Integrate PostHog

    To integrate PostHog, copy + paste the following snippet to your website. Ideally, put it just above the{' '}
    </head>
    tag.{' '} @@ -41,6 +38,11 @@ export function Setup() { This key is write-only, in that it can only create new events. It can't read any events or any of your other data stored on PostHog. +

    Permitted domains

    + These are the domains and urls where the toolbar will automatically open if you're logged in. It's also the + domains where you'll be able to create actions on. + +

    Slack or Teams Integration

    diff --git a/frontend/src/scenes/setup/SetupAppUrls.js b/frontend/src/scenes/setup/SetupAppUrls.js deleted file mode 100644 index f18412dfb3a00..0000000000000 --- a/frontend/src/scenes/setup/SetupAppUrls.js +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react' -import { kea, useActions, useValues } from 'kea' -import { Button, Input, Tooltip } from 'antd' -import { DeleteOutlined } from '@ant-design/icons' - -import { userLogic } from '../userLogic' - -const defaultValue = 'https://' - -const appUrlsLogic = kea({ - actions: () => ({ - addUrl: true, - removeUrl: index => ({ index }), - updateUrl: (index, value) => ({ index, value }), - saveAppUrls: true, - }), - - defaults: () => ({ - appUrls: state => userLogic.selectors.user(state).team.app_urls || [defaultValue], - }), - - reducers: ({ actions }) => ({ - appUrls: [ - [defaultValue], - { - [actions.addUrl]: state => state.concat([defaultValue]), - [actions.updateUrl]: (state, { index, value }) => Object.assign([...state], { [index]: value }), - [actions.removeUrl]: (state, { index }) => { - const newAppUrls = [...state] - newAppUrls.splice(index, 1) - return newAppUrls - }, - }, - ], - isSaved: [ - false, - { - [actions.addUrl]: () => false, - [actions.removeUrl]: () => false, - [actions.updateUrl]: () => false, - [userLogic.actions.userUpdateSuccess]: (state, { updateKey }) => updateKey === 'SetupAppUrls' || state, - }, - ], - }), - - listeners: ({ actions, values }) => ({ - [actions.saveAppUrls]: () => { - userLogic.actions.userUpdateRequest({ team: { app_urls: values.appUrls } }, 'SetupAppUrls') - }, - }), -}) - -export function SetupAppUrls() { - const { appUrls, isSaved } = useValues(appUrlsLogic) - const { addUrl, removeUrl, updateUrl, saveAppUrls } = useActions(appUrlsLogic) - - return ( -
    - - {appUrls.map((url, index) => ( -
    - updateUrl(index, e.target.value)} - autoFocus={appUrls.count === 1 && appUrls[0] === defaultValue} - type="url" - placeholder={defaultValue} - style={{ width: '400px' }} - suffix={ - -
    - ))} - {appUrls.length === 0 &&
    } - -
    - - - {isSaved && ( - - URLs saved. - - )} -
    - ) -} diff --git a/frontend/src/scenes/trends/ActionsTable.js b/frontend/src/scenes/trends/ActionsTable.js index 3dc39311be0bc..3f2800c6d1441 100644 --- a/frontend/src/scenes/trends/ActionsTable.js +++ b/frontend/src/scenes/trends/ActionsTable.js @@ -1,6 +1,7 @@ import React, { Component } from 'react' import api from '../../lib/api' import { Loading, toParams } from '../../lib/utils' +import { Table } from 'antd' import PropTypes from 'prop-types' export class ActionsTable extends Component { @@ -30,21 +31,21 @@ export class ActionsTable extends Component { let { filters } = this.props return data ? ( data[0] && (filters.session || data[0].labels) ? ( - - - - - - - - {data.map(item => ( - - - - - ))} - -
    {filters.session ? 'Session Attribute' : 'Action'}{filters.session ? 'Value' : 'Count'}
    {item.label}{item.count}
    +
    {label}
    , + }, + { title: filters.session ? 'Value' : 'Count', dataIndex: 'count' }, + ]} + rowKey={item => item.label} + pagination={{ pageSize: 9999, hideOnSinglePage: true }} + dataSource={data} + data-attr="trend-table-graph" + /> ) : (

    We couldn't find any matching actions.

    )