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

fix(ui,be): minor bugs on Incidents after facets #3072

Merged
merged 8 commits into from
Jan 21, 2025
3 changes: 2 additions & 1 deletion keep-ui/app/(keep)/incidents/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const defaultIncidentsParams: GetIncidentsParams = {
offset: 0,
sorting: { id: "creation_time", desc: true },
filters: DefaultIncidentFilters,
cel: "!(status in ['resolved', 'deleted'])", // on initial page load, we have to display only active incidents
};

export default async function Page() {
Expand All @@ -21,7 +22,7 @@ export default async function Page() {
const api = await createServerApiClient();

const tasks = [
getIncidents(api, defaultIncidentsParams),
getIncidents(api, defaultIncidentsParams, ),
getInitialFacets(api, "incidents"),
]
const [_incidents, _facetsData] = await Promise.all(tasks);
Expand Down
5 changes: 5 additions & 0 deletions keep-ui/entities/incidents/api/incidents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type GetIncidentsParams = {
offset: number;
sorting: { id: string; desc: boolean };
filters: Filters | {};
cel?: string;
};

function buildIncidentsUrl(params: GetIncidentsParams) {
Expand All @@ -30,6 +31,10 @@ function buildIncidentsUrl(params: GetIncidentsParams) {
}
});

if (params.cel) {
filtersParams.append("cel", params.cel);
}

return `/incidents?confirmed=${params.confirmed}&limit=${params.limit}&offset=${params.offset}&sorting=${
params.sorting.desc ? "-" : ""
}${params.sorting.id}&${filtersParams.toString()}`;
Expand Down
7 changes: 7 additions & 0 deletions keep-ui/features/filter/add-facet-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export const AddFacetModal: React.FC<AddFacetModalProps> = ({
onClose();
}

function isSubmitEnabled(): boolean {
return name.trim().length > 0 && propertyPath.trim().length > 0;
}

return (
<Modal
isOpen={isOpen}
Expand All @@ -47,6 +51,7 @@ export const AddFacetModal: React.FC<AddFacetModalProps> = ({

<TextInput
placeholder="Enter facet name"
required={true}
value={name}
onChange={(e) => setName(e.target.value)}
className="mb-4"
Expand All @@ -59,6 +64,7 @@ export const AddFacetModal: React.FC<AddFacetModalProps> = ({

<TextInput
placeholder="Enter facet property path"
required={true}
value={propertyPath}
onChange={(e) => setPropertyPath(e.target.value)}
className="mb-4"
Expand All @@ -79,6 +85,7 @@ export const AddFacetModal: React.FC<AddFacetModalProps> = ({
size="xs"
variant="primary"
type="submit"
disabled={!isSubmitEnabled()}
onClick={() => handleNewFacetCreation()}
>
Create
Expand Down
19 changes: 18 additions & 1 deletion keep-ui/features/filter/facet-panel-server-side.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,21 @@ export interface FacetsPanelProps {
entityName: string;
className?: string;
initialFacetsData?: InitialFacetsData;
/** Revalidation token to force recalculation of the facets */
/**
* Revalidation token to force recalculation of the facets.
* Will call API to recalculate facet options every revalidationToken value change
*/
revalidationToken?: string | null;
/**
* Token to clear filters related to facets.
* Filters will be cleared every clearFiltersToken value change.
**/
clearFiltersToken?: string | null;
/**
* Object with facets that should be unchecked by default.
* Key is the facet name, value is the list of option values to uncheck.
**/
uncheckedByDefaultOptionValues?: { [key: string]: string[] };
renderFacetOptionLabel?: (
facetName: string,
optionDisplayName: string
Expand All @@ -28,7 +41,9 @@ export const FacetsPanelServerSide: React.FC<FacetsPanelProps> = ({
className,
initialFacetsData,
revalidationToken,
clearFiltersToken,
onCelChange = undefined,
uncheckedByDefaultOptionValues,
renderFacetOptionIcon,
renderFacetOptionLabel,
}) => {
Expand Down Expand Up @@ -106,6 +121,8 @@ export const FacetsPanelServerSide: React.FC<FacetsPanelProps> = ({
facets={(facetsData as any) || []}
facetOptions={(facetOptions as any) || {}}
areFacetOptionsLoading={isLoading}
clearFiltersToken={clearFiltersToken}
uncheckedByDefaultOptionValues={uncheckedByDefaultOptionValues}
renderFacetOptionLabel={renderFacetOptionLabel}
renderFacetOptionIcon={renderFacetOptionIcon}
onCelChange={(cel: string) => {
Expand Down
12 changes: 6 additions & 6 deletions keep-ui/features/filter/facet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface FacetProps {
optionsReloading: boolean;
showIcon?: boolean;
facetKey: string;
facetState: any;
facetState: Set<string>;
renderOptionLabel?: (optionDisplayName: string) => JSX.Element | string | undefined;
renderIcon?: (option_display_name: string) => JSX.Element | undefined;
onSelectOneOption: (value: string) => void;
Expand Down Expand Up @@ -83,10 +83,10 @@ export const Facet: React.FC<FacetProps> = ({
return false;
}

const isSelected = facetState?.[optionValue];
const restNotSelected = Object.entries(facetState)
.filter(([key, value]) => key !== optionValue)
.every(([key, value]) => !value);
const isSelected = !facetState.has(optionValue);
const restNotSelected = options
.filter((option) => option.display_name !== optionValue)
.every((option) => facetState.has(option.display_name));

return isSelected && restNotSelected;
}
Expand All @@ -113,7 +113,7 @@ export const Facet: React.FC<FacetProps> = ({
facetOption.display_name
)}
isSelected={
facetState?.[facetOption.display_name] !== false &&
!facetState.has(facetOption.display_name) &&
facetOption.matches_count > 0
}
renderLabel={() => renderOptionLabel && renderOptionLabel(facetOption.display_name)}
Expand Down
94 changes: 67 additions & 27 deletions keep-ui/features/filter/facets-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import React, { useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { Facet } from "./facet";
import { CreateFacetDto, FacetDto, FacetOptionDto, FacetOptionsQueries } from "./models";
import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { useLocalStorage } from "@/utils/hooks/useLocalStorage";
import { AddFacetModal } from "./add-facet-modal";
import 'react-loading-skeleton/dist/skeleton.css';

/**
* It's facets state. Key is the facet id, and value is Set<string> of unselected options.
* If facet option value is selected, the set will contain it's display value, otherwise it will not.
*/
type FacetState = {
[facetId: string]: { [optionId: string]: boolean };
[facetId: string]: Set<string>;
}

function buildCel(facets: FacetDto[], facetOptions: { [key: string]: FacetOptionDto[] }, facetsState: FacetState): string {
const cel = Object.values(facets)
.filter((facet) => facet.id in facetsState)
.map((facet) => {
const notSelectedOptions = Object.values(facetOptions[facet.id])
.filter((facetOption) => facetsState[facet.id][facetOption.display_name] === false)
.filter((facetOption) => facetsState[facet.id]?.has(facetOption.display_name))
.map((option) => {
if (typeof option.value === 'string') {
return `'${option.value}'`;
Expand Down Expand Up @@ -46,6 +50,13 @@ export interface FacetsPanelProps {
facets: FacetDto[];
facetOptions: { [key: string]: FacetOptionDto[] };
areFacetOptionsLoading?: boolean;
/** Token to clear filters related to facets */
clearFiltersToken?: string | null;
/**
* Object with facets that should be unchecked by default.
* Key is the facet name, value is the list of option values to uncheck.
**/
uncheckedByDefaultOptionValues?: { [key: string]: string[] };
renderFacetOptionLabel?: (facetName: string, optionDisplayName: string) => JSX.Element | string | undefined;
renderFacetOptionIcon?: (facetName: string, optionDisplayName: string) => JSX.Element | undefined;
onCelChange: (cel: string) => void;
Expand All @@ -61,6 +72,8 @@ export const FacetsPanel: React.FC<FacetsPanelProps> = ({
facets,
facetOptions,
areFacetOptionsLoading = false,
clearFiltersToken,
uncheckedByDefaultOptionValues,
renderFacetOptionIcon = undefined,
renderFacetOptionLabel,
onCelChange = undefined,
Expand All @@ -69,9 +82,8 @@ export const FacetsPanel: React.FC<FacetsPanelProps> = ({
onLoadFacetOptions = undefined,
onReloadFacetOptions = undefined,
}) => {
const [facetsState, setFacetsState] = useState<{
[facetId: string]: { [optionId: string]: boolean };
}>({});
const defaultStateHandledForFacetIds = useMemo(() => new Set<string>(), []);
const [facetsState, setFacetsState] = useState<FacetState>({});
const [clickedFacetId, setClickedFacetId] = useState<string | null>(null);

const [isModalOpen, setIsModalOpen] = useLocalStorage<boolean>(
Expand All @@ -80,11 +92,27 @@ export const FacetsPanel: React.FC<FacetsPanelProps> = ({
);
const [celState, setCelState] = useState("");

function getFacetState(facetId: string): Set<string> {
if (!defaultStateHandledForFacetIds.has(facetId) && uncheckedByDefaultOptionValues && Object.keys(uncheckedByDefaultOptionValues).length) {
const facetState = new Set<string>(...(facetsState[facetId] || []));
const facet = facets.find((f) => f.id === facetId);

if (facet) {
uncheckedByDefaultOptionValues[facet?.name]?.forEach((optionValue) => facetState.add(optionValue));
defaultStateHandledForFacetIds.add(facetId);
}

facetsState[facetId] = facetState;
}

return facetsState[facetId] || new Set<string>();
}

const isOptionSelected = (facet_id: string, option_id: string) => {
return facetsState[facet_id]?.[option_id] !== false;
return !facetsState[facet_id] || !facetsState[facet_id].has(option_id);
}

function calculateFacetsState(changedFacetId: string | null, newFacetsState: FacetState): void {
function calculateFacetsState(newFacetsState: FacetState): void {
setFacetsState(newFacetsState);
var cel = buildCel(facets, facetOptions, newFacetsState);

Expand All @@ -105,45 +133,57 @@ export const FacetsPanel: React.FC<FacetsPanelProps> = ({

function toggleFacetOption(facetId: string, value: string) {
setClickedFacetId(facetId);
const currentFacetState: any = facetsState[facetId] || {};
currentFacetState[value] = !isOptionSelected(facetId, value);
const newFacetsState = {
...facetsState,
[facetId]: currentFacetState,
};
calculateFacetsState(facetId, newFacetsState);
facetsState[facetId] = facetsState[facetId] || new Set<string>();

if (isOptionSelected(facetId, value)) {
facetsState[facetId].add(value)
} else {
facetsState[facetId].delete(value)
}

calculateFacetsState({ ...facetsState });
}

function selectOneFacetOption(facetId: string, optionValue: string): void {
setClickedFacetId(facetId);
const newFacetState: any = {};
facetsState[facetId] = facetsState[facetId] || new Set<string>();

facetOptions[facetId].forEach(facetOption => {
if (facetOption.display_name === optionValue) {
newFacetState[facetOption.display_name] = true;
facetsState[facetId].delete(optionValue);
return;
}

newFacetState[facetOption.display_name] = false;
facetsState[facetId].add(facetOption.display_name);
})

calculateFacetsState(facetId, {
...facetsState,
[facetId]: newFacetState,
calculateFacetsState({
...facetsState
});
}

function selectAllFacetOptions(facetId: string) {
setClickedFacetId(facetId);
const newFacetState: any = { ...facetsState[facetId] };
Object.values(facetOptions[facetId]).forEach((option) => (newFacetState[option.display_name] = true));
facetsState[facetId] = facetsState[facetId] || new Set<string>();
Object.values(facetOptions[facetId])
.forEach((option) => (facetsState[facetId].delete(option.display_name)));

calculateFacetsState(null, {
calculateFacetsState({
...facetsState,
[facetId]: newFacetState,
});
}

function clearFilters(): void {
calculateFacetsState({});
}

useEffect(function clearFiltersWhenTokenChange(): void {
if (clearFiltersToken) {
clearFilters();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [clearFiltersToken]);

return (
<section id={`${panelId}-facets`} className={"min-w-56 max-w-56 " + className}>
<div className="space-y-2">
Expand All @@ -157,7 +197,7 @@ export const FacetsPanel: React.FC<FacetsPanelProps> = ({
Add Facet
</button>
<button
onClick={() => calculateFacetsState(null, {})}
onClick={() => clearFilters()}
className="p-1 pr-2 text-sm text-gray-600 hover:bg-gray-100 rounded flex items-center gap-1"
>
<XMarkIcon className="h-4 w-4" />
Expand All @@ -176,7 +216,7 @@ export const FacetsPanel: React.FC<FacetsPanelProps> = ({
onSelect={(value) => toggleFacetOption(facet.id, value)}
onSelectOneOption={(value) => selectOneFacetOption(facet.id, value)}
onSelectAllOptions={() => selectAllFacetOptions(facet.id)}
facetState={facetsState[facet.id]}
facetState={getFacetState(facet.id)}
facetKey={facet.id}
renderOptionLabel={(optionDisplayName) => renderFacetOptionLabel && renderFacetOptionLabel(facet.name, optionDisplayName)}
renderIcon={(optionDisplayName) => renderFacetOptionIcon && renderFacetOptionIcon(facet.name, optionDisplayName)}
Expand Down
Loading
Loading