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

Support IS NULL and IS NOT NULL filter search #2298

Merged
merged 4 commits into from
Sep 5, 2024
Merged
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
16 changes: 14 additions & 2 deletions src/lib/components/workflow/filter-search/boolean-filter.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,22 @@
MenuItem,
} from '$lib/holocene/menu';
import { translate } from '$lib/i18n/translate';
import { isNullConditional } from '$lib/utilities/is';

import { FILTER_CONTEXT, type FilterContext } from './index.svelte';

const { filter, handleSubmit } = getContext<FilterContext>(FILTER_CONTEXT);
const options = [
{ value: 'true', label: translate('common.true') },
{ value: 'false', label: translate('common.false') },
{ value: 'is', label: translate('common.is-null') },
{ value: 'is not', label: translate('common.is-not-null') },
];

$: selectedOption =
options.find((o) => o.value === $filter.value) ?? options[0];
options.find(
(o) => o.value === $filter.value || o.value === $filter.conditional,
) ?? options[0];
$: selectedLabel = selectedOption?.label;
</script>

Expand All @@ -34,9 +39,16 @@
{#each options as { value, label }}
<MenuItem
on:click={() => {
$filter.value = value;
if (isNullConditional(value)) {
$filter.conditional = value;
$filter.value = null;
} else {
$filter.conditional = '=';
$filter.value = value;
}
handleSubmit();
}}
class="text-nowrap"
>
{label}
</MenuItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
MenuContainer,
MenuItem,
} from '$lib/holocene/menu';
import { translate } from '$lib/i18n/translate';
import { isNullConditional } from '$lib/utilities/is';

import { FILTER_CONTEXT, type FilterContext } from './index.svelte';

