mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Log: Added panel support for filtering callbacks (#88980)
* chore: update stale comment * Logs Panel: add props for interactive callbacks * LogsPanel: type guard unknown props * chore: add comments * chore: rename popover filtering callbacks prop names * chore: format panelcfg * Formatting * LogsPanel: add default label filter handlers using panel context * Formatting * chore: add tests for new props
This commit is contained in:
parent
f32afbcb0a
commit
ff0c9bd66a
@ -15,6 +15,14 @@ export const pluginVersion = "11.1.0-pre";
|
||||
export interface Options {
|
||||
dedupStrategy: common.LogsDedupStrategy;
|
||||
enableLogDetails: boolean;
|
||||
isFilterLabelActive?: unknown;
|
||||
/**
|
||||
* TODO: figure out how to define callbacks
|
||||
*/
|
||||
onClickFilterLabel?: unknown;
|
||||
onClickFilterOutLabel?: unknown;
|
||||
onClickFilterOutString?: unknown;
|
||||
onClickFilterString?: unknown;
|
||||
prettifyLogMessage: boolean;
|
||||
showCommonLabels: boolean;
|
||||
showLabels: boolean;
|
||||
|
@ -188,8 +188,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
|
||||
/**
|
||||
* Used by Logs details.
|
||||
* Returns true if all queries have the filter, otherwise false.
|
||||
* TODO: In the future, we would like to return active filters based the query that produced the log line.
|
||||
* Returns true if the query identified by `refId` has a filter with the provided key and value.
|
||||
* @alpha
|
||||
*/
|
||||
isFilterLabelActive = async (key: string, value: string | number, refId?: string) => {
|
||||
@ -235,14 +234,14 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
/**
|
||||
* Used by Logs Popover Menu.
|
||||
*/
|
||||
onClickFilterValue = (value: string | number, refId?: string) => {
|
||||
onClickFilterString = (value: string | number, refId?: string) => {
|
||||
this.onModifyQueries({ type: 'ADD_STRING_FILTER', options: { value: value.toString() } }, refId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Used by Logs Popover Menu.
|
||||
*/
|
||||
onClickFilterOutValue = (value: string | number, refId?: string) => {
|
||||
onClickFilterOutString = (value: string | number, refId?: string) => {
|
||||
this.onModifyQueries({ type: 'ADD_STRING_FILTER_OUT', options: { value: value.toString() } }, refId);
|
||||
};
|
||||
|
||||
@ -437,8 +436,8 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
|
||||
splitOpenFn={this.onSplitOpen('logs')}
|
||||
scrollElement={this.scrollElement}
|
||||
isFilterLabelActive={this.isFilterLabelActive}
|
||||
onClickFilterValue={this.onClickFilterValue}
|
||||
onClickFilterOutValue={this.onClickFilterOutValue}
|
||||
onClickFilterString={this.onClickFilterString}
|
||||
onClickFilterOutString={this.onClickFilterOutString}
|
||||
/>
|
||||
</ContentOutlineItem>
|
||||
);
|
||||
|
@ -109,8 +109,8 @@ interface Props extends Themeable2 {
|
||||
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||
logsFrames?: DataFrame[];
|
||||
range: TimeRange;
|
||||
onClickFilterValue?: (value: string, refId?: string) => void;
|
||||
onClickFilterOutValue?: (value: string, refId?: string) => void;
|
||||
onClickFilterString?: (value: string, refId?: string) => void;
|
||||
onClickFilterOutString?: (value: string, refId?: string) => void;
|
||||
loadMoreLogs?(range: AbsoluteTimeRange): void;
|
||||
}
|
||||
|
||||
@ -943,8 +943,8 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
scrollIntoView={this.scrollIntoView}
|
||||
isFilterLabelActive={this.props.isFilterLabelActive}
|
||||
containerRendered={!!this.state.logsContainer}
|
||||
onClickFilterValue={this.props.onClickFilterValue}
|
||||
onClickFilterOutValue={this.props.onClickFilterOutValue}
|
||||
onClickFilterString={this.props.onClickFilterString}
|
||||
onClickFilterOutString={this.props.onClickFilterOutString}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
|
@ -58,8 +58,8 @@ interface LogsContainerProps extends PropsFromRedux {
|
||||
splitOpenFn: SplitOpen;
|
||||
scrollElement?: HTMLDivElement;
|
||||
isFilterLabelActive: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||
onClickFilterValue: (value: string, refId?: string) => void;
|
||||
onClickFilterOutValue: (value: string, refId?: string) => void;
|
||||
onClickFilterString: (value: string, refId?: string) => void;
|
||||
onClickFilterOutString: (value: string, refId?: string) => void;
|
||||
}
|
||||
|
||||
type DataSourceInstance =
|
||||
@ -350,8 +350,8 @@ class LogsContainer extends PureComponent<LogsContainerProps, LogsContainerState
|
||||
scrollElement={scrollElement}
|
||||
isFilterLabelActive={this.logDetailsFilterAvailable() ? this.props.isFilterLabelActive : undefined}
|
||||
range={range}
|
||||
onClickFilterValue={this.filterValueAvailable() ? this.props.onClickFilterValue : undefined}
|
||||
onClickFilterOutValue={this.filterOutValueAvailable() ? this.props.onClickFilterOutValue : undefined}
|
||||
onClickFilterString={this.filterValueAvailable() ? this.props.onClickFilterString : undefined}
|
||||
onClickFilterOutString={this.filterOutValueAvailable() ? this.props.onClickFilterOutString : undefined}
|
||||
/>
|
||||
</LogsCrossFadeTransition>
|
||||
</>
|
||||
|
@ -15,9 +15,9 @@ test('Does not render if the filter functions are not defined', () => {
|
||||
});
|
||||
|
||||
test('Renders copy and line contains filter', async () => {
|
||||
const onClickFilterValue = jest.fn();
|
||||
const onClickFilterString = jest.fn();
|
||||
render(
|
||||
<PopoverMenu selection="test" x={0} y={0} row={row} close={() => {}} onClickFilterValue={onClickFilterValue} />
|
||||
<PopoverMenu selection="test" x={0} y={0} row={row} close={() => {}} onClickFilterString={onClickFilterString} />
|
||||
);
|
||||
|
||||
expect(screen.getByText('Copy selection')).toBeInTheDocument();
|
||||
@ -25,12 +25,12 @@ test('Renders copy and line contains filter', async () => {
|
||||
|
||||
await userEvent.click(screen.getByText('Add as line contains filter'));
|
||||
|
||||
expect(onClickFilterValue).toHaveBeenCalledTimes(1);
|
||||
expect(onClickFilterValue).toHaveBeenCalledWith('test', row.dataFrame.refId);
|
||||
expect(onClickFilterString).toHaveBeenCalledTimes(1);
|
||||
expect(onClickFilterString).toHaveBeenCalledWith('test', row.dataFrame.refId);
|
||||
});
|
||||
|
||||
test('Renders copy and line does not contain filter', async () => {
|
||||
const onClickFilterOutValue = jest.fn();
|
||||
const onClickFilterOutString = jest.fn();
|
||||
render(
|
||||
<PopoverMenu
|
||||
selection="test"
|
||||
@ -38,7 +38,7 @@ test('Renders copy and line does not contain filter', async () => {
|
||||
y={0}
|
||||
row={row}
|
||||
close={() => {}}
|
||||
onClickFilterOutValue={onClickFilterOutValue}
|
||||
onClickFilterOutString={onClickFilterOutString}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -47,8 +47,8 @@ test('Renders copy and line does not contain filter', async () => {
|
||||
|
||||
await userEvent.click(screen.getByText('Add as line does not contain filter'));
|
||||
|
||||
expect(onClickFilterOutValue).toHaveBeenCalledTimes(1);
|
||||
expect(onClickFilterOutValue).toHaveBeenCalledWith('test', row.dataFrame.refId);
|
||||
expect(onClickFilterOutString).toHaveBeenCalledTimes(1);
|
||||
expect(onClickFilterOutString).toHaveBeenCalledWith('test', row.dataFrame.refId);
|
||||
});
|
||||
|
||||
test('Renders copy, line contains filter, and line does not contain filter', () => {
|
||||
@ -59,8 +59,8 @@ test('Renders copy, line contains filter, and line does not contain filter', ()
|
||||
y={0}
|
||||
row={row}
|
||||
close={() => {}}
|
||||
onClickFilterValue={() => {}}
|
||||
onClickFilterOutValue={() => {}}
|
||||
onClickFilterString={() => {}}
|
||||
onClickFilterOutString={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -78,8 +78,8 @@ test('Can be dismissed with escape', async () => {
|
||||
y={0}
|
||||
row={row}
|
||||
close={close}
|
||||
onClickFilterValue={() => {}}
|
||||
onClickFilterOutValue={() => {}}
|
||||
onClickFilterString={() => {}}
|
||||
onClickFilterOutString={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -11,8 +11,8 @@ interface PopoverMenuProps {
|
||||
selection: string;
|
||||
x: number;
|
||||
y: number;
|
||||
onClickFilterValue?: (value: string, refId?: string) => void;
|
||||
onClickFilterOutValue?: (value: string, refId?: string) => void;
|
||||
onClickFilterString?: (value: string, refId?: string) => void;
|
||||
onClickFilterOutString?: (value: string, refId?: string) => void;
|
||||
row: LogRowModel;
|
||||
close: () => void;
|
||||
}
|
||||
@ -20,8 +20,8 @@ interface PopoverMenuProps {
|
||||
export const PopoverMenu = ({
|
||||
x,
|
||||
y,
|
||||
onClickFilterValue,
|
||||
onClickFilterOutValue,
|
||||
onClickFilterString,
|
||||
onClickFilterOutString,
|
||||
selection,
|
||||
row,
|
||||
close,
|
||||
@ -42,7 +42,7 @@ export const PopoverMenu = ({
|
||||
};
|
||||
}, [close]);
|
||||
|
||||
const supported = onClickFilterValue || onClickFilterOutValue;
|
||||
const supported = onClickFilterString || onClickFilterOutString;
|
||||
|
||||
if (!supported) {
|
||||
return null;
|
||||
@ -59,21 +59,21 @@ export const PopoverMenu = ({
|
||||
track('copy', selection.length, row.datasourceType);
|
||||
}}
|
||||
/>
|
||||
{onClickFilterValue && (
|
||||
{onClickFilterString && (
|
||||
<Menu.Item
|
||||
label="Add as line contains filter"
|
||||
onClick={() => {
|
||||
onClickFilterValue(selection, row.dataFrame.refId);
|
||||
onClickFilterString(selection, row.dataFrame.refId);
|
||||
close();
|
||||
track('line_contains', selection.length, row.datasourceType);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{onClickFilterOutValue && (
|
||||
{onClickFilterOutString && (
|
||||
<Menu.Item
|
||||
label="Add as line does not contain filter"
|
||||
onClick={() => {
|
||||
onClickFilterOutValue(selection, row.dataFrame.refId);
|
||||
onClickFilterOutString(selection, row.dataFrame.refId);
|
||||
close();
|
||||
track('line_does_not_contain', selection.length, row.datasourceType);
|
||||
}}
|
||||
|
@ -221,8 +221,8 @@ describe('Popover menu', () => {
|
||||
logsSortOrder={LogsSortOrder.Descending}
|
||||
enableLogDetails={true}
|
||||
displayedFields={[]}
|
||||
onClickFilterOutValue={() => {}}
|
||||
onClickFilterValue={() => {}}
|
||||
onClickFilterOutString={() => {}}
|
||||
onClickFilterString={() => {}}
|
||||
app={app}
|
||||
/>
|
||||
);
|
||||
|
@ -68,8 +68,8 @@ export interface Props extends Themeable2 {
|
||||
* Any overflowing content will be clipped at the table boundary.
|
||||
*/
|
||||
overflowingContent?: boolean;
|
||||
onClickFilterValue?: (value: string, refId?: string) => void;
|
||||
onClickFilterOutValue?: (value: string, refId?: string) => void;
|
||||
onClickFilterString?: (value: string, refId?: string) => void;
|
||||
onClickFilterOutString?: (value: string, refId?: string) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@ -107,7 +107,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
if (!config.featureToggles.logRowsPopoverMenu || this.props.app !== CoreApp.Explore) {
|
||||
return false;
|
||||
}
|
||||
return Boolean(this.props.onClickFilterOutValue || this.props.onClickFilterValue);
|
||||
return Boolean(this.props.onClickFilterOutString || this.props.onClickFilterString);
|
||||
}
|
||||
|
||||
handleSelection = (e: MouseEvent<HTMLTableRowElement>, row: LogRowModel): boolean => {
|
||||
@ -217,8 +217,8 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
row={this.state.selectedRow}
|
||||
selection={this.state.selection}
|
||||
{...this.state.popoverMenuCoordinates}
|
||||
onClickFilterValue={rest.onClickFilterValue}
|
||||
onClickFilterOutValue={rest.onClickFilterOutValue}
|
||||
onClickFilterString={rest.onClickFilterString}
|
||||
onClickFilterOutString={rest.onClickFilterOutString}
|
||||
/>
|
||||
)}
|
||||
<table className={cx(styles.logsRowsTable, this.props.overflowingContent ? '' : styles.logsRowsTableContain)}>
|
||||
|
@ -324,6 +324,63 @@ describe('LogsPanel', () => {
|
||||
expect(jest.mocked(styles.getLogRowStyles).mock.calls.length).toBeGreaterThan(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filters', () => {
|
||||
const series = [
|
||||
createDataFrame({
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{
|
||||
name: 'time',
|
||||
type: FieldType.time,
|
||||
values: ['2019-04-26T09:28:11.352440161Z'],
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: FieldType.string,
|
||||
values: ['logline text'],
|
||||
labels: {
|
||||
app: 'common_app',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
it('allow to filter for a value or filter out a value', async () => {
|
||||
const filterForMock = jest.fn();
|
||||
const filterOutMock = jest.fn();
|
||||
const isFilterLabelActiveMock = jest.fn();
|
||||
setup({
|
||||
data: {
|
||||
series,
|
||||
},
|
||||
options: {
|
||||
showLabels: false,
|
||||
showTime: false,
|
||||
wrapLogMessage: false,
|
||||
showCommonLabels: false,
|
||||
prettifyLogMessage: false,
|
||||
sortOrder: LogsSortOrder.Descending,
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
enableLogDetails: true,
|
||||
onClickFilterLabel: filterForMock,
|
||||
onClickFilterOutLabel: filterOutMock,
|
||||
isFilterLabelActive: isFilterLabelActiveMock,
|
||||
},
|
||||
});
|
||||
|
||||
expect(await screen.findByRole('row')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByText('logline text'));
|
||||
await userEvent.click(screen.getByLabelText('Filter for value in query A'));
|
||||
expect(filterForMock).toHaveBeenCalledTimes(1);
|
||||
await userEvent.click(screen.getByLabelText('Filter out value in query A'));
|
||||
expect(filterOutMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(isFilterLabelActiveMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const setup = (propsOverrides?: {}) => {
|
||||
|
@ -29,10 +29,34 @@ import { LogLabels } from '../../../features/logs/components/LogLabels';
|
||||
import { LogRows } from '../../../features/logs/components/LogRows';
|
||||
import { COMMON_LABELS, dataFrameToLogsModel, dedupLogRows } from '../../../features/logs/logsModel';
|
||||
|
||||
import { Options } from './types';
|
||||
import {
|
||||
isIsFilterLabelActive,
|
||||
isOnClickFilterLabel,
|
||||
isOnClickFilterOutLabel,
|
||||
isOnClickFilterOutString,
|
||||
isOnClickFilterString,
|
||||
Options,
|
||||
} from './types';
|
||||
import { useDatasourcesFromTargets } from './useDatasourcesFromTargets';
|
||||
|
||||
interface LogsPanelProps extends PanelProps<Options> {}
|
||||
interface LogsPanelProps extends PanelProps<Options> {
|
||||
/**
|
||||
* Adds a key => value filter to the query referenced by the provided DataFrame refId. Used by Log details and Logs table.
|
||||
* onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void;
|
||||
*
|
||||
* Adds a negative key => value filter to the query referenced by the provided DataFrame refId. Used by Log details and Logs table.
|
||||
* onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void;
|
||||
*
|
||||
* Adds a string filter to the query referenced by the provided DataFrame refId. Used by the Logs popover menu.
|
||||
* onClickFilterOutString?: (value: string, refId?: string) => void;
|
||||
*
|
||||
* Removes a string filter to the query referenced by the provided DataFrame refId. Used by the Logs popover menu.
|
||||
* onClickFilterString?: (value: string, refId?: string) => void;
|
||||
*
|
||||
* Determines if a given key => value filter is active in a given query. Used by Log details.
|
||||
* isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||
*/
|
||||
}
|
||||
interface LogsPermalinkUrlState {
|
||||
logs?: {
|
||||
id?: string;
|
||||
@ -55,6 +79,11 @@ export const LogsPanel = ({
|
||||
dedupStrategy,
|
||||
enableLogDetails,
|
||||
showLogContextToggle,
|
||||
onClickFilterLabel,
|
||||
onClickFilterOutLabel,
|
||||
onClickFilterOutString,
|
||||
onClickFilterString,
|
||||
isFilterLabelActive,
|
||||
},
|
||||
id,
|
||||
}: LogsPanelProps) => {
|
||||
@ -68,7 +97,7 @@ export const LogsPanel = ({
|
||||
const [scrollElement, setScrollElement] = useState<HTMLDivElement | null>(null);
|
||||
let closeCallback = useRef<() => void>();
|
||||
|
||||
const { eventBus } = usePanelContext();
|
||||
const { eventBus, onAddAdHocFilter } = usePanelContext();
|
||||
const onLogRowHover = useCallback(
|
||||
(row?: LogRowModel) => {
|
||||
if (!row) {
|
||||
@ -220,6 +249,28 @@ export const LogsPanel = ({
|
||||
[scrollElement]
|
||||
);
|
||||
|
||||
const defaultOnClickFilterLabel = useCallback(
|
||||
(key: string, value: string) => {
|
||||
onAddAdHocFilter?.({
|
||||
key,
|
||||
value,
|
||||
operator: '=',
|
||||
});
|
||||
},
|
||||
[onAddAdHocFilter]
|
||||
);
|
||||
|
||||
const defaultOnClickFilterOutLabel = useCallback(
|
||||
(key: string, value: string) => {
|
||||
onAddAdHocFilter?.({
|
||||
key,
|
||||
value,
|
||||
operator: '!=',
|
||||
});
|
||||
},
|
||||
[onAddAdHocFilter]
|
||||
);
|
||||
|
||||
if (!data || logRows.length === 0) {
|
||||
return <PanelDataErrorView fieldConfig={fieldConfig} panelId={id} data={data} needsStringField />;
|
||||
}
|
||||
@ -275,6 +326,17 @@ export const LogsPanel = ({
|
||||
onLogRowHover={onLogRowHover}
|
||||
app={CoreApp.Dashboard}
|
||||
onOpenContext={onOpenContext}
|
||||
onClickFilterLabel={
|
||||
isOnClickFilterLabel(onClickFilterLabel) ? onClickFilterLabel : defaultOnClickFilterLabel
|
||||
}
|
||||
onClickFilterOutLabel={
|
||||
isOnClickFilterOutLabel(onClickFilterOutLabel) ? onClickFilterOutLabel : defaultOnClickFilterOutLabel
|
||||
}
|
||||
onClickFilterString={isOnClickFilterString(onClickFilterString) ? onClickFilterString : undefined}
|
||||
onClickFilterOutString={
|
||||
isOnClickFilterOutString(onClickFilterOutString) ? onClickFilterOutString : undefined
|
||||
}
|
||||
isFilterLabelActive={isIsFilterLabelActive(isFilterLabelActive) ? isFilterLabelActive : undefined}
|
||||
/>
|
||||
{showCommonLabels && isAscending && renderCommonLabels()}
|
||||
</div>
|
||||
|
@ -26,15 +26,21 @@ composableKinds: PanelCfg: {
|
||||
version: [0, 0]
|
||||
schema: {
|
||||
Options: {
|
||||
showLabels: bool
|
||||
showCommonLabels: bool
|
||||
showTime: bool
|
||||
showLogContextToggle: bool
|
||||
wrapLogMessage: bool
|
||||
prettifyLogMessage: bool
|
||||
enableLogDetails: bool
|
||||
sortOrder: common.LogsSortOrder
|
||||
dedupStrategy: common.LogsDedupStrategy
|
||||
showLabels: bool
|
||||
showCommonLabels: bool
|
||||
showTime: bool
|
||||
showLogContextToggle: bool
|
||||
wrapLogMessage: bool
|
||||
prettifyLogMessage: bool
|
||||
enableLogDetails: bool
|
||||
sortOrder: common.LogsSortOrder
|
||||
dedupStrategy: common.LogsDedupStrategy
|
||||
// TODO: figure out how to define callbacks
|
||||
onClickFilterLabel?: _
|
||||
onClickFilterOutLabel?: _
|
||||
isFilterLabelActive?: _
|
||||
onClickFilterString?: _
|
||||
onClickFilterOutString?: _
|
||||
} @cuetsy(kind="interface")
|
||||
}
|
||||
}]
|
||||
|
@ -13,6 +13,14 @@ import * as common from '@grafana/schema';
|
||||
export interface Options {
|
||||
dedupStrategy: common.LogsDedupStrategy;
|
||||
enableLogDetails: boolean;
|
||||
isFilterLabelActive?: unknown;
|
||||
/**
|
||||
* TODO: figure out how to define callbacks
|
||||
*/
|
||||
onClickFilterLabel?: unknown;
|
||||
onClickFilterOutLabel?: unknown;
|
||||
onClickFilterOutString?: unknown;
|
||||
onClickFilterString?: unknown;
|
||||
prettifyLogMessage: boolean;
|
||||
showCommonLabels: boolean;
|
||||
showLabels: boolean;
|
||||
|
@ -1 +1,29 @@
|
||||
import { DataFrame } from '@grafana/data';
|
||||
|
||||
export { Options } from './panelcfg.gen';
|
||||
|
||||
type onClickFilterLabelType = (key: string, value: string, frame?: DataFrame) => void;
|
||||
type onClickFilterOutLabelType = (key: string, value: string, frame?: DataFrame) => void;
|
||||
type onClickFilterValueType = (value: string, refId?: string) => void;
|
||||
type onClickFilterOutStringType = (value: string, refId?: string) => void;
|
||||
type isFilterLabelActiveType = (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||
|
||||
export function isOnClickFilterLabel(callback: unknown): callback is onClickFilterLabelType {
|
||||
return typeof callback === 'function';
|
||||
}
|
||||
|
||||
export function isOnClickFilterOutLabel(callback: unknown): callback is onClickFilterOutLabelType {
|
||||
return typeof callback === 'function';
|
||||
}
|
||||
|
||||
export function isOnClickFilterString(callback: unknown): callback is onClickFilterValueType {
|
||||
return typeof callback === 'function';
|
||||
}
|
||||
|
||||
export function isOnClickFilterOutString(callback: unknown): callback is onClickFilterOutStringType {
|
||||
return typeof callback === 'function';
|
||||
}
|
||||
|
||||
export function isIsFilterLabelActive(callback: unknown): callback is isFilterLabelActiveType {
|
||||
return typeof callback === 'function';
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user