diff --git a/dashboard/src/features/orgs/projects/common/components/DataGridFilters/DataGridFilter.tsx b/dashboard/src/features/orgs/projects/common/components/DataGridFilters/DataGridFilter.tsx
new file mode 100644
index 000000000..ddb56ac1d
--- /dev/null
+++ b/dashboard/src/features/orgs/projects/common/components/DataGridFilters/DataGridFilter.tsx
@@ -0,0 +1,80 @@
+import { Button } from '@/components/ui/v3/button';
+import { Input } from '@/components/ui/v3/input';
+import {
+ useDataGridFilter,
+ type DataGridFilterOperator,
+} from '@/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider';
+import { cn, isNotEmptyValue } from '@/lib/utils';
+import { X } from 'lucide-react';
+import DataGridFilterColumn from './DataGridFilterColumn';
+import DataGridFilterOperators from './DataGridFilterOperators';
+
+type FilterProps = {
+ column: string;
+ op: DataGridFilterOperator;
+ value: string;
+ index: number;
+ columns: Array<{ id: string; dataType: string }>;
+ error?: string;
+};
+
+function DataGridFilter({
+ column,
+ op,
+ value,
+ index,
+ columns,
+ error,
+}: FilterProps) {
+ const { setColumn, setOp, setValue, removeFilter } = useDataGridFilter();
+
+ function handleOpChange(newOp: DataGridFilterOperator) {
+ setOp(index, newOp);
+ if (
+ newOp === '$like' ||
+ newOp === '$ilike' ||
+ newOp === '$nlike' ||
+ newOp === '$nilike'
+ ) {
+ setValue(index, '%%');
+ } else if (newOp === '$in' || newOp === '$nin') {
+ setValue(index, '[]');
+ }
+ }
+
+ return (
+
+ );
+}
+
+export default DataGridFilter;
diff --git a/dashboard/src/features/orgs/projects/common/components/DataGridFilters/DataGridFilterColumn.tsx b/dashboard/src/features/orgs/projects/common/components/DataGridFilters/DataGridFilterColumn.tsx
new file mode 100644
index 000000000..e7adf0c9e
--- /dev/null
+++ b/dashboard/src/features/orgs/projects/common/components/DataGridFilters/DataGridFilterColumn.tsx
@@ -0,0 +1,42 @@
+import { Badge } from '@/components/ui/v3/badge';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+} from '@/components/ui/v3/select';
+
+type DataFilterColumnProps = {
+ value: string;
+ onChange: (newValue: string) => void;
+ columns: Array<{ id: string; dataType: string }>;
+};
+
+function DataGrdiFitlerColumn({
+ value,
+ onChange,
+ columns,
+}: DataFilterColumnProps) {
+ return (
+
+ );
+}
+
+export default DataGrdiFitlerColumn;
diff --git a/dashboard/src/features/orgs/projects/common/components/DataGridFilters/DataGridFilterOperators.tsx b/dashboard/src/features/orgs/projects/common/components/DataGridFilters/DataGridFilterOperators.tsx
new file mode 100644
index 000000000..490c80f27
--- /dev/null
+++ b/dashboard/src/features/orgs/projects/common/components/DataGridFilters/DataGridFilterOperators.tsx
@@ -0,0 +1,50 @@
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+} from '@/components/ui/v3/select';
+import type { DataGridFilterOperator } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider';
+
+const OPERATORS = [
+ { op: '$eq', label: '[$eq] equals' },
+ { op: '$ne', label: '[$ne] not equals' },
+ { op: '$in', label: '[$in] in' },
+ { op: '$nin', label: '[$nin] not in' },
+ { op: '$gt', label: '[$gt] >' },
+ { op: '$lt', label: '[$lt] <' },
+ { op: '$gte', label: '[$gte] >=' },
+ { op: '$lte', label: '[$lte] <=' },
+ { op: '$like', label: '[$like] like' },
+ { op: '$nlike', label: '[$nlike] not like' },
+ { op: '$ilike', label: '[$ilike] like (case-insensitive)' },
+ { op: '$nilike', label: '[$nilike] not like (case-insensitive)' },
+ { op: '$similar', label: '[$similar] similar' },
+ { op: '$nsimilar', label: '[$nsimilar] not similar' },
+ { op: '$regex', label: '[$regex] ~' },
+ { op: '$iregex', label: '[$iregex] ~*' },
+ { op: '$nregex', label: '[$nregex] !~' },
+ { op: '$niregex', label: '[$niregex] !~*' },
+];
+
+type DataFilterProps = {
+ value: DataGridFilterOperator;
+ onChange: (newOp: DataGridFilterOperator) => void;
+};
+
+function DataGridOperators({ value, onChange }: DataFilterProps) {
+ return (
+
+ );
+}
+
+export default DataGridOperators;
diff --git a/dashboard/src/features/orgs/projects/common/components/DataGridFilters/DataGridFilterTrigger.tsx b/dashboard/src/features/orgs/projects/common/components/DataGridFilters/DataGridFilterTrigger.tsx
new file mode 100644
index 000000000..9c5161832
--- /dev/null
+++ b/dashboard/src/features/orgs/projects/common/components/DataGridFilters/DataGridFilterTrigger.tsx
@@ -0,0 +1,34 @@
+import { Button, type ButtonProps } from '@/components/ui/v3/button';
+import { useDataGridFilter } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider';
+import { cn } from '@/lib/utils';
+import { Funnel } from 'lucide-react';
+import { type ForwardedRef, forwardRef } from 'react';
+
+function DataBrowserCustomizerTrigger(
+ props: ButtonProps,
+ ref: ForwardedRef,
+) {
+ const { appliedFilters } = useDataGridFilter();
+ const numberOfAppliedFilters = appliedFilters.length;
+
+ const { className, ...buttonProps } = props;
+
+ return (
+
+ );
+}
+
+export default forwardRef(DataBrowserCustomizerTrigger);
diff --git a/dashboard/src/features/orgs/projects/common/components/DataGridFilters/DataGridFilters.tsx b/dashboard/src/features/orgs/projects/common/components/DataGridFilters/DataGridFilters.tsx
new file mode 100644
index 000000000..2130632ac
--- /dev/null
+++ b/dashboard/src/features/orgs/projects/common/components/DataGridFilters/DataGridFilters.tsx
@@ -0,0 +1,104 @@
+import { Button } from '@/components/ui/v3/button';
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/v3/popover';
+import {
+ type DataGridFilter as Filter,
+ useDataGridFilter,
+} from '@/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider';
+import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
+import { isEmptyValue, isNotEmptyValue } from '@/lib/utils';
+import { useState } from 'react';
+import { v4 as uuidV4 } from 'uuid';
+import DataGridFilter from './DataGridFilter';
+import DataGridFilterTrigger from './DataGridFilterTrigger';
+
+function hasErrors(filters: Filter[]) {
+ return filters.reduce((errors, { op, value, column }, index) => {
+ if (isEmptyValue(value)) {
+ return { ...errors, [`${column}.${index}`]: 'Empty filter' };
+ }
+ if (['$in', '$nin'].includes(op)) {
+ try {
+ JSON.parse(value);
+ } catch {
+ return {
+ ...errors,
+ [`${column}.${index}`]: 'Invalid format. ["item1","item 2"]',
+ };
+ }
+ }
+
+ return errors;
+ }, {});
+}
+
+function DataGridFilters() {
+ const { filters, addFilter, appliedFilters, setFilters, setAppliedFilters } =
+ useDataGridFilter();
+ const { columns } = useDataGridConfig();
+ const [errors, setErrors] = useState({});
+
+ function resetFilters() {
+ setFilters(appliedFilters);
+ }
+
+ function handleApplyFilter() {
+ const filterErrors = hasErrors(filters);
+ setErrors(filterErrors);
+ if (isEmptyValue(filterErrors)) {
+ setAppliedFilters(filters);
+ }
+ }
+
+ function handleOpenChange(newOpenState: boolean) {
+ if (!newOpenState) {
+ resetFilters();
+ }
+ }
+
+ function handleAddFilter() {
+ addFilter({ column: columns[0].id, op: '$eq', value: '', id: uuidV4() });
+ }
+
+ return (
+
+
+
+
+
+
+ {isNotEmptyValue(filters) &&
+ filters.map((filter, index) => (
+
+ ))}
+ {isEmptyValue(filters) && (
+
+ No filters applied to this table
+
+ Add a filter below to filter the table
+
+ )}
+
+
+
+
+
+
+
+ );
+}
+
+export default DataGridFilters;
diff --git a/dashboard/src/features/orgs/projects/common/components/DataGridFilters/index.ts b/dashboard/src/features/orgs/projects/common/components/DataGridFilters/index.ts
new file mode 100644
index 000000000..d5c39086b
--- /dev/null
+++ b/dashboard/src/features/orgs/projects/common/components/DataGridFilters/index.ts
@@ -0,0 +1 @@
+export { default as DataGridFilters } from './DataGridFilters';
diff --git a/dashboard/src/features/orgs/projects/database/common/hooks/useTablePath/useTablePath.ts b/dashboard/src/features/orgs/projects/database/common/hooks/useTablePath/useTablePath.ts
index ae2f1b3bc..a6fd24bd7 100644
--- a/dashboard/src/features/orgs/projects/database/common/hooks/useTablePath/useTablePath.ts
+++ b/dashboard/src/features/orgs/projects/database/common/hooks/useTablePath/useTablePath.ts
@@ -9,7 +9,6 @@ export default function useTablePath() {
const {
query: { dataSourceSlug, schemaSlug, tableSlug },
} = useRouter();
-
if (!dataSourceSlug || !schemaSlug || !tableSlug) {
return '';
}
diff --git a/dashboard/src/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataBrowserGrid.tsx b/dashboard/src/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataBrowserGrid.tsx
index 4f7bfa0ad..f1e7d19ff 100644
--- a/dashboard/src/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataBrowserGrid.tsx
+++ b/dashboard/src/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataBrowserGrid.tsx
@@ -3,6 +3,7 @@ import { FormActivityIndicator } from '@/components/form/FormActivityIndicator';
import { InlineCode } from '@/components/ui/v3/inline-code';
import { useTablePath } from '@/features/orgs/projects/database/common/hooks/useTablePath';
import { DataBrowserEmptyState } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserEmptyState';
+import { useDataGridFilter } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider';
import { DataBrowserGridControls } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserGridControls';
import { useTableQuery } from '@/features/orgs/projects/database/dataGrid/hooks/useTableQuery';
import type { UpdateRecordVariables } from '@/features/orgs/projects/database/dataGrid/hooks/useUpdateRecordMutation';
@@ -26,6 +27,7 @@ import { DataGridNumericCell } from '@/features/orgs/projects/storage/dataGrid/c
import { DataGridTextCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridTextCell';
import { isNotEmptyValue } from '@/lib/utils';
+import { useTableRows } from '@/features/orgs/projects/database/dataGrid/hooks/useTableRows';
import { useQueryClient } from '@tanstack/react-query';
import { KeyRound } from 'lucide-react';
import dynamic from 'next/dynamic';
@@ -157,16 +159,45 @@ export default function DataBrowserGrid({
const [currentOffset, setCurrentOffset] = useState(
parseInt(page as string, 10) - 1 || 0,
);
-
+ const { appliedFilters } = useDataGridFilter();
const { mutateAsync: updateRow } = useUpdateRecordWithToastMutation();
const sortByString = isNotEmptyValue(sortBy?.[0])
? `${sortBy[0].id}.${sortBy[0].desc}`
: 'default-order';
+ const filterString = isNotEmptyValue(appliedFilters)
+ ? appliedFilters
+ .map((filter) => `${filter.column}-${filter.op}-${filter.value}`)
+ .join('')
+ : 'no-filter';
- const { data, status, error, refetch } = useTableQuery(
- [currentTablePath, currentOffset, sortByString],
+ const {
+ data,
+ status,
+ error,
+ refetch,
+ isFetching: isTableDataFetching,
+ } = useTableQuery([currentTablePath], {
+ limit,
+ offset: currentOffset * limit,
+ orderBy:
+ sortBy?.map(({ id, desc }) => ({
+ columnName: id,
+ mode: desc ? 'DESC' : 'ASC',
+ })) || [],
+ filters: appliedFilters,
+ });
+
+ const { columns, metadata } = data || {
+ columns: [] as NormalizedQueryDataRow[],
+ };
+
+ const columnNames = columns?.map((column) => column.column_name);
+
+ const { data: tableRowsData, isFetching: isTableRowsFetching } = useTableRows(
+ [currentTablePath, currentOffset, sortByString, filterString],
{
+ columnNames,
limit,
offset: currentOffset * limit,
orderBy:
@@ -174,15 +205,16 @@ export default function DataBrowserGrid({
columnName: id,
mode: desc ? 'DESC' : 'ASC',
})) || [],
+ filters: appliedFilters,
+ queryOptions: {
+ enabled: !isTableDataFetching,
+ },
},
);
- const { columns, rows, numberOfRows, metadata } = data || {
- columns: [] as NormalizedQueryDataRow[],
- rows: [] as NormalizedQueryDataRow[],
- numberOfRows: 0,
- };
-
+ const rows = tableRowsData?.rows || ([] as NormalizedQueryDataRow[]);
+ const numberOfRows = tableRowsData?.numberOfRows || 0;
+ const tableRowsError = tableRowsData?.error;
useEffect(() => {
if (
currentTablePath &&
@@ -262,8 +294,6 @@ export default function DataBrowserGrid({
[columns, currentTablePath, queryClient, updateRow],
);
- const memoizedData = useMemo(() => rows, [rows]);
-
async function handleInsertRowClick() {
openDrawer({
title: 'Insert a New Row',
@@ -324,11 +354,17 @@ export default function DataBrowserGrid({
Error: {tableRowsError}
+ ) : (
+ 'No rows found.'
+ )
+ }
loading={status === 'loading'}
sortBy={sortBy}
className="pb-17 sm:pb-0"
@@ -352,6 +388,7 @@ export default function DataBrowserGrid({
refetchData={refetch}
/>
}
+ isFetching={!!isTableRowsFetching}
{...props}
/>
);
diff --git a/dashboard/src/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider/DataGridFilterProvider.tsx b/dashboard/src/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider/DataGridFilterProvider.tsx
new file mode 100644
index 000000000..ac47f95aa
--- /dev/null
+++ b/dashboard/src/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider/DataGridFilterProvider.tsx
@@ -0,0 +1,168 @@
+import { useTablePath } from '@/features/orgs/projects/database/common/hooks/useTablePath';
+import PersistenDataGrdiFilterStorage from '@/features/orgs/projects/database/dataGrid/utils/PersistentDataGridFilterStorage';
+import {
+ createContext,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+ type PropsWithChildren,
+} from 'react';
+
+function updateFilterInArray(
+ filters: DataGridFilter[],
+ index: number,
+ newValue: DataGridFilter,
+) {
+ return [...filters.slice(0, index), newValue, ...filters.slice(index + 1)];
+}
+
+function updateFilter(
+ oldFilters: DataGridFilter[],
+ index: number,
+ filterKey: keyof DataGridFilter,
+ newValue: string | DataGridFilterOperator,
+) {
+ const filter = oldFilters[index];
+ const filterToUpdate = {
+ ...filter,
+ [filterKey]: newValue,
+ };
+ return updateFilterInArray(oldFilters, index, filterToUpdate);
+}
+
+export type DataGridFilterOperator =
+ | '$eq'
+ | '$ne'
+ | '$in'
+ | '$nin'
+ | '$gt'
+ | '$lt'
+ | '$gte'
+ | '$lte'
+ | '$like'
+ | '$nlike'
+ | '$ilike'
+ | '$nilike'
+ | '$similar'
+ | '$nsimilar'
+ | '$regex'
+ | '$iregex'
+ | '$nregex'
+ | '$niregex';
+
+export type DataGridFilter = {
+ column: string;
+ op: DataGridFilterOperator;
+ value: string;
+ id: string;
+};
+
+type DataGridFilterContextProps = {
+ appliedFilters: DataGridFilter[];
+ setAppliedFilters: (filters: DataGridFilter[]) => void;
+ filters: DataGridFilter[];
+ setFilters: (filters: DataGridFilter[]) => void;
+ addFilter: (newFilter: DataGridFilter) => void;
+ removeFilter: (index: number) => void;
+ setValue: (index: number, newValue: string) => void;
+ setColumn: (index: number, newColumn: string) => void;
+ setOp: (index: number, newOp: DataGridFilterOperator) => void;
+};
+
+const DataGridFilterContext = createContext({
+ appliedFilters: [] as DataGridFilter[],
+ setAppliedFilters: () => {},
+ filters: [] as DataGridFilter[],
+ setFilters: () => {},
+ addFilter: () => {},
+ removeFilter: () => {},
+ setValue: () => {},
+ setColumn: () => {},
+ setOp: () => {},
+});
+
+function DataGridFilterProvider({ children }: PropsWithChildren) {
+ const tablePath = useTablePath();
+ const [appliedFilters, _setAppliedFilters] = useState(() =>
+ PersistenDataGrdiFilterStorage.getDataGridFilters(tablePath),
+ );
+ const [filters, setFilters] = useState(() =>
+ PersistenDataGrdiFilterStorage.getDataGridFilters(tablePath),
+ );
+ // const [loadedFiltersTablePath, setLoadedFiltersTablePath] = useState(
+ // () => tablePath,
+ // );
+
+ // const test = useRef(null);
+
+ function addFilter(newFilter: DataGridFilter) {
+ setFilters((oldFilters) => oldFilters.concat(newFilter));
+ }
+
+ function setAppliedFilters(newFilters: DataGridFilter[]) {
+ _setAppliedFilters(newFilters);
+ PersistenDataGrdiFilterStorage.saveDataGridFilters(tablePath, newFilters);
+ }
+
+ useEffect(() => {
+ const filtersForTheTable =
+ PersistenDataGrdiFilterStorage.getDataGridFilters(tablePath);
+ setFilters(filtersForTheTable);
+ _setAppliedFilters(filtersForTheTable);
+ }, [tablePath]);
+
+ const contextValue: DataGridFilterContextProps = useMemo(
+ () => ({
+ filters,
+ setFilters,
+ appliedFilters,
+ setAppliedFilters,
+ addFilter,
+ removeFilter(index: number) {
+ setFilters((oldFilters) => {
+ const newFilters = oldFilters.filter((_, i) => index !== i);
+ PersistenDataGrdiFilterStorage.saveDataGridFilters(
+ tablePath,
+ newFilters,
+ );
+ return newFilters;
+ });
+ },
+
+ setColumn(index: number, newColumnValue: string) {
+ setFilters((oldFilters) =>
+ updateFilter(oldFilters, index, 'column', newColumnValue),
+ );
+ },
+
+ setValue(index: number, newValue: string) {
+ setFilters((oldFilters) =>
+ updateFilter(oldFilters, index, 'value', newValue),
+ );
+ },
+
+ setOp(index: number, newOp: DataGridFilterOperator) {
+ setFilters((oldFilters) =>
+ updateFilter(oldFilters, index, 'op', newOp),
+ );
+ },
+ }),
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [appliedFilters, filters],
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+export default DataGridFilterProvider;
+
+export function useDataGridFilter() {
+ const context = useContext(DataGridFilterContext);
+
+ return context;
+}
diff --git a/dashboard/src/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider/index.ts b/dashboard/src/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider/index.ts
new file mode 100644
index 000000000..26c867242
--- /dev/null
+++ b/dashboard/src/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider/index.ts
@@ -0,0 +1,3 @@
+export { default as DataGridFilterProvider } from './DataGridFilterProvider';
+
+export * from './DataGridFilterProvider';
diff --git a/dashboard/src/features/orgs/projects/database/dataGrid/components/DataBrowserGridControls/DataBrowserGridControls.tsx b/dashboard/src/features/orgs/projects/database/dataGrid/components/DataBrowserGridControls/DataBrowserGridControls.tsx
index 998a3f9f8..e05af3721 100644
--- a/dashboard/src/features/orgs/projects/database/dataGrid/components/DataBrowserGridControls/DataBrowserGridControls.tsx
+++ b/dashboard/src/features/orgs/projects/database/dataGrid/components/DataBrowserGridControls/DataBrowserGridControls.tsx
@@ -2,6 +2,7 @@ import { useDialog } from '@/components/common/DialogProvider';
import { Badge } from '@/components/ui/v3/badge';
import { ButtonWithLoading as Button } from '@/components/ui/v3/button';
import { DataGridCustomizerControls } from '@/features/orgs/projects/common/components/DataGridCustomizerControls';
+import { DataGridFilters } from '@/features/orgs/projects/common/components/DataGridFilters';
import { useDeleteRecordMutation } from '@/features/orgs/projects/database/dataGrid/hooks/useDeleteRecordMutation';
import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
@@ -156,6 +157,7 @@ export default function DataBrowserGridControls({
{...restPaginationProps}
/>
)}
+