mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
fix(ui): address grouped Jira follow-up feedback
This commit is contained in:
@@ -86,7 +86,7 @@ describe("sendJiraDispatch", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail completed task polling when Jira dispatch has partial failures", async () => {
|
||||
it("should preserve partial success when Jira dispatch has created and failed issues", async () => {
|
||||
// Given
|
||||
pollTaskUntilSettledMock.mockResolvedValue({
|
||||
ok: true,
|
||||
@@ -99,8 +99,9 @@ describe("sendJiraDispatch", () => {
|
||||
|
||||
// Then
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error:
|
||||
success: true,
|
||||
message: "2 Jira issues were created or updated successfully.",
|
||||
warning:
|
||||
"Jira dispatch completed with 1 failed and 2 created/updated issues.",
|
||||
});
|
||||
});
|
||||
@@ -118,8 +119,9 @@ describe("sendJiraDispatch", () => {
|
||||
|
||||
// Then
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error:
|
||||
success: true,
|
||||
message: "2 Jira issues were created or updated successfully.",
|
||||
warning:
|
||||
"Jira dispatch completed with 1 failed and 2 created/updated issues.",
|
||||
});
|
||||
});
|
||||
@@ -137,8 +139,9 @@ describe("sendJiraDispatch", () => {
|
||||
|
||||
// Then
|
||||
expect(result).toEqual({
|
||||
success: false,
|
||||
error:
|
||||
success: true,
|
||||
message: "Finding successfully sent to Jira!",
|
||||
warning:
|
||||
"Jira dispatch completed with 1 failed and 1 created/updated issue.",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,6 +61,17 @@ const buildJiraDispatchFailureMessage = (
|
||||
return `Jira dispatch completed with ${failedCount} failed and ${successCount} created/updated issue${successCount === 1 ? "" : "s"}.`;
|
||||
};
|
||||
|
||||
const buildJiraDispatchSuccessMessage = (
|
||||
result: JiraTaskResult | undefined,
|
||||
) => {
|
||||
const successCount = getJiraDispatchSuccessCount(result);
|
||||
if (successCount > 1) {
|
||||
return `${successCount} Jira issues were created or updated successfully.`;
|
||||
}
|
||||
|
||||
return "Finding successfully sent to Jira!";
|
||||
};
|
||||
|
||||
export const getJiraIssueTypes = async (
|
||||
integrationId: string,
|
||||
projectKey: string,
|
||||
@@ -221,7 +232,8 @@ export const sendJiraDispatch = async ({
|
||||
export const pollJiraDispatchTask = async (
|
||||
taskId: string,
|
||||
): Promise<
|
||||
{ success: true; message: string } | { success: false; error: string }
|
||||
| { success: true; message: string; warning?: string }
|
||||
| { success: false; error: string }
|
||||
> => {
|
||||
const res = await pollTaskUntilSettled(taskId, {
|
||||
maxAttempts: 5,
|
||||
@@ -236,6 +248,15 @@ export const pollJiraDispatchTask = async (
|
||||
if (state === "completed") {
|
||||
const failedCount = getJiraDispatchFailedCount(jiraResult);
|
||||
if (failedCount > 0) {
|
||||
const successCount = getJiraDispatchSuccessCount(jiraResult);
|
||||
if (successCount > 0) {
|
||||
return {
|
||||
success: true,
|
||||
message: buildJiraDispatchSuccessMessage(jiraResult),
|
||||
warning: buildJiraDispatchFailureMessage(jiraResult, failedCount),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: buildJiraDispatchFailureMessage(jiraResult, failedCount),
|
||||
@@ -257,7 +278,10 @@ export const pollJiraDispatchTask = async (
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, message: "Finding successfully sent to Jira!" };
|
||||
return {
|
||||
success: true,
|
||||
message: buildJiraDispatchSuccessMessage(jiraResult),
|
||||
};
|
||||
}
|
||||
|
||||
if (state === "failed") {
|
||||
|
||||
@@ -60,9 +60,8 @@ async function getFindingGroupFilterOptions({
|
||||
const optionFilters = excludeFindingGroupOwnFilters(filters);
|
||||
const options = new Map<string, { checkId: string; checkTitle: string }>();
|
||||
let page = 1;
|
||||
let totalPages = 1;
|
||||
|
||||
do {
|
||||
while (true) {
|
||||
const response = await fetchFindingGroups({
|
||||
filters: optionFilters,
|
||||
page,
|
||||
@@ -76,9 +75,10 @@ async function getFindingGroupFilterOptions({
|
||||
});
|
||||
}
|
||||
|
||||
totalPages = response?.meta?.pagination?.pages ?? page;
|
||||
const totalPages = response?.meta?.pagination?.pages ?? page;
|
||||
if (page >= totalPages) break;
|
||||
page += 1;
|
||||
} while (page <= totalPages);
|
||||
}
|
||||
|
||||
return Array.from(options.values());
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ const countVisibleFilterKeys = (filters: Record<string, string[]>): number =>
|
||||
const FILTER_CONTROL_COLUMN_CLASS =
|
||||
"min-w-0 flex-none basis-full sm:basis-[calc((100%_-_0.75rem)/2)] lg:basis-[calc((100%_-_1.5rem)/3)] xl:basis-[calc((100%_-_2.25rem)/4)] 2xl:basis-[calc((100%_-_3rem)/5)]";
|
||||
const FILTER_GRID_ITEM_CLASS = "min-w-0";
|
||||
const FINDING_GROUP_FILTER_KEYS = ["filter[check_id]", "filter[check_id__in]"];
|
||||
|
||||
export const FindingsFilterBatchControls = ({
|
||||
providers,
|
||||
@@ -367,6 +368,7 @@ export const FindingsFilters = (props: FindingsFiltersProps) => {
|
||||
getFilterValue,
|
||||
} = useFilterBatch({
|
||||
defaultParams: { "filter[muted]": "false" },
|
||||
exclusiveFilterGroups: [FINDING_GROUP_FILTER_KEYS],
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,6 +2,8 @@ import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { JIRA_DISPATCH_MODE, JIRA_DISPATCH_TARGET } from "@/types/integrations";
|
||||
|
||||
import { SendToJiraModal } from "./send-to-jira-modal";
|
||||
|
||||
const {
|
||||
@@ -104,20 +106,18 @@ describe("SendToJiraModal", () => {
|
||||
onOpenChange={vi.fn()}
|
||||
findingId="check-a"
|
||||
findingTitle="Check A"
|
||||
targetIds={["check-a"]}
|
||||
targetType="check_id"
|
||||
targetBatches={[
|
||||
{
|
||||
targetIds: ["check-a"],
|
||||
targetType: "check_id",
|
||||
dispatchMode: "grouped",
|
||||
targetType: JIRA_DISPATCH_TARGET.CHECK_ID,
|
||||
dispatchMode: JIRA_DISPATCH_MODE.GROUPED,
|
||||
},
|
||||
{
|
||||
targetIds: ["finding-1", "finding-2"],
|
||||
targetType: "finding_id",
|
||||
targetType: JIRA_DISPATCH_TARGET.FINDING_ID,
|
||||
},
|
||||
]}
|
||||
defaultDispatchMode="grouped"
|
||||
defaultDispatchMode={JIRA_DISPATCH_MODE.GROUPED}
|
||||
selectedResourceCount={1}
|
||||
description="Create Jira issues for 1 Group and 2 Findings."
|
||||
/>,
|
||||
@@ -189,20 +189,18 @@ describe("SendToJiraModal", () => {
|
||||
onOpenChange={onOpenChange}
|
||||
findingId="check-a"
|
||||
findingTitle="Check A"
|
||||
targetIds={["check-a"]}
|
||||
targetType="check_id"
|
||||
targetBatches={[
|
||||
{
|
||||
targetIds: ["check-a"],
|
||||
targetType: "check_id",
|
||||
dispatchMode: "grouped",
|
||||
targetType: JIRA_DISPATCH_TARGET.CHECK_ID,
|
||||
dispatchMode: JIRA_DISPATCH_MODE.GROUPED,
|
||||
},
|
||||
{
|
||||
targetIds: ["finding-1", "finding-2"],
|
||||
targetType: "finding_id",
|
||||
targetType: JIRA_DISPATCH_TARGET.FINDING_ID,
|
||||
},
|
||||
]}
|
||||
defaultDispatchMode="grouped"
|
||||
defaultDispatchMode={JIRA_DISPATCH_MODE.GROUPED}
|
||||
selectedResourceCount={1}
|
||||
description="Create Jira issues for 1 Group and 2 Findings."
|
||||
/>,
|
||||
@@ -442,13 +440,14 @@ describe("SendToJiraModal", () => {
|
||||
expect(toastMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("shows an error toast when Jira dispatch polling reports partial failures", async () => {
|
||||
it("shows a partial success toast when a task reports created and failed issues", async () => {
|
||||
// Given
|
||||
const user = userEvent.setup();
|
||||
const onOpenChange = vi.fn();
|
||||
pollJiraDispatchTaskMock.mockResolvedValue({
|
||||
success: false,
|
||||
error: "Jira dispatch completed with 1 failed and 2 created issues.",
|
||||
success: true,
|
||||
message: "2 Jira issues were created or updated successfully.",
|
||||
warning: "Jira dispatch completed with 1 failed and 2 created issues.",
|
||||
});
|
||||
render(
|
||||
<SendToJiraModal
|
||||
@@ -472,10 +471,9 @@ describe("SendToJiraModal", () => {
|
||||
// Then
|
||||
await waitFor(() =>
|
||||
expect(toastMock).toHaveBeenCalledWith({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
title: "Partial success",
|
||||
description:
|
||||
"Jira dispatch completed with 1 failed and 2 created issues.",
|
||||
"2 Jira issues were created or updated successfully. Some Jira dispatches failed: Jira dispatch completed with 1 failed and 2 created issues.",
|
||||
}),
|
||||
);
|
||||
expect(toastMock).not.toHaveBeenCalledWith(
|
||||
|
||||
@@ -62,15 +62,15 @@ interface SendToJiraSingleTargetProps extends SendToJiraModalBaseProps {
|
||||
}
|
||||
|
||||
interface SendToJiraTargetListProps extends SendToJiraModalBaseProps {
|
||||
targetIds?: string[];
|
||||
targetType?: JiraDispatchTarget;
|
||||
targetIds: string[];
|
||||
targetType: JiraDispatchTarget;
|
||||
targetBatches?: never;
|
||||
}
|
||||
|
||||
interface SendToJiraBatchProps extends SendToJiraModalBaseProps {
|
||||
targetIds?: string[];
|
||||
targetType?: JiraDispatchTarget;
|
||||
targetBatches?: JiraDispatchTargetBatch[];
|
||||
targetIds?: never;
|
||||
targetType?: never;
|
||||
targetBatches: JiraDispatchTargetBatch[];
|
||||
}
|
||||
|
||||
type SendToJiraModalProps =
|
||||
@@ -293,15 +293,20 @@ export const SendToJiraModal = ({
|
||||
),
|
||||
...launchErrors,
|
||||
];
|
||||
const warnings = taskResults.flatMap((taskResult) => {
|
||||
if (!taskResult.success || !taskResult.warning) return [];
|
||||
return [taskResult.warning];
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
if (errors.length > 0 || warnings.length > 0) {
|
||||
const successfulTask = taskResults.find(
|
||||
(taskResult) => taskResult.success,
|
||||
);
|
||||
if (successfulTask) {
|
||||
const failedMessages = [...warnings, ...errors].join(" ");
|
||||
toast({
|
||||
title: "Partial success",
|
||||
description: `${successfulTask.message || "Some Jira issues were created successfully."} Some Jira dispatches failed: ${errors.join(" ")}`,
|
||||
description: `${successfulTask.message || "Some Jira issues were created successfully."} Some Jira dispatches failed: ${failedMessages}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import type {
|
||||
FindingTriageLoadedNote,
|
||||
FindingTriageSummary,
|
||||
} from "@/types/findings-triage";
|
||||
import { JIRA_DISPATCH_MODE } from "@/types/integrations";
|
||||
import { JIRA_DISPATCH_MODE, JIRA_DISPATCH_TARGET } from "@/types/integrations";
|
||||
|
||||
import { canMuteFindingResource } from "./finding-resource-selection";
|
||||
import {
|
||||
@@ -133,7 +133,7 @@ const ResourceRowActions = ({
|
||||
findingId={resource.findingId}
|
||||
findingTitle={resource.checkId}
|
||||
targetIds={displayIds}
|
||||
targetType="finding_id"
|
||||
targetType={JIRA_DISPATCH_TARGET.FINDING_ID}
|
||||
defaultDispatchMode={
|
||||
displayIds.length > 1
|
||||
? JIRA_DISPATCH_MODE.GROUPED
|
||||
|
||||
@@ -23,7 +23,7 @@ import type {
|
||||
FindingTriageLoadedNote,
|
||||
FindingTriageSummary,
|
||||
} from "@/types/findings-triage";
|
||||
import { JIRA_DISPATCH_MODE } from "@/types/integrations";
|
||||
import { JIRA_DISPATCH_MODE, JIRA_DISPATCH_TARGET } from "@/types/integrations";
|
||||
import type { ProviderType } from "@/types/providers";
|
||||
|
||||
import { canMuteFindingGroup } from "./finding-group-selection";
|
||||
@@ -279,7 +279,11 @@ export function DataTableRowActions<T extends FindingRowData>({
|
||||
findingId={finding.id}
|
||||
findingTitle={findingTitle}
|
||||
targetIds={jiraTargetIds}
|
||||
targetType={isGroup ? "check_id" : "finding_id"}
|
||||
targetType={
|
||||
isGroup
|
||||
? JIRA_DISPATCH_TARGET.CHECK_ID
|
||||
: JIRA_DISPATCH_TARGET.FINDING_ID
|
||||
}
|
||||
defaultDispatchMode={jiraDefaultDispatchMode}
|
||||
canChooseGroupedDispatch={canChooseGroupedJiraDispatch}
|
||||
selectedResourceCount={selectedJiraResourceCount}
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
isFindingGroupMuted,
|
||||
} from "@/lib/findings-groups";
|
||||
import { FindingGroupRow } from "@/types";
|
||||
import { JIRA_DISPATCH_MODE } from "@/types/integrations";
|
||||
import { JIRA_DISPATCH_MODE, JIRA_DISPATCH_TARGET } from "@/types/integrations";
|
||||
|
||||
import { FloatingMuteButton } from "../floating-mute-button";
|
||||
import { getColumnFindingResources } from "./column-finding-resources";
|
||||
@@ -268,7 +268,7 @@ export function FindingsGroupDrillDown({
|
||||
findingId={selectedFindingIds[0] ?? ""}
|
||||
findingTitle={group.checkTitle}
|
||||
targetIds={selectedFindingIds}
|
||||
targetType="finding_id"
|
||||
targetType={JIRA_DISPATCH_TARGET.FINDING_ID}
|
||||
defaultDispatchMode={
|
||||
selectedFindingIds.length > 1
|
||||
? JIRA_DISPATCH_MODE.GROUPED
|
||||
|
||||
@@ -573,8 +573,6 @@ describe("FindingsGroupTable", () => {
|
||||
const lastCall = SendToJiraModalMock.mock.calls.at(-1)?.[0];
|
||||
expect(lastCall).toMatchObject({
|
||||
isOpen: true,
|
||||
targetIds: ["check-a"],
|
||||
targetType: "check_id",
|
||||
targetBatches: [
|
||||
{
|
||||
targetIds: ["check-a"],
|
||||
@@ -641,8 +639,6 @@ describe("FindingsGroupTable", () => {
|
||||
const lastCall = SendToJiraModalMock.mock.calls.at(-1)?.[0];
|
||||
expect(lastCall).toMatchObject({
|
||||
isOpen: true,
|
||||
targetIds: ["check-b"],
|
||||
targetType: "check_id",
|
||||
targetBatches: [
|
||||
{
|
||||
targetIds: ["check-b"],
|
||||
@@ -711,8 +707,6 @@ describe("FindingsGroupTable", () => {
|
||||
};
|
||||
expect(lastCall).toMatchObject({
|
||||
isOpen: true,
|
||||
targetIds: ["check-b"],
|
||||
targetType: "check_id",
|
||||
targetBatches: [
|
||||
{
|
||||
targetIds: ["check-b"],
|
||||
|
||||
@@ -17,7 +17,7 @@ import { canDrillDownFindingGroup } from "@/lib/findings-groups";
|
||||
import { getFlowById } from "@/lib/onboarding";
|
||||
import { createExploreFindingsTourStepHandlers } from "@/lib/tours/explore-findings.tour";
|
||||
import { FindingGroupRow, MetaDataProps } from "@/types";
|
||||
import { JIRA_DISPATCH_MODE } from "@/types/integrations";
|
||||
import { JIRA_DISPATCH_MODE, JIRA_DISPATCH_TARGET } from "@/types/integrations";
|
||||
|
||||
import { FloatingMuteButton } from "../floating-mute-button";
|
||||
import { getColumnFindingGroups } from "./column-finding-groups";
|
||||
@@ -171,8 +171,8 @@ export function FindingsGroupTable({
|
||||
? selectedCheckIds
|
||||
: resourceSelection;
|
||||
const jiraTargetType = jiraGroupSelectionTakesPrecedence
|
||||
? "check_id"
|
||||
: "finding_id";
|
||||
? JIRA_DISPATCH_TARGET.CHECK_ID
|
||||
: JIRA_DISPATCH_TARGET.FINDING_ID;
|
||||
const singleSelectedGroup =
|
||||
selectedCheckIds.length === 1
|
||||
? selectedFindings.find(
|
||||
@@ -203,12 +203,12 @@ export function FindingsGroupTable({
|
||||
? [
|
||||
{
|
||||
targetIds: selectedCheckIds,
|
||||
targetType: "check_id" as const,
|
||||
targetType: JIRA_DISPATCH_TARGET.CHECK_ID,
|
||||
dispatchMode: JIRA_DISPATCH_MODE.GROUPED,
|
||||
},
|
||||
{
|
||||
targetIds: resourceSelection,
|
||||
targetType: "finding_id" as const,
|
||||
targetType: JIRA_DISPATCH_TARGET.FINDING_ID,
|
||||
...(resourceSelection.length > 1
|
||||
? {}
|
||||
: { dispatchMode: JIRA_DISPATCH_MODE.INDIVIDUAL }),
|
||||
@@ -438,7 +438,24 @@ export function FindingsGroupTable({
|
||||
/>
|
||||
)}
|
||||
|
||||
{canSendToJira && (
|
||||
{canSendToJira && jiraBatches && (
|
||||
<SendToJiraModal
|
||||
isOpen={isJiraModalOpen}
|
||||
onOpenChange={setIsJiraModalOpen}
|
||||
findingId={jiraTargetIds[0] ?? ""}
|
||||
findingTitle={jiraTitle}
|
||||
targetBatches={jiraBatches}
|
||||
defaultDispatchMode={jiraDispatchMode}
|
||||
isFindingGroupSelection={
|
||||
!jiraGroupSelectionTakesPrecedence && Boolean(expandedGroup)
|
||||
}
|
||||
canChooseGroupedDispatch={canChooseGroupedJiraDispatch}
|
||||
selectedResourceCount={selectedJiraResourceCount}
|
||||
description={jiraDescription}
|
||||
/>
|
||||
)}
|
||||
|
||||
{canSendToJira && !jiraBatches && (
|
||||
<SendToJiraModal
|
||||
isOpen={isJiraModalOpen}
|
||||
onOpenChange={setIsJiraModalOpen}
|
||||
@@ -446,7 +463,6 @@ export function FindingsGroupTable({
|
||||
findingTitle={jiraTitle}
|
||||
targetIds={jiraTargetIds}
|
||||
targetType={jiraTargetType}
|
||||
targetBatches={jiraBatches}
|
||||
defaultDispatchMode={jiraDispatchMode}
|
||||
isFindingGroupSelection={
|
||||
!jiraGroupSelectionTakesPrecedence && Boolean(expandedGroup)
|
||||
|
||||
@@ -198,6 +198,30 @@ describe("useFilterBatch", () => {
|
||||
// Then
|
||||
expect(result.current.pendingFilters["filter[severity__in]"]).toEqual([]);
|
||||
});
|
||||
|
||||
it("should replace exclusive legacy filters when the new grouped filter is edited", () => {
|
||||
// Given - a legacy deep link with the exact check_id filter applied.
|
||||
setSearchParams({
|
||||
"filter[check_id]": "teams_external_users_can_join",
|
||||
});
|
||||
const { result } = renderHook(() =>
|
||||
useFilterBatch({
|
||||
exclusiveFilterGroups: [["filter[check_id]", "filter[check_id__in]"]],
|
||||
}),
|
||||
);
|
||||
|
||||
// When - the Finding Group multi-select writes the __in filter.
|
||||
act(() => {
|
||||
result.current.setPending("filter[check_id__in]", [
|
||||
"teams_external_users_cannot_join",
|
||||
]);
|
||||
});
|
||||
|
||||
// Then - the stale exact filter is removed from pending state.
|
||||
expect(result.current.pendingFilters).toEqual({
|
||||
"filter[check_id__in]": ["teams_external_users_cannot_join"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── getFilterValue ─────────────────────────────────────────────────────────
|
||||
@@ -246,6 +270,24 @@ describe("useFilterBatch", () => {
|
||||
// Then
|
||||
expect(values).toEqual(["critical"]);
|
||||
});
|
||||
|
||||
it("should expose legacy exclusive filter values through the replacement key", () => {
|
||||
// Given - a legacy grouped finding deep link uses filter[check_id].
|
||||
setSearchParams({
|
||||
"filter[check_id]": "teams_external_users_can_join",
|
||||
});
|
||||
const { result } = renderHook(() =>
|
||||
useFilterBatch({
|
||||
exclusiveFilterGroups: [["filter[check_id]", "filter[check_id__in]"]],
|
||||
}),
|
||||
);
|
||||
|
||||
// When - the new Finding Group control asks for filter[check_id__in].
|
||||
const values = result.current.getFilterValue("filter[check_id__in]");
|
||||
|
||||
// Then - the existing exact value appears selected in the control.
|
||||
expect(values).toEqual(["teams_external_users_can_join"]);
|
||||
});
|
||||
});
|
||||
|
||||
// ── hasChanges & changeCount ───────────────────────────────────────────────
|
||||
|
||||
@@ -151,6 +151,23 @@ export interface UseFilterBatchOptions {
|
||||
* (e.g. `{ "filter[muted]": "false" }` on the Findings page).
|
||||
*/
|
||||
defaultParams?: Record<string, string>;
|
||||
/**
|
||||
* Filter keys that represent the same logical control. Updating one removes
|
||||
* the others so legacy exact params and new multi-select params cannot be
|
||||
* applied together.
|
||||
*/
|
||||
exclusiveFilterGroups?: string[][];
|
||||
}
|
||||
|
||||
function normalizeFilterKey(key: string): string {
|
||||
return key.startsWith("filter[") ? key : `filter[${key}]`;
|
||||
}
|
||||
|
||||
function getExclusiveFilterGroup(
|
||||
filterKey: string,
|
||||
exclusiveFilterGroups: string[][] | undefined,
|
||||
): string[] | undefined {
|
||||
return exclusiveFilterGroups?.find((group) => group.includes(filterKey));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,15 +204,27 @@ export const useFilterBatch = (
|
||||
}, [searchParams]);
|
||||
|
||||
const setPending = (key: string, values: string[]) => {
|
||||
const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`;
|
||||
setPendingFilters((prev) => ({
|
||||
...prev,
|
||||
[filterKey]: values,
|
||||
}));
|
||||
const filterKey = normalizeFilterKey(key);
|
||||
const exclusiveGroup = getExclusiveFilterGroup(
|
||||
filterKey,
|
||||
options?.exclusiveFilterGroups,
|
||||
);
|
||||
setPendingFilters((prev) => {
|
||||
const next = { ...prev };
|
||||
|
||||
exclusiveGroup
|
||||
?.filter((exclusiveKey) => exclusiveKey !== filterKey)
|
||||
.forEach((exclusiveKey) => {
|
||||
delete next[exclusiveKey];
|
||||
});
|
||||
|
||||
next[filterKey] = values;
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const removePending = (key: string) => {
|
||||
const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`;
|
||||
const filterKey = normalizeFilterKey(key);
|
||||
setPendingFilters((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[filterKey];
|
||||
@@ -265,7 +294,7 @@ export const useFilterBatch = (
|
||||
};
|
||||
|
||||
const removeAppliedAndApply = (key: string, value?: string) => {
|
||||
const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`;
|
||||
const filterKey = normalizeFilterKey(key);
|
||||
const applied = deriveAppliedFromUrl(
|
||||
new URLSearchParams(searchParams.toString()),
|
||||
);
|
||||
@@ -286,8 +315,20 @@ export const useFilterBatch = (
|
||||
};
|
||||
|
||||
const getFilterValue = (key: string): string[] => {
|
||||
const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`;
|
||||
return pendingFilters[filterKey] ?? [];
|
||||
const filterKey = normalizeFilterKey(key);
|
||||
const values = pendingFilters[filterKey];
|
||||
if (values) return values;
|
||||
|
||||
const exclusiveGroup = getExclusiveFilterGroup(
|
||||
filterKey,
|
||||
options?.exclusiveFilterGroups,
|
||||
);
|
||||
const alternateKey = exclusiveGroup?.find(
|
||||
(exclusiveKey) =>
|
||||
exclusiveKey !== filterKey && pendingFilters[exclusiveKey],
|
||||
);
|
||||
|
||||
return alternateKey ? pendingFilters[alternateKey] : [];
|
||||
};
|
||||
|
||||
const hasChanges = !areFiltersEqual(pendingFilters, appliedFilters);
|
||||
|
||||
@@ -10,54 +10,6 @@ describe("enterprise feature flags", () => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("should enable billing and PostHog by default", async () => {
|
||||
// Given / When
|
||||
const { isBillingEnabled, isPostHogEnabled } = await importFresh();
|
||||
|
||||
// Then
|
||||
expect(isBillingEnabled()).toBe(true);
|
||||
expect(isPostHogEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it("should disable billing independently from PostHog", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_PROWLER_ENTERPRISE_BILLING_ENABLED", "false");
|
||||
vi.stubEnv("NEXT_PUBLIC_PROWLER_ENTERPRISE_POSTHOG_ENABLED", "true");
|
||||
|
||||
// When
|
||||
const { isBillingEnabled, isPostHogEnabled } = await importFresh();
|
||||
|
||||
// Then
|
||||
expect(isBillingEnabled()).toBe(false);
|
||||
expect(isPostHogEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it("should disable PostHog when billing and PostHog are disabled", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_PROWLER_ENTERPRISE_BILLING_ENABLED", "false");
|
||||
vi.stubEnv("NEXT_PUBLIC_PROWLER_ENTERPRISE_POSTHOG_ENABLED", "false");
|
||||
|
||||
// When
|
||||
const { isBillingEnabled, isPostHogEnabled } = await importFresh();
|
||||
|
||||
// Then
|
||||
expect(isBillingEnabled()).toBe(false);
|
||||
expect(isPostHogEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it("should force PostHog on when billing is enabled", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_PROWLER_ENTERPRISE_BILLING_ENABLED", "true");
|
||||
vi.stubEnv("NEXT_PUBLIC_PROWLER_ENTERPRISE_POSTHOG_ENABLED", "false");
|
||||
|
||||
// When
|
||||
const { isBillingEnabled, isPostHogEnabled } = await importFresh();
|
||||
|
||||
// Then
|
||||
expect(isBillingEnabled()).toBe(true);
|
||||
expect(isPostHogEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it("should keep grouped Jira dispatch disabled by default", async () => {
|
||||
// Given / When
|
||||
const { isGroupedJiraDispatchEnabled } = await importFresh();
|
||||
|
||||
+1
-17
@@ -4,10 +4,8 @@ export const DEPLOYMENT_MODE = {
|
||||
} as const;
|
||||
|
||||
export const ENTERPRISE_FEATURE_ENV = {
|
||||
BILLING_ENABLED: "NEXT_PUBLIC_PROWLER_ENTERPRISE_BILLING_ENABLED",
|
||||
GROUPED_JIRA_DISPATCH_ENABLED:
|
||||
"NEXT_PUBLIC_PROWLER_ENTERPRISE_GROUPED_JIRA_DISPATCH_ENABLED",
|
||||
POSTHOG_ENABLED: "NEXT_PUBLIC_PROWLER_ENTERPRISE_POSTHOG_ENABLED",
|
||||
} as const;
|
||||
|
||||
export const PROWLER_CLOUD_ONLY_TOOLTIP = "Available only in Prowler Cloud";
|
||||
@@ -21,16 +19,12 @@ type EnterpriseFeatureEnv =
|
||||
const getEnterpriseFeatureValue = (
|
||||
envName: EnterpriseFeatureEnv,
|
||||
): string | undefined => {
|
||||
if (envName === ENTERPRISE_FEATURE_ENV.BILLING_ENABLED) {
|
||||
return process.env.NEXT_PUBLIC_PROWLER_ENTERPRISE_BILLING_ENABLED;
|
||||
}
|
||||
|
||||
if (envName === ENTERPRISE_FEATURE_ENV.GROUPED_JIRA_DISPATCH_ENABLED) {
|
||||
return process.env
|
||||
.NEXT_PUBLIC_PROWLER_ENTERPRISE_GROUPED_JIRA_DISPATCH_ENABLED;
|
||||
}
|
||||
|
||||
return process.env.NEXT_PUBLIC_PROWLER_ENTERPRISE_POSTHOG_ENABLED;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getBooleanEnv = (
|
||||
@@ -56,16 +50,6 @@ export const getDeploymentMode = (): DeploymentMode | undefined => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const isOnPremiseDeployment = (): boolean =>
|
||||
getDeploymentMode() === DEPLOYMENT_MODE.ON_PREMISE;
|
||||
|
||||
export const isBillingEnabled = (): boolean =>
|
||||
getBooleanEnv(ENTERPRISE_FEATURE_ENV.BILLING_ENABLED, true);
|
||||
|
||||
export const isPostHogEnabled = (): boolean =>
|
||||
isBillingEnabled() ||
|
||||
getBooleanEnv(ENTERPRISE_FEATURE_ENV.POSTHOG_ENABLED, true);
|
||||
|
||||
export const isGroupedJiraDispatchEnabled = (): boolean =>
|
||||
getDeploymentMode() === DEPLOYMENT_MODE.CLOUD &&
|
||||
getBooleanEnv(ENTERPRISE_FEATURE_ENV.GROUPED_JIRA_DISPATCH_ENABLED, false);
|
||||
|
||||
Reference in New Issue
Block a user