Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: UX improvements to Action #22253

Merged
merged 16 commits into from
May 22, 2024
2 changes: 1 addition & 1 deletion cypress/e2e/actions.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const createAction = (actionName: string): void => {
cy.get('input[name="item-name-large"]').should('exist')

cy.get('input[name="item-name-large"]').type(actionName)
cy.get('.LemonSegmentedButton > ul > :nth-child(2)').click() // Click "Pageview"
cy.get('[data-attr=action-type-pageview]').click() // Click "Pageview"
cy.get('[data-attr=edit-action-url-input]').click().type(Cypress.config().baseUrl)

cy.get('[data-attr=save-action-button]').first().click()
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
95 changes: 95 additions & 0 deletions frontend/src/scenes/actions/Action.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Meta, StoryFn } from '@storybook/react'
import { router } from 'kea-router'
import { MOCK_DEFAULT_BASIC_USER } from 'lib/api.mock'
import { useEffect } from 'react'
import { App } from 'scenes/App'
import { urls } from 'scenes/urls'

import { mswDecorator } from '~/mocks/browser'
import { toPaginatedResponse } from '~/mocks/handlers'
import { ActionType } from '~/types'

const MOCK_ACTION: ActionType = {
id: 1,
name: 'Test Action',
description: '',
tags: [],
post_to_slack: false,
slack_message_format: '',
steps: [
{
event: '$pageview',
selector: null,
text: null,
text_matching: null,
href: null,
href_matching: 'contains',
url: 'posthog.com/pricing',
url_matching: 'contains',
},
{
event: '$autocapture',
selector: null,
text: 'this text',
text_matching: null,
href: null,
href_matching: 'contains',
url: null,
url_matching: 'contains',
},
{
event: '$identify',
properties: [
{
key: '$browser',
value: ['Chrome'],
operator: 'exact',
type: 'person',
},
] as any,
selector: null,
text: null,
text_matching: null,
href: null,
href_matching: 'contains',
url: null,
url_matching: 'contains',
},
],
created_at: '2024-05-21T12:57:50.907581Z',
created_by: MOCK_DEFAULT_BASIC_USER,
deleted: false,
is_calculating: false,
last_calculated_at: '2024-05-21T12:57:50.894221Z',
}

const meta: Meta = {
title: 'Scenes-App/Data Management/Actions',
parameters: {
layout: 'fullscreen',
viewMode: 'story',
mockDate: '2023-02-15', // To stabilize relative dates
},
decorators: [
mswDecorator({
get: {
'/api/projects/:team_id/actions/': toPaginatedResponse([MOCK_ACTION]),
'/api/projects/:team_id/actions/1/': MOCK_ACTION,
},
}),
],
}
export default meta
export const ActionsList: StoryFn = () => {
useEffect(() => {
router.actions.push(urls.actions())
}, [])
return <App />
}

export const Action: StoryFn = () => {
useEffect(() => {
router.actions.push(urls.action(MOCK_ACTION.id))
}, [])
return <App />
}
89 changes: 57 additions & 32 deletions frontend/src/scenes/actions/Action.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { LemonSkeleton } from '@posthog/lemon-ui'
import { useValues } from 'kea'
import { NotFound } from 'lib/components/NotFound'
import { Spinner } from 'lib/lemon-ui/Spinner/Spinner'
import { actionLogic, ActionLogicProps } from 'scenes/actions/actionLogic'
import { SceneExport } from 'scenes/sceneTypes'
Expand All @@ -17,42 +19,65 @@ export const scene: SceneExport = {
}

export function Action({ id }: { id?: ActionType['id'] } = {}): JSX.Element {
const { action, isComplete } = useValues(actionLogic)
const { action, actionLoading, isComplete } = useValues(actionLogic)

if (actionLoading) {
return (
<div className="space-y-2">
<LemonSkeleton className="w-1/4 h-6" />

<LemonSkeleton className="w-1/3 h-10" />
<LemonSkeleton className="w-1/2 h-6" />

<div className="flex gap-2">
<LemonSkeleton className="w-1/2 h-120" />
<LemonSkeleton className="w-1/2 h-120" />
</div>
</div>
)
}

if (id && !action) {
return <NotFound object="action" />
}

return (
<>
{(!id || action) && <ActionEdit id={id} action={action} />}
{id &&
(isComplete ? (
<div>
<h2 className="subtitle">Matching events</h2>
<p>
This is the list of <strong>recent</strong> events that match this action.
</p>
<div className="pt-4 border-t" />
<Query
query={{
kind: NodeKind.DataTableNode,
source: {
kind: NodeKind.EventsQuery,
select: defaultDataTableColumns(NodeKind.EventsQuery),
actionId: id,
},
full: true,
showEventFilter: false,
showPropertyFilter: false,
}}
/>
</div>
) : (
<div>
<h2 className="subtitle">Matching events</h2>
<div className="flex items-center">
<Spinner className="mr-4" />
Calculating action, please hold on.
<ActionEdit id={id} action={action} />
{id && (
<>
{isComplete ? (
<div className="mt-8">
<h2 className="subtitle">Matching events</h2>
<p>
This is the list of <strong>recent</strong> events that match this action.
</p>
<div className="pt-4 border-t" />
<Query
query={{
kind: NodeKind.DataTableNode,
source: {
kind: NodeKind.EventsQuery,
select: defaultDataTableColumns(NodeKind.EventsQuery),
actionId: id,
},
full: true,
showEventFilter: false,
showPropertyFilter: false,
}}
/>
</div>
) : (
<div>
<h2 className="subtitle">Matching events</h2>
<div className="flex items-center">
<Spinner className="mr-4" />
Calculating action, please hold on.
</div>
</div>
</div>
))}
)}
</>
)}
</>
)
}
88 changes: 46 additions & 42 deletions frontend/src/scenes/actions/ActionEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@ import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { LemonField } from 'lib/lemon-ui/LemonField'
import { LemonLabel } from 'lib/lemon-ui/LemonLabel/LemonLabel'
import { Link } from 'lib/lemon-ui/Link'
import { uuid } from 'lib/utils'
import { teamLogic } from 'scenes/teamLogic'
import { urls } from 'scenes/urls'

