Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions frontend/common/services/useWarehouseConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,35 @@ export const warehouseConnectionService = service
Req['getWarehouseConnections']
>({
providesTags: [{ id: 'LIST', type: 'WarehouseConnection' }],
query: ({ environmentId }) => ({
url: `environments/${environmentId}/warehouse-connections/`,
query: ({ environmentId, exclude_event_stats }) => ({
url: `environments/${environmentId}/warehouse-connections/${
exclude_event_stats ? '?exclude_event_stats=true' : ''
}`,
}),
}),
testWarehouseConnection: builder.mutation<
Res['warehouseConnections'][number],
Req['testWarehouseConnection']
>({
invalidatesTags: [{ id: 'LIST', type: 'WarehouseConnection' }],
async onQueryStarted({ environmentId }, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled
dispatch(
warehouseConnectionService.util.updateQueryData(
'getWarehouseConnections',
{ environmentId, exclude_event_stats: true },
(draft) => {
const index = draft.findIndex(
(connection) => connection.id === data.id,
)
if (index !== -1) draft[index] = data
},
),
)
} catch {
return
}
},
query: ({ environmentId, id }) => ({
method: 'POST',
url: `environments/${environmentId}/warehouse-connections/${id}/test-warehouse-connection/`,
Expand Down
13 changes: 11 additions & 2 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
StageActionType,
StageActionBody,
ChangeRequest,
ExpectedDirection,
ExperimentStatus,
MetricAggregation,
MetricDirection,
Expand Down Expand Up @@ -1010,7 +1011,10 @@ export type Req = {
project_id: number
gitlab_project_id: number
}>
getWarehouseConnections: { environmentId: string }
getWarehouseConnections: {
environmentId: string
exclude_event_stats?: boolean
}
createWarehouseConnection: {
environmentId: string
warehouse_type: string
Expand All @@ -1031,7 +1035,12 @@ export type Req = {
}>
createExperiment: {
environmentId: string
body: { name: string; hypothesis: string; feature: number }
body: {
name: string
hypothesis: string
feature: number
metrics: { metric: number; expected_direction: ExpectedDirection }[]
}
}
experimentAction: { environmentId: string; experimentId: number }
deleteExperiment: { environmentId: string; experimentId: number }
Expand Down
16 changes: 16 additions & 0 deletions frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,21 @@ export type Metric = {
updated_at: string
}

export type ExpectedDirection =
| 'increase'
| 'decrease'
| 'not_increase'
| 'not_decrease'

export type ExperimentMetric = {
id: number
metric: number
metric_name: string
aggregation: MetricAggregation
expected_direction: ExpectedDirection
created_at: string
}

export type ExperimentFeature = {
id: number
name: string
Expand All @@ -622,6 +637,7 @@ export type Experiment = {
hypothesis: string
feature: ExperimentFeature
status: ExperimentStatus
metrics: ExperimentMetric[]
created_at: string
updated_at: string
started_at: string | null
Expand Down
100 changes: 100 additions & 0 deletions frontend/documentation/components/CenteredModal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { useState } from 'react'
import type { Meta, StoryObj } from 'storybook'

import CenteredModal from 'components/base/CenteredModal'
import Button from 'components/base/forms/Button'

const meta: Meta<typeof CenteredModal> = {
component: CenteredModal,
parameters: {
chromatic: { delay: 300 },
docs: {
description: {
component:
'Traditional centred modal sized to ~80% of the viewport, with a scrollable body. ' +
'Unlike the global `openModal` drawer, it is fully declarative: the parent owns the ' +
'open state and passes `isOpen`/`onClose`, with arbitrary children as the body.',
},
},
layout: 'centered',
},
title: 'Components/Modals/CenteredModal',
}
export default meta

type Story = StoryObj<typeof CenteredModal>

export const Default: Story = {
render: () => (
<CenteredModal isOpen title='Create Metric' onClose={() => {}}>
<p className='mb-0'>Modal body content goes here.</p>
</CenteredModal>
),
}

export const WithFormContent: Story = {
render: () => (
<CenteredModal isOpen title='Create Metric' onClose={() => {}}>
<div className='d-flex flex-column gap-4'>
<div className='d-flex flex-column gap-1'>
<label htmlFor='story-metric-name'>Name</label>
<input
id='story-metric-name'
className='form-control'
placeholder='e.g. Signup Completion Rate'
/>
</div>
<div className='d-flex flex-column gap-1'>
<label htmlFor='story-metric-description'>Description</label>
<input
id='story-metric-description'
className='form-control'
placeholder='What does this metric measure?'
/>
</div>
<div className='d-flex justify-content-end gap-2'>
<Button theme='secondary'>Cancel</Button>
<Button>Create Metric</Button>
</div>
</div>
</CenteredModal>
),
}

export const ScrollableBody: Story = {
render: () => (
<CenteredModal isOpen title='Terms of Service' onClose={() => {}}>
<div className='d-flex flex-column gap-3'>
{Array.from({ length: 30 }, (_, i) => (
<p key={i} className='text-secondary mb-0'>
Long content block {i + 1} — the modal body scrolls once content
exceeds 75% of the viewport height.
</p>
))}
</div>
</CenteredModal>
),
}

const InteractiveExample = () => {
const [isOpen, setIsOpen] = useState(false)
return (
<>
<Button onClick={() => setIsOpen(true)}>Open modal</Button>
<CenteredModal
isOpen={isOpen}
title='Create Metric'
onClose={() => setIsOpen(false)}
>
<p className='mb-0'>
Close me via the header button or by clicking the backdrop.
</p>
</CenteredModal>
</>
)
}

export const Interactive: Story = {
parameters: { chromatic: { disableSnapshot: true } },
render: () => <InteractiveExample />,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.centered-modal {
width: 80vw;
max-width: 80vw;

.modal-body {
max-height: 75vh;
overflow-y: auto;
}
}
32 changes: 32 additions & 0 deletions frontend/web/components/base/CenteredModal/CenteredModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { FC, ReactNode } from 'react'
import { Modal, ModalBody } from 'reactstrap'
import ModalHeader from 'components/modals/base/ModalHeader'
import './CenteredModal.scss'

type CenteredModalProps = {
isOpen: boolean
title: ReactNode
onClose: () => void
children: ReactNode
className?: string
}

const CenteredModal: FC<CenteredModalProps> = ({
children,
className,
isOpen,
onClose,
title,
}) => (
<Modal
className={`modal-dialog-centered centered-modal ${className ?? ''}`}
isOpen={isOpen}
toggle={onClose}
unmountOnClose
>
<ModalHeader onDismissClick={onClose}>{title}</ModalHeader>
<ModalBody>{children}</ModalBody>
</Modal>
)

export default CenteredModal
1 change: 1 addition & 0 deletions frontend/web/components/base/CenteredModal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './CenteredModal'
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@

&__title {
font-size: var(--font-body-size);
font-weight: var(--font-weight-semibold);
font-weight: var(--font-weight-medium);
color: var(--color-text-default);
}

Expand All @@ -59,6 +59,22 @@
color: var(--color-text-secondary);
}

&__tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-top: 4px;
}

&__tag {
font-size: var(--font-caption-xs-size);
font-weight: var(--font-weight-regular);
padding: 2px 8px;
border-radius: var(--radius-sm);
background: var(--color-surface-emphasis);
color: var(--color-text-secondary);
}

&__aside {
display: flex;
align-items: center;
Expand All @@ -68,7 +84,7 @@

&__badge {
font-size: var(--font-caption-xs-size);
font-weight: var(--font-weight-semibold);
font-weight: var(--font-weight-medium);
padding: 4px 12px;
border-radius: var(--radius-full);
flex-shrink: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type SelectableCardProps = {
title: string
description: string
badge?: { label: string; variant: BadgeVariant }
tags?: string[]
disabled?: boolean
}

Expand All @@ -20,6 +21,7 @@ const SelectableCard: FC<SelectableCardProps> = ({
icon,
onClick,
selected,
tags,
title,
}) => {
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
Expand All @@ -43,6 +45,15 @@ const SelectableCard: FC<SelectableCardProps> = ({
{icon && <div className='selectable-card__icon'>{icon}</div>}
<span className='selectable-card__title'>{title}</span>
<span className='selectable-card__description'>{description}</span>
{!!tags?.length && (
<div className='selectable-card__tags'>
{tags.map((tag) => (
<span key={tag} className='selectable-card__tag'>
{tag}
</span>
))}
</div>
)}
</div>
{badge && (
<div className='selectable-card__aside'>
Expand Down
1 change: 1 addition & 0 deletions frontend/web/components/base/SelectableCard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './SelectableCard'
12 changes: 12 additions & 0 deletions frontend/web/components/base/grid/ContentCard/ContentCard.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,23 @@
border-radius: var(--radius-lg);

&__header {
display: flex;
flex-direction: column;
gap: 4px;
}

&__heading {
display: flex;
align-items: center;
justify-content: space-between;
}

&__description {
font-size: var(--font-caption-size);
color: var(--color-text-secondary);
margin: 0;
}

.input-container {
display: block;
}
Expand Down
13 changes: 10 additions & 3 deletions frontend/web/components/base/grid/ContentCard/ContentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import './ContentCard.scss'

type ContentCardProps = {
title?: string
description?: string
action?: ReactNode
className?: string
children: ReactNode
Expand All @@ -13,14 +14,20 @@ const ContentCard: FC<ContentCardProps> = ({
action,
children,
className,
description,
title,
}) => {
return (
<div className={cn('content-card', className)}>
{(title || action) && (
{(title || action || description) && (
<div className='content-card__header'>
{title && <h3 className='content-card__title'>{title}</h3>}
{action}
<div className='content-card__heading'>
{title && <h3 className='content-card__title'>{title}</h3>}
{action}
</div>
{description && (
<p className='content-card__description'>{description}</p>
)}
</div>
)}
{children}
Expand Down
Loading
Loading