Skip to content

Commit

Permalink
feat: add include and exclude by cells in table cells
Browse files Browse the repository at this point in the history
Fixes #1774
  • Loading branch information
mainawycliffe authored and moshloop committed May 22, 2024
1 parent c93180e commit b6e8f8b
Show file tree
Hide file tree
Showing 9 changed files with 442 additions and 27 deletions.
17 changes: 14 additions & 3 deletions src/api/services/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ export const getAllConfigs = () =>
resolve<ConfigItem[]>(ConfigDB.get(`/configs`));

export const getConfigsTags = async () => {
const res =
await ConfigDB.get<{ key: string; value: string }[]>("/config_tags");
const res = await ConfigDB.get<{ key: string; value: string }[]>(
"/config_tags"
);
return res.data ?? [];
};

Expand Down Expand Up @@ -209,6 +210,7 @@ export type GetConfigsRelatedChangesParams = {
pageSize?: string;
sortBy?: string;
sortOrder?: "asc" | "desc";
arbitraryFilter?: Record<string, string>;
};

export async function getConfigsChanges({
Expand All @@ -224,7 +226,8 @@ export async function getConfigsChanges({
page,
pageSize,
sortBy,
sortOrder
sortOrder,
arbitraryFilter
}: GetConfigsRelatedChangesParams) {
const queryParams = new URLSearchParams();
if (id) {
Expand All @@ -248,6 +251,14 @@ export async function getConfigsChanges({
queryParams.set("type", value);
}
}
if (arbitraryFilter) {
Object.entries(arbitraryFilter).forEach(([key, value]) => {
const filterExpression = tristateOutputToQueryParamValue(value);
if (filterExpression) {
queryParams.set(key, filterExpression);
}
});
}
if (from) {
queryParams.set("from", from);
}
Expand Down
82 changes: 69 additions & 13 deletions src/components/Configs/Changes/ConfigChangeHistory/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useGetConfigChangesById } from "@flanksource-ui/api/query-hooks/useGetC
import { ConfigChange } from "@flanksource-ui/api/types/configs";
import GetUserAvatar from "@flanksource-ui/components/Users/GetUserAvatar";
import { PaginationOptions } from "@flanksource-ui/ui/DataTable";
import FilterByCellValue from "@flanksource-ui/ui/DataTable/FilterByCellValue";
import { DateCell } from "@flanksource-ui/ui/table";
import { SortingState, Updater } from "@tanstack/react-table";
import { ColumnDef } from "@tanstack/table-core";
Expand All @@ -11,6 +12,10 @@ import { DataTable } from "../../../index";
import ConfigLink from "../../ConfigLink/ConfigLink";
import { ConfigDetailChangeModal } from "../ConfigDetailsChanges/ConfigDetailsChanges";

export const paramsToReset = {
configChanges: ["pageIndex", "pageSize"]
};

const columns: ColumnDef<ConfigChange>[] = [
{
header: "Created",
Expand All @@ -30,11 +35,18 @@ const columns: ColumnDef<ConfigChange>[] = [
enableHiding: true,
enableSorting: false,
cell: function ConfigLinkCell({ row }) {
const configId = row.original.config_id;
return (
<ConfigLink
config={row.original.config}
configId={row.original.config_id}
/>
<FilterByCellValue
filterValue={configId}
paramKey="id"
paramsToReset={paramsToReset.configChanges}
>
<ConfigLink
config={row.original.config}
configId={row.original.config_id}
/>
</FilterByCellValue>
);
},
size: 84
Expand All @@ -45,10 +57,16 @@ const columns: ColumnDef<ConfigChange>[] = [
cell: function ConfigChangeTypeCell({ row, column }) {
const changeType = row?.getValue(column.id) as string;
return (
<div className="text-ellipsis overflow-hidden space-x-1">
<ChangeIcon change={row.original} />
<span>{changeType}</span>
</div>
<FilterByCellValue
filterValue={changeType}
paramKey="changeType"
paramsToReset={paramsToReset.configChanges}
>
<div className="text-ellipsis overflow-hidden space-x-1">
<ChangeIcon change={row.original} />
<span>{changeType}</span>
</div>
</FilterByCellValue>
);
},
maxSize: 70
Expand All @@ -59,7 +77,20 @@ const columns: ColumnDef<ConfigChange>[] = [
meta: {
cellClassName: "text-ellipsis overflow-hidden"
},
maxSize: 150
maxSize: 150,
cell: ({ getValue }) => {
const summary = getValue<string>();

return (
<FilterByCellValue
filterValue={summary}
paramKey="summary"
paramsToReset={paramsToReset.configChanges}
>
{summary}
</FilterByCellValue>
);
}
},
{
header: "Created By",
Expand All @@ -68,18 +99,43 @@ const columns: ColumnDef<ConfigChange>[] = [
cell: ({ row }) => {
const userID = row.original.created_by;
if (userID) {
return <GetUserAvatar userID={userID} />;
return (
<FilterByCellValue
filterValue={userID}
paramKey="created_by"
paramsToReset={paramsToReset.configChanges}
>
<GetUserAvatar userID={userID} />
</FilterByCellValue>
);
}

const externalCreatedBy = row.original.external_created_by;
if (externalCreatedBy) {
return <span>{externalCreatedBy}</span>;
return (
<FilterByCellValue
filterValue={externalCreatedBy}
paramKey="external_created_by"
paramsToReset={paramsToReset.configChanges}
>
<span>{externalCreatedBy}</span>
</FilterByCellValue>
);
}

const source = row.original.source;
if (source) {
return <span>{source}</span>;
return (
<FilterByCellValue
filterValue={source}
paramKey="source"
paramsToReset={paramsToReset.configChanges}
>
<span>{source}</span>
</FilterByCellValue>
);
}


return null;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,89 @@
import ClosableBadge from "@flanksource-ui/ui/Badge/ClosableBadge";
import clsx from "clsx";
import { useCallback } from "react";
import { FaBan } from "react-icons/fa";
import { useSearchParams } from "react-router-dom";
import { paramsToReset } from "../ConfigChangeHistory";
import { ChangesTypesDropdown } from "./ChangeTypesDropdown";
import { ConfigChangeSeverity } from "./ConfigChangeSeverity";
import ConfigChangesDateRangeFilter from "./ConfigChangesDateRangeFIlter";
import ConfigTypesTristateDropdown from "./ConfigTypesTristateDropdown";

type FilterBadgeProps = {
filters: string;
paramKey: string;
};

function FilterBadge({ filters, paramKey }: FilterBadgeProps) {
const [params, setParams] = useSearchParams();

const onRemove = useCallback(
(key: string, value: string) => {
const currentValue = params.get(key);
const arrayValue = currentValue?.split(",") || [];
const newValues = arrayValue.filter(
(v) => decodeURIComponent(v) !== decodeURIComponent(value)
);
if (newValues.length === 0) {
params.delete(key);
} else {
const updateValue = newValues.join(",");
params.set(key, updateValue);
}
paramsToReset.configChanges.forEach((param) => params.delete(param));
setParams(params);
},
[params, setParams]
);

const filtersArray = filters.split(",");

return (
<>
{filtersArray.map((filter) => (
<ClosableBadge
color="gray"
key={filter}
className="flex flex-row gap-1 px-2 text-xs"
onRemove={() => onRemove(paramKey, filter)}
>
<span className="text-gray-500 font-semibold">{paramKey}:</span>
<span className="text-gray-500">
{decodeURIComponent(filter.split(":")[0])}
</span>
<span>{filter.split(":")[1] === "-1" && <FaBan />}</span>
</ClosableBadge>
))}
</>
);
}

type ConfigChangeFiltersProps = React.HTMLProps<HTMLDivElement> & {
paramsToReset?: string[];
arbitraryFilters?: Record<string, string>;
};

export function ConfigChangeFilters({
className,
paramsToReset = [],
arbitraryFilters,
...props
}: ConfigChangeFiltersProps) {
return (
<div className={clsx("flex flex-row gap-2", className)} {...props}>
<ConfigTypesTristateDropdown
paramsToReset={[...paramsToReset, "configType"]}
/>
<ChangesTypesDropdown paramsToReset={paramsToReset} />
<ConfigChangeSeverity paramsToReset={paramsToReset} />
<ConfigChangesDateRangeFilter paramsToReset={paramsToReset} />
<div className="flex flex-col gap-2">
<div className={clsx("flex flex-row gap-1", className)} {...props}>
<ConfigTypesTristateDropdown
paramsToReset={[...paramsToReset, "configType"]}
/>
<ChangesTypesDropdown paramsToReset={paramsToReset} />
<ConfigChangeSeverity paramsToReset={paramsToReset} />
<ConfigChangesDateRangeFilter paramsToReset={paramsToReset} />
</div>
<div className="flex flex-wrap gap-2">
{Object.entries(arbitraryFilters ?? {}).map(([key, value]) => (
<FilterBadge filters={value} key={value} paramKey={key} />
))}
</div>
</div>
);
}
33 changes: 31 additions & 2 deletions src/pages/config/ConfigChangesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,31 @@ export function ConfigChangesPage() {
const pageSize = params.get("pageSize") ?? "200";
const sortBy = params.get("sortBy") ?? undefined;
const sortDirection = params.get("sortDirection") === "asc" ? "asc" : "desc";
const configId = params.get("id") ?? undefined;
const changeSummary = params.get("summary") ?? undefined;
const source = params.get("source") ?? undefined;
const createdBy = params.get("created_by") ?? undefined;
const externalCreatedBy = params.get("external_created_by") ?? undefined;

const arbitraryFilter = useMemo(() => {
const filter = new Map<string, string>();
if (configId) {
filter.set("id", configId);
}
if (changeSummary) {
filter.set("summary", changeSummary);
}
if (source) {
filter.set("source", source);
}
if (createdBy) {
filter.set("created_by", createdBy);
}
if (externalCreatedBy) {
filter.set("created_by", externalCreatedBy);
}
return Object.fromEntries(filter);
}, [changeSummary, configId, createdBy, externalCreatedBy, source]);

const sortState: SortingState = useMemo(
() => [
Expand Down Expand Up @@ -66,7 +91,8 @@ export function ConfigChangesPage() {
sortBy,
sortOrder: sortDirection === "desc" ? "desc" : "asc",
page: page,
pageSize: pageSize
pageSize: pageSize,
arbitraryFilter
},
{
keepPreviousData: true
Expand Down Expand Up @@ -160,7 +186,10 @@ export function ConfigChangesPage() {
<InfoMessage message={errorMessage} />
) : (
<>
<ConfigChangeFilters paramsToReset={["pageIndex", "pageSize"]} />
<ConfigChangeFilters
paramsToReset={["pageIndex", "pageSize"]}
arbitraryFilters={arbitraryFilter}
/>
<ConfigChangeHistory
data={changes}
isLoading={isLoading}
Expand Down
62 changes: 62 additions & 0 deletions src/ui/Badge/ClosableBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from "react";
import { IoMdClose } from "react-icons/io";
import { Button } from "../Buttons/Button";

type BadgeProps = {
size?: "xs" | "sm" | "md";
color?: "blue" | "gray";
dot?: string;
title?: string;
className?: string;
colorClass?: string;
roundedClass?: string;
children: React.ReactNode;
onRemove?: () => void;
};

export default function ClosableBadge({
children,
size = "sm",
dot,
title,
color = "blue",
className,
roundedClass = "rounded",
onRemove = () => {}
}: BadgeProps) {
if (children == null || children === "") {
return null;
}

const colorClass =
color === "blue"
? "bg-blue-100 text-blue-800"
: "bg-gray-200 text-gray-700";
const spanClassName =
size === "sm" ? "text-sm px-1 py-0.5" : "text-xs px-1 py-0.5";
const svgClassName =
size === "sm" ? "mr-1.5 h-2 w-2" : "-ml-0.5 mr-1.5 h-2 w-2";

return (
<div
className={`${className} ${spanClassName} inline items-center ${roundedClass} font-medium ${colorClass}`}
title={title}
>
{dot != null && (
<svg
className={`${svgClassName} text-blue-400" fill="${dot}" viewBox="0 0 8 8"`}
>
<circle cx={4} cy={4} r={3} />
</svg>
)}
{children}
<Button
icon={<IoMdClose size={12} />}
className="hover:text-gray-700 text-gray-500 border-l border-gray-50 pl-1.5 py-1.5 text-xs rounded"
size="none"
onClick={onRemove}
title="Remove filter"
/>
</div>
);
}
Loading

0 comments on commit b6e8f8b

Please sign in to comment.