import { tagsModel } from '~/models/tagsModel'
import { ActionStepType, AvailableFeature } from '~/types'

import { actionEditLogic, ActionEditLogicProps } from './actionEditLogic'
import { actionEditLogic, ActionEditLogicProps, DEFAULT_ACTION_STEP } from './actionEditLogic'
import { ActionStep } from './ActionStep'

export function ActionEdit({ action: loadedAction, id }: ActionEditLogicProps): JSX.Element {
Expand All @@ -27,7 +26,7 @@ export function ActionEdit({ action: loadedAction, id }: ActionEditLogicProps):
action: loadedAction,
}
const logic = actionEditLogic(logicProps)
const { action, actionLoading } = useValues(logic)
const { action, actionLoading, actionChanged } = useValues(logic)
const { submitAction, deleteAction } = useActions(logic)
const { currentTeam } = useValues(teamLogic)
const { tags } = useValues(tagsModel)
Expand Down Expand Up @@ -136,20 +135,23 @@ export function ActionEdit({ action: loadedAction, id }: ActionEditLogicProps):
</LemonButton>
) : null}
{id ? deleteButton() : cancelButton()}
<LemonButton
data-attr="save-action-button"
type="primary"
htmlType="submit"
loading={actionLoading}
onClick={submitAction}
>
Save
</LemonButton>
{actionChanged || !id ? (
<LemonButton
data-attr="save-action-button"
type="primary"
htmlType="submit"
loading={actionLoading}
onClick={submitAction}
disabledReason={!actionChanged && !id ? 'No changes to save' : undefined}
>
Save
</LemonButton>
) : null}
</>
}
/>

<div>
<div className="@container">
daibhin marked this conversation as resolved.
Show resolved Hide resolved
<h2 className="subtitle">Match groups</h2>
<p>
Your action will be triggered whenever <b>any of your match groups</b> are received.
Expand All @@ -159,7 +161,7 @@ export function ActionEdit({ action: loadedAction, id }: ActionEditLogicProps):
</p>
<LemonField name="steps">
{({ value: stepsValue, onChange }) => (
<div className="grid lg:grid-cols-2 gap-3">
<div className=" grid @4xl:grid-cols-2 gap-3">
{stepsValue.map((step: ActionStepType, index: number) => {
const identifier = String(JSON.stringify(step))
return (
Expand Down Expand Up @@ -187,11 +189,12 @@ export function ActionEdit({ action: loadedAction, id }: ActionEditLogicProps):
<div>
<LemonButton
icon={<IconPlus />}
type="secondary"
onClick={() => {
onChange([...(action.steps || []), { isNew: uuid() }])
onChange([...(action.steps || []), DEFAULT_ACTION_STEP])
}}
center
className="w-full h-full border-dashed border"
className="w-full h-full"
>
Add match group
</LemonButton>
Expand All @@ -200,36 +203,37 @@ export function ActionEdit({ action: loadedAction, id }: ActionEditLogicProps):
)}
</LemonField>
</div>
<LemonField name="post_to_slack">
{({ value, onChange }) => (
<div className="my-4">
<LemonCheckbox
id="webhook-checkbox"
checked={action.bytecode_error ? false : !!value}
onChange={onChange}
disabledReason={
!slackEnabled
? 'Configure webhooks in project settings'
: action.bytecode_error ?? null
}
label={
<>
<span>Post to webhook when this action is triggered.</span>
{action.bytecode_error ? (
<IconWarning className="text-warning text-xl ml-1" />
) : null}
</>
}
/>
<div className="mt-1 pl-6">

<div className="my-4 space-y-2">
<h2 className="subtitle">Webhook delivery</h2>
<LemonField name="post_to_slack">
{({ value, onChange }) => (
<div className="flex items-center gap-2 flex-wrap">
<LemonCheckbox
id="webhook-checkbox"
bordered
checked={action.bytecode_error ? false : !!value}
onChange={onChange}
disabledReason={
!slackEnabled
? 'Configure webhooks in project settings'
: action.bytecode_error ?? null
}
label={
<>
<span>Post to webhook when this action is triggered.</span>
{action.bytecode_error ? (
<IconWarning className="text-warning text-xl ml-1" />
) : null}
</>
}
/>
<Link to={urls.settings('project-integrations', 'integration-webhooks')}>
{slackEnabled ? 'Configure' : 'Enable'} webhooks in project settings.
</Link>
</div>
</div>
)}
</LemonField>
<div>
)}
</LemonField>
{action.post_to_slack && (
<>
{!action.bytecode_error && action.post_to_slack && (
Expand Down
7 changes: 0 additions & 7 deletions frontend/src/scenes/actions/ActionStep.scss

This file was deleted.

Loading
Loading