Skip to content
22 changes: 20 additions & 2 deletions src/api/services/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,27 @@ export const getAllViews = (
})
);
};

/**
* Get the data for a view by its id.
*/
export const getViewDataById = async (
viewId: string,
variables?: Record<string, string>,
headers?: Record<string, string>
): Promise<ViewResult> => {
const body: { variables?: Record<string, string> } = {
variables: variables
};

const response = await fetch(`/api/view/${viewId}`, {
method: "POST",
credentials: "include",
headers
headers: {
"Content-Type": "application/json",
...headers
},
body: JSON.stringify(body)
});

if (!response.ok) {
Expand Down Expand Up @@ -102,7 +116,8 @@ export const queryViewTable = async (
namespace: string,
name: string,
columns: ViewColumnDef[],
searchParams: URLSearchParams
searchParams: URLSearchParams,
requestFingerprint: string
) => {
const cleanNamespace = namespace.replace(/-/g, "_");
const cleanName = name.replace(/-/g, "_");
Expand Down Expand Up @@ -143,6 +158,9 @@ export const queryViewTable = async (
}
}

// Add requestFingerprint as a filter if provided
queryString += `&request_fingerprint=eq.${encodeURIComponent(requestFingerprint)}`;

const response = await resolvePostGrestRequestWithPagination(
ConfigDB.get(`/${tableName}${queryString}`, {
headers: {
Expand Down
65 changes: 65 additions & 0 deletions src/hooks/usePrefixedSearchParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useCallback, useMemo } from "react";
import { useSearchParams } from "react-router-dom";

/**
* Hook that manages URL search params with a specific prefix.
* Provides filtered params (without prefix) and a setter that adds the prefix.
*
* @param prefix - The prefix to use for this component's params (e.g., 'viewvar', 'view_namespace_name')
*/
export function usePrefixedSearchParams(
prefix: string
): [
URLSearchParams,
(updater: (prev: URLSearchParams) => URLSearchParams) => void
] {
const [searchParams, setSearchParams] = useSearchParams();

const prefixedParams = useMemo(() => {
const filtered = new URLSearchParams();
const prefixWithSeparator = `${prefix}__`;

Array.from(searchParams.entries()).forEach(([key, value]) => {
if (["sortBy", "sortOrder"].includes(key)) {
filtered.set(key, value);
} else if (key.startsWith(prefixWithSeparator)) {
const cleanKey = key.substring(prefixWithSeparator.length);
filtered.set(cleanKey, value);
}
});

return filtered;
}, [searchParams, prefix]);

// Setter that adds prefix to keys when updating URL
const setPrefixedParams = useCallback(
(updater: (prev: URLSearchParams) => URLSearchParams) => {
setSearchParams((currentParams) => {
const newParams = new URLSearchParams(currentParams);
const prefixWithSeparator = `${prefix}__`;

// Remove all existing params with our prefix
Array.from(currentParams.entries()).forEach(([key]) => {
if (key.startsWith(prefixWithSeparator)) {
newParams.delete(key);
}
});

// Get the updated params from the updater
const updatedParams = updater(prefixedParams);

// Add new params with prefix
Array.from(updatedParams.entries()).forEach(([key, value]) => {
if (value && value.trim() !== "") {
newParams.set(`${prefixWithSeparator}${key}`, value);
}
});

return newParams;
});
},
[setSearchParams, prefixedParams, prefix]
);

return [prefixedParams, setPrefixedParams];
}
20 changes: 16 additions & 4 deletions src/pages/audit-report/components/DynamicDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface DynamicDataTableProps {
pageCount: number;
totalRowCount?: number;
isLoading?: boolean;
tablePrefix?: string;
}

interface RowAttributes {
Expand All @@ -36,7 +37,8 @@ const DynamicDataTable: React.FC<DynamicDataTableProps> = ({
rows,
pageCount,
totalRowCount,
isLoading
isLoading,
tablePrefix
}) => {
const columnDef: MRT_ColumnDef<any>[] = columns
.filter((col) => !col.hidden && col.type !== "row_attributes")
Expand All @@ -47,7 +49,7 @@ const DynamicDataTable: React.FC<DynamicDataTableProps> = ({
maxSize: minWidthForColumnType(col.type),
header: formatDisplayLabel(col.name),
Cell: ({ cell, row }: { cell: any; row: any }) =>
renderCellValue(cell.getValue(), col, row.original)
renderCellValue(cell.getValue(), col, row.original, tablePrefix)
};
});

Expand Down Expand Up @@ -91,7 +93,12 @@ const DynamicDataTable: React.FC<DynamicDataTableProps> = ({
);
};

const renderCellValue = (value: any, column: ViewColumnDef, row: any) => {
const renderCellValue = (
value: any,
column: ViewColumnDef,
row: any,
tablePrefix?: string
) => {
if (value == null) return "-";

let cellContent: any;
Expand Down Expand Up @@ -238,9 +245,14 @@ const renderCellValue = (value: any, column: ViewColumnDef, row: any) => {

// Wrap with FilterByCellValue if column has multiselect filter
if (column.filter?.type === "multiselect") {
// Use prefixed parameter key if tablePrefix is provided
const paramKey = tablePrefix
? `${tablePrefix}__${column.name}`
: column.name;

return (
<FilterByCellValue
paramKey={column.name}
paramKey={paramKey}
filterValue={value}
paramsToReset={["pageIndex"]}
>
Expand Down
82 changes: 82 additions & 0 deletions src/pages/audit-report/components/View/GlobalFilters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useMemo } from "react";
import {
GroupByOptions,
MultiSelectDropdown
} from "../../../../ui/Dropdowns/MultiSelectDropdown";
import { ViewVariable } from "../../types";
import { formatDisplayLabel } from "./panels/utils";
import { useField } from "formik";

interface DropdownProps {
label: string;
paramsKey: string;
options: string[];
}

const Dropdown: React.FC<DropdownProps> = ({ label, paramsKey, options }) => {
const [field] = useField({
name: paramsKey
});
const dropdownOptions = useMemo(() => {
const mappedOptions = options.map(
(option) =>
({
value: option,
label: option
}) satisfies GroupByOptions
);

return mappedOptions;
}, [options]);

return (
<MultiSelectDropdown
label={label}
options={dropdownOptions}
value={dropdownOptions.find((option) => option.value === field.value)}
onChange={(selectedOption: unknown) => {
const option = selectedOption as GroupByOptions;
field.onChange({
target: { name: paramsKey, value: option?.value }
});
}}
className="w-auto max-w-[400px]"
isMulti={false}
closeMenuOnSelect={true}
isClearable={false}
/>
);
};

interface GlobalFiltersProps {
variables?: ViewVariable[];
}

const GlobalFilters: React.FC<GlobalFiltersProps> = ({ variables }) => {
const filterComponents = useMemo(() => {
if (!variables || variables.length === 0) return [];

return variables.map((variable) => (
<Dropdown
key={variable.key}
label={variable.label || formatDisplayLabel(variable.key)}
paramsKey={variable.key}
options={variable.options}
/>
));
}, [variables]);

if (!variables || variables.length === 0) {
return null;
}

return (
<div className="mb-4">
<div className="flex flex-wrap items-center gap-2">
{filterComponents}
</div>
</div>
);
};

export default GlobalFilters;
90 changes: 90 additions & 0 deletions src/pages/audit-report/components/View/GlobalFiltersForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Form, Formik, useFormikContext } from "formik";
import { useEffect } from "react";
import { usePrefixedSearchParams } from "../../../../hooks/usePrefixedSearchParams";
import { ViewVariable } from "../../types";

type GlobalFiltersFormProps = {
children: React.ReactNode;
variables: ViewVariable[];
globalVarPrefix: string;
currentVariables?: Record<string, string>;
};

function GlobalFiltersListener({
children,
variables,
globalVarPrefix,
currentVariables = {}
}: GlobalFiltersFormProps): React.ReactElement {
const { values, setFieldValue } =
useFormikContext<Record<string, string | undefined>>();
const [globalParams, setGlobalParams] =
usePrefixedSearchParams(globalVarPrefix);

useEffect(() => {
setGlobalParams(() => {
const newParams = new URLSearchParams();

variables.forEach((variable) => {
const value = values[variable.key];
if (value) {
newParams.set(variable.key, value);
}
});

return newParams;
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [values, setGlobalParams]);

// Initialize form values when variables load or URL params change
useEffect(() => {
variables.forEach((variable) => {
const urlValue = globalParams.get(variable.key);
const currentValue = currentVariables[variable.key];
const defaultValue =
variable.default ||
(variable.options.length > 0 ? variable.options[0] : "");

const valueToUse = urlValue || currentValue || defaultValue;
if (valueToUse) {
setFieldValue(variable.key, valueToUse);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [globalParams.toString(), variables, currentVariables, setFieldValue]);

return children as React.ReactElement;
}

/**
* Global filters form that manages view-level filter parameters.
* This handles synchronization between Formik form state and URL parameters
* for global filters that affect the entire view (panels + table).
*/
export default function GlobalFiltersForm({
children,
variables,
globalVarPrefix,
currentVariables
}: GlobalFiltersFormProps) {
return (
<Formik
initialValues={{}}
onSubmit={() => {
// Form submission is handled by the listener
}}
enableReinitialize
>
<Form>
<GlobalFiltersListener
variables={variables}
globalVarPrefix={globalVarPrefix}
currentVariables={currentVariables}
>
{children}
</GlobalFiltersListener>
</Form>
</Formik>
);
}
Loading
Loading