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
5 changes: 5 additions & 0 deletions .changeset/curly-jokes-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@propeldata/ui-kit': minor
---

Added MultiSelectFilter component
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ArgTypes, Meta } from '@storybook/blocks'
import LinkTo from '@storybook/addon-links/react'

import { SourceCode } from '../../../.storybook/blocks/SourceCode'
import * as MultiSelectFilterStories from '../../../../../packages/ui-kit/src/components/MultiSelectFilter/MultiSelectFilter.stories'
import * as AutocompleteStories from '../../../../../packages/ui-kit/src/components/Autocomplete/Autocomplete.stories'
import * as MultiSelectFilterQueryPropsStories from '../../../../../packages/ui-kit/src/components/MultiSelectFilter/MultiSelectFilterQueryProps.stories'

<Meta title="Components/MultiSelectFilter" of={MultiSelectFilterStories} />

# MultiSelectFilter Overview

The MultiSelectFilter component is a dropdown designed to build metric filters and supply them to the components in
your dashboard. You can either use it in `static` mode by providing a set of `options` and a `columnName` that the
component can use to build the filter, or use it in `connected` mode by providing a `query` object which uses
[Propel's Top Values API](https://www.propeldata.com/docs/query-your-data/top-values) to populate the dropdown.

## Basic usage

This components require wrapping your app in a `FilterProvider` which will provide the built filter to the child
components. All the components wrapped within the same `FilterProvider` will share the same filters.

You can import the component in your React file and use it as shown in the following example:

<SourceCode of={MultiSelectFilterStories.ConnectedStory} shown />

## Props API

The following table describes the props available for the SimpleFilter component:

<ArgTypes of={MultiSelectFilterStories.StaticStory} sort="alpha" />

### `SimpleFilterQueryProps`

<ArgTypes of={MultiSelectFilterQueryPropsStories.Primary} sort="alpha" />

See the documentation below for a complete reference to all of the props and classes available to the components
mentioned here.

- <LinkTo kind="API/LoaderProps" story="loaderprops">
LoaderProps
</LinkTo>
- <LinkTo kind="API/TimeRangeInput" story="overview">
TimeRangeInput
</LinkTo>
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import React, { SyntheticEvent } from 'react'
import { render, fireEvent, waitFor } from '@testing-library/react'

import { Dom } from 'src/testing'
import { DropdownOption } from '../shared.types'

import { Autocomplete } from './Autocomplete'
import { AutocompleteOption } from './Autocomplete.types'

const options = [
{
Expand All @@ -23,7 +24,7 @@ describe('Autocomplete', () => {
it('should enable selecting an option', async () => {
const onChangeValue = jest.fn()

const onChange = (_: SyntheticEvent, value: AutocompleteOption | string | null) => {
const onChange = (_: SyntheticEvent, value: DropdownOption | string | null) => {
onChangeValue(value)
}

Expand Down
7 changes: 4 additions & 3 deletions packages/ui-kit/src/components/Autocomplete/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import * as React from 'react'
import classnames from 'classnames'
import componentStyles from './Autocomplete.module.scss'
import { useCombinedRefs } from '../../helpers'
import { AutocompleteOption, AutocompleteProps } from './Autocomplete.types'
import { AutocompleteProps } from './Autocomplete.types'
import { ChevronUpIcon } from '../Icons/ChevronUp'
import { ChevronDownIcon } from '../Icons/ChevronDown'
import { DropdownOption } from '../shared.types'

// See full Autocomplete example here: https://mui.com/base-ui/react-autocomplete/#introduction
export const Autocomplete = React.forwardRef(function Autocomplete(
Expand Down Expand Up @@ -132,11 +133,11 @@ export const Autocomplete = React.forwardRef(function Autocomplete(
style={{ ...getListboxProps().style, ...listStyle }}
>
{groupedOptions.map((option, index) => {
let optionLabel: AutocompleteOption
let optionLabel: DropdownOption
if (typeof option === 'string') optionLabel = { label: option }
else optionLabel = { ...option }

const optionProps = getOptionProps({ option: option as AutocompleteOption, index })
const optionProps = getOptionProps({ option: option as DropdownOption, index })

return (
<li
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import { CSSProperties } from 'react'
import { UseAutocompleteProps } from '@mui/base/useAutocomplete'
import { DropdownOption } from '../shared.types'

export interface AutocompleteOption {
/** The display label */
label?: string
/** The value that will be used when the option is selected */
value?: string
[x: string | number | symbol]: unknown
}

export interface AutocompleteProps extends UseAutocompleteProps<string | AutocompleteOption, false, false, boolean> {
export interface AutocompleteProps extends UseAutocompleteProps<string | DropdownOption, false, false, boolean> {
placeholder?: string

/** Styles to be applied to the container element */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.loader {
height: 34.2px !important;
overflow: hidden;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import React from 'react'

import { Meta, StoryObj } from '@storybook/react'
import axiosInstance from '../../../../../app/storybook/src/axios'
import {
quotedStringRegex,
RelativeTimeRange,
storybookCodeTemplate,
TimeSeriesGranularity,
useStorybookAccessToken
} from '../../helpers'

import { MultiSelectFilter as MultiSelectFilterSource } from './MultiSelectFilter'
import { TimeSeries as TimeSeriesSource, TimeSeriesProps } from '../TimeSeries'
import { FilterProvider } from '../FilterProvider'

const meta: Meta<typeof MultiSelectFilterSource> = {
title: 'Components/MultiSelectFilter',
component: MultiSelectFilterSource,
tags: ['tag'],
parameters: {
controls: { sort: 'alpha' },
imports: 'SimpleFilter, FilterProvider',
transformBody: (body: string) => body.replace(quotedStringRegex('LAST_N_DAYS'), 'RelativeTimeRange.LastNDays'),
codeTemplate: storybookCodeTemplate
}
}

export default meta

type Story = StoryObj<typeof MultiSelectFilterSource>

const MultiSelectFilter = (args: Story['args']) => {
const { accessToken } = useStorybookAccessToken(axiosInstance)

if (!accessToken && args?.query) {
return null
}

return (
<MultiSelectFilterSource
{...{
...args,
query: args?.query
? {
...args?.query,
accessToken
}
: undefined
}}
selectProps={{
...args?.selectProps,
containerStyle: { width: '350px' },
}}
/>
)
}

const TimeSeries = (args: TimeSeriesProps) => {
const { accessToken } = useStorybookAccessToken(axiosInstance)

if (!accessToken && args?.query) {
return null
}

return (
<TimeSeriesSource
{...{
...args,
query: args?.query
? {
...args?.query,
accessToken
}
: undefined
}}
/>
)
}

export const StaticStory: Story = {
name: 'Static',
args: {
columnName: 'Color',
options: ['Red', 'Green', 'Blue'],
selectProps: { placeholder: 'Select a color' }
},
render: (args) => (
<FilterProvider>
<MultiSelectFilter {...args} />
</FilterProvider>
)
}

export const StaticCustomLabelStory: Story = {
name: 'Static Custom Label',
args: {
columnName: 'Color',
options: [
{ label: 'Red', value: '#FF0000' },
{ label: 'Green', value: '#00FF00' },
{ label: 'Blue', value: '#0000FF' }
],
selectProps: { placeholder: 'Select a color' }
},
render: (args) => (
<FilterProvider>
<MultiSelectFilter {...args} />
</FilterProvider>
)
}

export const ConnectedStory: Story = {
name: 'Connected',
args: {
query: {
columnName: process.env.STORYBOOK_DIMENSION_1 ?? '',
dataPool: {
name: process.env.STORYBOOK_DATA_POOL_UNIQUE_NAME_1 ?? ''
},
maxValues: 1000,
timeRange: {
relative: RelativeTimeRange.LastNDays,
n: 30
}
},
selectProps: {
placeholder: 'Select a sauce name'
}
},
render: (args) => (
<FilterProvider>
<MultiSelectFilter {...args} />
<TimeSeries
card
query={{
metric: 'Revenue',
timeRange: { relative: RelativeTimeRange.LastNDays, n: 30 },
granularity: TimeSeriesGranularity.Day
}}
style={{ marginTop: '12px' }}
/>
</FilterProvider>
)
}

export const MultipleStory: Story = {
name: 'Multiple',
args: {
query: {
columnName: process.env.STORYBOOK_DIMENSION_1 ?? '',
dataPool: {
name: process.env.STORYBOOK_DATA_POOL_UNIQUE_NAME_1 ?? ''
},
maxValues: 1000,
timeRange: {
relative: RelativeTimeRange.LastNDays,
n: 30
}
},
selectProps: {
placeholder: 'Select a sauce name'
}
},
render: (args) => (
<FilterProvider>
<MultiSelectFilter {...args} />
<MultiSelectFilter
query={{
columnName: 'taco_name',
dataPool: {
name: 'TacoSoft Demo Data'
},
maxValues: 1000,
timeRange: {
relative: RelativeTimeRange.LastNDays,
n: 30
}
}}
selectProps={{
placeholder: 'Select a taco name',
containerStyle: { marginTop: '12px' }
}}
/>
<TimeSeries
card
query={{
metric: 'Revenue',
timeRange: { relative: RelativeTimeRange.LastNDays, n: 30 },
granularity: TimeSeriesGranularity.Day
}}
style={{ marginTop: '12px' }}
/>
</FilterProvider>
)
}

export const ErrorStory: Story = {
name: 'Error',
args: {
query: {
columnName: 'sauce_name',
dataPool: {
name: 'invalid_data_pool_name'
},
maxValues: 3,
timeRange: {
relative: RelativeTimeRange.LastNDays,
n: 30
},
retry: false
},
selectProps: {
placeholder: 'Select a sauce name'
}
},
render: (args) => (
<FilterProvider>
<MultiSelectFilter {...args} />
<TimeSeries
card
query={{
metric: 'Revenue',
timeRange: { relative: RelativeTimeRange.LastNDays, n: 30 },
granularity: TimeSeriesGranularity.Day
}}
style={{ marginTop: '1rem' }}
/>
</FilterProvider>
)
}
Loading