const { filter, focusedElementId } =
const { filter, focusedElementId, handleSubmit } =
getContext<FilterContext>(FILTER_CONTEXT);
const defaultConditionOptions = [
{ value: '>' },
Expand All @@ -27,41 +29,58 @@
export let noBorderLeft = false;
export let noBorderRight = false;

$: filterConditionalOption = options.find(
let conditionalOptions = [
...options,
{ value: 'is', label: translate('common.is-null') },
{ value: 'is not', label: translate('common.is-not-null') },
];

$: filterConditionalOption = conditionalOptions.find(
(o) => o.value === $filter.conditional,
);
$: options, updateFilterConditional();
$: selectedOption = filterConditionalOption ?? options[0];
$: filterConditionalOption, updateFilterConditional();
$: isNullFilter = isNullConditional($filter.conditional);
$: selectedOption = filterConditionalOption ?? conditionalOptions[0];
$: selectedLabel = selectedOption?.label ?? selectedOption?.value;

function handleNullFilter() {
$filter.value = null;
handleSubmit();
}

function updateFilterConditional() {
if (!filterConditionalOption) {
$filter.conditional = options[0].value;
}
if (!filterConditionalOption)
$filter.conditional = conditionalOptions[0].value;
}
</script>

<MenuContainer>
<MenuButton
class="{noBorderRight ? '!border-r-0' : ''} {noBorderLeft
? '!border-l-0'
: ''} whitespace-nowrap"
: ''} whitespace-nowrap {isNullFilter
? 'rounded-l-none'
: 'rounded-none'}"
id="conditional-menu-button"
controls="conditional-menu"
{disabled}
>
{selectedLabel}
</MenuButton>
<Menu id="conditional-menu" class="whitespace-nowrap">
{#each options as { value, label }}
{#each conditionalOptions as { value, label }}
<MenuItem
on:click={() => {
$filter.conditional = value;
$focusedElementId = inputId;
if (isNullConditional(value)) handleNullFilter();
}}
>
{label ?? value}
</MenuItem>
{/each}
</Menu>
</MenuContainer>
{#if !isNullFilter}
<slot />
{/if}
23 changes: 11 additions & 12 deletions src/lib/components/workflow/filter-search/datetime-filter.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -125,17 +125,16 @@
};
</script>

<div class="flex items-center">
<ConditionalMenu
inputId="time-range-filter"
options={[
{ value: '<=', label: translate('common.before') },
{ value: 'BETWEEN', label: translate('common.between') },
{ value: '>=', label: translate('common.after') },
]}
noBorderLeft
noBorderRight
/>
<ConditionalMenu
inputId="time-range-filter"
options={[
{ value: '<=', label: translate('common.before') },
{ value: 'BETWEEN', label: translate('common.between') },
{ value: '>=', label: translate('common.after') },
]}
noBorderLeft
noBorderRight
>
<MenuContainer>
<MenuButton
id="time-range-filter"
Expand Down Expand Up @@ -267,4 +266,4 @@
</MenuItem>
</Menu>
</MenuContainer>
</div>
</ConditionalMenu>
35 changes: 18 additions & 17 deletions src/lib/components/workflow/filter-search/duration-filter.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,21 @@
};
</script>

<ConditionalMenu inputId="duration-filter-search" noBorderLeft />
<Input
label={$filter.attribute}
labelHidden
id="duration-filter-search"
type="search"
placeholder={`${translate('common.enter')} ${$filter.attribute} (${translate(
'workflows.duration-filter-placeholder',
)})`}
icon="search"
class="w-full"
unroundLeft
bind:value={_value}
on:keydown={handleKeydown}
on:input={validateDuration}
valid={isValid}
/>
<ConditionalMenu inputId="duration-filter-search" noBorderLeft>
<Input
label={$filter.attribute}
labelHidden
id="duration-filter-search"
type="search"
placeholder={`${translate('common.enter')} ${
$filter.attribute
} (${translate('workflows.duration-filter-placeholder')})`}
icon="search"
class="w-full"
unroundLeft
bind:value={_value}
on:keydown={handleKeydown}
on:input={validateDuration}
valid={isValid}
/>
</ConditionalMenu>
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
type TimeFormat,
} from '$lib/stores/time-format';
import { formatDate } from '$lib/utilities/format-date';
import { isStartsWith } from '$lib/utilities/is';
import { isNullConditional, isStartsWith } from '$lib/utilities/is';
import {
isDateTimeFilter,
isTextFilter,
Expand Down Expand Up @@ -114,7 +114,10 @@
{:else}
<span class="max-w-xs truncate md:max-w-lg xl:max-w-2xl">
{attribute}
{#if isDateTimeFilter({ attribute, type })}
{#if isNullConditional(conditional)}
{conditional}
{value}
{:else if isDateTimeFilter({ attribute, type })}
{#if customDate}
{formatDateTimeRange(value, $timeFormat, $relativeTime)}
{:else}
Expand Down
4 changes: 0 additions & 4 deletions src/lib/components/workflow/filter-search/index.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -294,10 +294,6 @@
@apply rounded-r-none;
}

:global(#conditional-menu-button) {
@apply rounded-none;
}

:global(#time-range-filter),
:global(#boolean-filter) {
@apply rounded-l-none;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@
};
</script>

<div class="flex items-center">
<ConditionalMenu inputId="number-filter-search" noBorderLeft />
<ConditionalMenu inputId="number-filter-search" noBorderLeft>
<NumberInput
label={translate('common.number-input-placeholder')}
labelHidden
Expand All @@ -39,4 +38,4 @@
search
class="-mr-2"
/>
</div>
</ConditionalMenu>
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
function isOptionDisabled(value: string, filters: WorkflowFilter[]) {
return filters.some(
(filter) =>
(filter.conditional === '=' || filter.conditional === '!=') &&
['=', '!=', 'is', 'is not'].includes(filter.conditional) &&
filter.attribute === value,
);
}
Expand Down
27 changes: 14 additions & 13 deletions src/lib/components/workflow/filter-search/text-filter.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,17 @@
];
</script>

<ConditionalMenu {options} inputId="text-filter-search" noBorderLeft />
<Input
label={$filter.attribute}
labelHidden
id="text-filter-search"
type="search"
placeholder={`${translate('common.enter')} ${$filter.attribute}`}
icon="search"
class="w-full"
unroundLeft
bind:value={_value}
on:keydown={handleKeydown}
/>
<ConditionalMenu {options} inputId="text-filter-search" noBorderLeft>
<Input
label={$filter.attribute}
labelHidden
id="text-filter-search"
type="search"
placeholder={`${translate('common.enter')} ${$filter.attribute}`}
icon="search"
class="w-full"
unroundLeft
bind:value={_value}
on:keydown={handleKeydown}
/>
</ConditionalMenu>
4 changes: 2 additions & 2 deletions src/lib/i18n/locales/en/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ export const Strings = {
'greater-than-or-equal-to': 'Greater than or equal to',
'less-than': 'Less than',
'less-than-or-equal-to': 'Less than or equal to',
is: 'Is',
'is-not': 'Is Not',
'is-null': 'Is null',
'is-not-null': 'Is not null',
all: 'All',
submit: 'Submit',
reason: 'Reason',
Expand Down
17 changes: 17 additions & 0 deletions src/lib/utilities/is.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest';
import {
isExecutionStatus,
isNull,
isNullConditional,
isNumber,
isObject,
isOperator,
Expand Down Expand Up @@ -337,3 +338,19 @@ describe('isQuote', () => {
expect(isQuote(undefined)).toBe(false);
});
});

describe('isNullConditional', () => {
it('should return true for is', () => {
expect(isNullConditional('IS')).toBe(true);
expect(isNullConditional('is')).toBe(true);
});

it('should return true for is not', () => {
expect(isNullConditional('IS NOT')).toBe(true);
expect(isNullConditional('is not')).toBe(true);
});

it('should return false for null', () => {
expect(isNullConditional(null)).toBe(false);
});
});
9 changes: 9 additions & 0 deletions src/lib/utilities/is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ const conditionals = [
'<',
'!',
'starts_with',
'is',
'is not',
] as const;

const joins = ['and', 'or'] as const;
Expand Down Expand Up @@ -153,6 +155,13 @@ export const isBetween = (x: unknown) => {
return x === 'between';
};

export const isNullConditional = (x: unknown) => {
if (!isString(x)) return false;
x = x.toLocaleLowerCase();

return x === 'is' || x === 'is not';
};

export const isSortOrder = (
sortOrder: string | EventSortOrder,
): sortOrder is EventSortOrder => {
Expand Down
12 changes: 8 additions & 4 deletions src/lib/utilities/query/filter-workflow-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
SearchAttributesValue,
} from '$lib/types/workflows';

import { isStartsWith } from '../is';
import { isNullConditional, isStartsWith } from '../is';
import { isDuration, isDurationString, toDate, tomorrow } from '../to-duration';

export type QueryKey =
Expand All @@ -30,8 +30,8 @@ const filterKeys: Readonly<Record<string, QueryKey>> = {
runId: 'RunId',
} as const;

const isValid = (value: unknown): boolean => {
if (value === null) return false;
const isValid = (value: unknown, conditional: string): boolean => {
if (value === null && !isNullConditional(conditional)) return false;
if (value === undefined) return false;
if (value === '') return false;
if (typeof value === 'string' && value === 'undefined') return false;
Expand Down Expand Up @@ -74,6 +74,10 @@ const toFilterQueryStatement = (
return `${queryKey} ${value}`;
}

if (isNullConditional(conditional)) {
return `\`${queryKey}\` ${conditional} ${value}`;
}

if (isDuration(value) || isDurationString(value)) {
if (archived || get(supportsAdvancedVisibility)) {
return `${queryKey} ${conditional} "${toDate(value)}"`;
Expand Down Expand Up @@ -103,7 +107,7 @@ const toQueryStatementsFromFilters = (
parenthesis,
customDate,
}) => {
if (isValid(value)) {
if (isValid(value, conditional)) {
let statement = toFilterQueryStatement(
attribute,
type,
Expand Down
Loading
Loading