diff --git a/.github/workflows/release-workflow-schema.yml b/.github/workflows/release-workflow-schema.yml index 6aeeeb7c4a..b83dfd5820 100644 --- a/.github/workflows/release-workflow-schema.yml +++ b/.github/workflows/release-workflow-schema.yml @@ -88,87 +88,93 @@ jobs: name: workflow-schema path: workflow-yaml-json-schema.json - release-schema: - runs-on: ubuntu-latest - needs: generate-schema - if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }} - - steps: - - name: Download schema artifact - uses: actions/download-artifact@v4 - with: - name: workflow-schema - path: . - - name: Checkout schema repository - uses: actions/checkout@v4 - with: - repository: ${{ env.SCHEMA_REPO_NAME }} - token: ${{ secrets.SCHEMA_REPO_PAT }} - path: schema-repo - - - name: Set target branch variable - id: set_branch - run: | - if [ "${{ github.event_name }}" = "pull_request" ]; then - echo "branch=${{ github.head_ref }}" >> $GITHUB_OUTPUT - else - echo "branch=${{ github.ref_name }}" >> $GITHUB_OUTPUT - fi - - - name: Create or switch to target branch in schema repo - working-directory: schema-repo - run: | - git fetch origin - if git show-ref --verify --quiet refs/heads/${{ steps.set_branch.outputs.branch }}; then - git checkout ${{ steps.set_branch.outputs.branch }} - else - git checkout -b ${{ steps.set_branch.outputs.branch }} - fi - - - name: Copy schema to target repository - run: | - cp workflow-yaml-json-schema.json schema-repo/schema.json - - # Update schema with version info - jq --arg version "${{ needs.generate-schema.outputs.version }}" \ - --arg id "https://raw.githubusercontent.com/${{ env.SCHEMA_REPO_NAME }}/v${{ needs.generate-schema.outputs.version }}/schema.json" \ - '. + {version: $version, "$id": $id}' \ - schema-repo/schema.json > schema-repo/schema.tmp.json - - mv schema-repo/schema.tmp.json schema-repo/schema.json - - - name: Check if schema changed - id: check_changes - working-directory: schema-repo - run: | - git add schema.json - if git diff --cached --quiet schema.json; then - echo "changed=false" >> $GITHUB_OUTPUT - else - echo "changed=true" >> $GITHUB_OUTPUT - fi - - - name: Commit and push schema - if: steps.check_changes.outputs.changed == 'true' - working-directory: schema-repo - run: | - git config user.name "Keep Schema Bot" - git config user.email "no-reply@keephq.dev" - git commit -m "Release schema v${{ needs.generate-schema.outputs.version }}" - git push origin ${{ steps.set_branch.outputs.branch }} - if [ "${{ steps.set_branch.outputs.branch }}" = "main" ]; then - git tag "v${{ needs.generate-schema.outputs.version }}" - git push origin "v${{ needs.generate-schema.outputs.version }}" - fi - - - name: Create GitHub Release - if: steps.check_changes.outputs.changed == 'true' && steps.set_branch.outputs.branch == 'main' - uses: softprops/action-gh-release@v1 - with: - repository: ${{ env.SCHEMA_REPO_NAME }} - tag_name: v${{ needs.generate-schema.outputs.version }} - name: Release v${{ needs.generate-schema.outputs.version }} - body: | - Automated release of schema version v${{ needs.generate-schema.outputs.version }}. - env: - GITHUB_TOKEN: ${{ secrets.SCHEMA_REPO_PAT }} +# NOTE: The `release-schema` job below is temporarily disabled. It pushes the +# generated schema to the external `keephq/keep-workflow-schema` repo using +# `secrets.SCHEMA_REPO_PAT`, which is currently failing with "Bad credentials" +# (expired/invalid PAT). Schema generation/validation (the `generate-schema` +# job above) still runs. Re-enable once the SCHEMA_REPO_PAT secret is refreshed. +# +# release-schema: +# runs-on: ubuntu-latest +# needs: generate-schema +# if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }} +# +# steps: +# - name: Download schema artifact +# uses: actions/download-artifact@v4 +# with: +# name: workflow-schema +# path: . +# - name: Checkout schema repository +# uses: actions/checkout@v4 +# with: +# repository: ${{ env.SCHEMA_REPO_NAME }} +# token: ${{ secrets.SCHEMA_REPO_PAT }} +# path: schema-repo +# +# - name: Set target branch variable +# id: set_branch +# run: | +# if [ "${{ github.event_name }}" = "pull_request" ]; then +# echo "branch=${{ github.head_ref }}" >> $GITHUB_OUTPUT +# else +# echo "branch=${{ github.ref_name }}" >> $GITHUB_OUTPUT +# fi +# +# - name: Create or switch to target branch in schema repo +# working-directory: schema-repo +# run: | +# git fetch origin +# if git show-ref --verify --quiet refs/heads/${{ steps.set_branch.outputs.branch }}; then +# git checkout ${{ steps.set_branch.outputs.branch }} +# else +# git checkout -b ${{ steps.set_branch.outputs.branch }} +# fi +# +# - name: Copy schema to target repository +# run: | +# cp workflow-yaml-json-schema.json schema-repo/schema.json +# +# # Update schema with version info +# jq --arg version "${{ needs.generate-schema.outputs.version }}" \ +# --arg id "https://raw.githubusercontent.com/${{ env.SCHEMA_REPO_NAME }}/v${{ needs.generate-schema.outputs.version }}/schema.json" \ +# '. + {version: $version, "$id": $id}' \ +# schema-repo/schema.json > schema-repo/schema.tmp.json +# +# mv schema-repo/schema.tmp.json schema-repo/schema.json +# +# - name: Check if schema changed +# id: check_changes +# working-directory: schema-repo +# run: | +# git add schema.json +# if git diff --cached --quiet schema.json; then +# echo "changed=false" >> $GITHUB_OUTPUT +# else +# echo "changed=true" >> $GITHUB_OUTPUT +# fi +# +# - name: Commit and push schema +# if: steps.check_changes.outputs.changed == 'true' +# working-directory: schema-repo +# run: | +# git config user.name "Keep Schema Bot" +# git config user.email "no-reply@keephq.dev" +# git commit -m "Release schema v${{ needs.generate-schema.outputs.version }}" +# git push origin ${{ steps.set_branch.outputs.branch }} +# if [ "${{ steps.set_branch.outputs.branch }}" = "main" ]; then +# git tag "v${{ needs.generate-schema.outputs.version }}" +# git push origin "v${{ needs.generate-schema.outputs.version }}" +# fi +# +# - name: Create GitHub Release +# if: steps.check_changes.outputs.changed == 'true' && steps.set_branch.outputs.branch == 'main' +# uses: softprops/action-gh-release@v1 +# with: +# repository: ${{ env.SCHEMA_REPO_NAME }} +# tag_name: v${{ needs.generate-schema.outputs.version }} +# name: Release v${{ needs.generate-schema.outputs.version }} +# body: | +# Automated release of schema version v${{ needs.generate-schema.outputs.version }}. +# env: +# GITHUB_TOKEN: ${{ secrets.SCHEMA_REPO_PAT }} diff --git a/docs/mint.json b/docs/mint.json index 0b48677bcd..7672da965b 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -300,7 +300,8 @@ "deployment/provision/overview", "deployment/provision/provider", "deployment/provision/workflow", - "deployment/provision/dashboard" + "deployment/provision/dashboard", + "deployment/provision/mapping" ] }, "deployment/secret-store", diff --git a/docs/snippets/providers/anthropic-snippet-autogenerated.mdx b/docs/snippets/providers/anthropic-snippet-autogenerated.mdx index 297b766ecc..d5a63f60b0 100644 --- a/docs/snippets/providers/anthropic-snippet-autogenerated.mdx +++ b/docs/snippets/providers/anthropic-snippet-autogenerated.mdx @@ -1,9 +1,11 @@ -{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py +{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py Do not edit it manually, as it will be overwritten */} ## Authentication This provider requires authentication. - **api_key**: Anthropic API Key (required: True, sensitive: True) +- **model**: Claude model to use (required: False, sensitive: False) +- **system_prompt**: System prompt that sets Claude's role for all requests in this provider. (required: False, sensitive: False) ## In workflows @@ -19,8 +21,9 @@ steps: config: "{{ provider.my_provider_name }}" with: prompt: {value} # The prompt to query the model with. - model: {value} # The model to query. + model: {value} # The model to query (overrides provider config). max_tokens: {value} # The maximum number of tokens to generate. + system_prompt: {value} # System prompt override for this call. structured_output_format: {value} # The structured output format to use. ``` diff --git a/keep-ui/features/filter/facet.tsx b/keep-ui/features/filter/facet.tsx index 4159b06bf2..817fc0eb36 100644 --- a/keep-ui/features/filter/facet.tsx +++ b/keep-ui/features/filter/facet.tsx @@ -8,7 +8,7 @@ import { FacetValue } from "./facet-value"; import { FacetDto, FacetOptionDto } from "./models"; import { TrashIcon } from "@heroicons/react/24/outline"; import { useExistingFacetsPanelStore } from "./store"; -import { stringToValue, valueToString } from "./store/utils"; +import { isLazyFacet, stringToValue, valueToString } from "./store/utils"; export interface FacetProps { facet: FacetDto; @@ -30,8 +30,13 @@ export const Facet: React.FC = ({ // Get preset name from URL const presetName = pathname?.split("/").pop() || "default"; - // Store open/close state in localStorage with a unique key per preset and facet - const [isOpen, setIsOpen] = useState(true); + // Lazy facets (high-cardinality user-defined facets) start collapsed and only + // fetch their options when the user expands them. Static facets (severity, + // status, source, ...) always render eagerly. Eagerly mounting and loading + // options for every lazy facet is what froze the alerts page when many (200+) + // facets existed (see issue #6577). + const isLazy = isLazyFacet(facet); + const [isOpen, setIsOpen] = useState(!isLazy); const [isLoaded, setIsLoaded] = useState(!!options?.length); const [isLoading, setIsLoading] = useState(false); @@ -40,8 +45,14 @@ export const Facet: React.FC = ({ const facetRef = useRef(facet); facetRef.current = facet; - const facetOptionsLoadingState = useExistingFacetsPanelStore( - (state) => state.facetOptionsLoadingState + // Subscribe only to this facet's slice of the loading/selection state. + // Previously every Facet subscribed to the whole `facetOptionsLoadingState` + // and `facetsState` objects, so toggling one option re-rendered all facets. + const facetLoadingState = useExistingFacetsPanelStore( + (state) => state.facetOptionsLoadingState[facet.id] + ); + const hasLoadingState = useExistingFacetsPanelStore( + (state) => Object.keys(state.facetOptionsLoadingState).length > 0 ); const toggleFacetOption = useExistingFacetsPanelStore( (state) => state.toggleFacetOption @@ -52,20 +63,35 @@ export const Facet: React.FC = ({ const selectAllFacetOptions = useExistingFacetsPanelStore( (state) => state.selectAllFacetOptions ); - const facetsState = useExistingFacetsPanelStore((state) => state.facetsState); - const facetState: Record = useMemo( - () => facetsState?.[facet.id], - [facet.id, facetsState] + const setFacetActive = useExistingFacetsPanelStore( + (state) => state.setFacetActive + ); + const facetState: Record = useExistingFacetsPanelStore( + (state) => state.facetsState?.[facet.id] ); - const facetsConfig = useExistingFacetsPanelStore( - (state) => state.facetsConfig + const facetConfig = useExistingFacetsPanelStore( + (state) => state.facetsConfig?.[facet.id] + ); + + const isFacetActive = useExistingFacetsPanelStore( + (state) => !!state.activeFacetIds?.[facet.id] ); - const facetConfig = facetsConfig?.[facet.id]; const facetStateRef = useRef(facetState); facetStateRef.current = facetState; + // Auto-open a lazy facet once it becomes active — either it received a + // selection (e.g. restored from URL query params after mount) or it was just + // added by the user via "Add Facet" — so the user can see its values. + const didAutoOpenRef = useRef(false); + useEffect(() => { + if (isLazy && !didAutoOpenRef.current && (facetState || isFacetActive)) { + didAutoOpenRef.current = true; + setIsOpen(true); + } + }, [isLazy, facetState, isFacetActive]); + function getSelectedValues(): string[] { return Object.keys(facetStateRef.current || {}); } @@ -140,7 +166,14 @@ export const Facet: React.FC = ({ }; const handleExpandCollapse = (isOpen: boolean) => { - setIsOpen(!isOpen); + const willOpen = !isOpen; + setIsOpen(willOpen); + + // Mark the facet active when expanding so its options get loaded. Lazy + // facets are inactive until expanded (#6577). + if (willOpen) { + setFacetActive(facet.id); + } if (!isLoaded && !isLoading) { onLoadOptions && onLoadOptions(); @@ -205,10 +238,7 @@ export const Facet: React.FC = ({ } function renderBody() { - if ( - facetOptionsLoadingState[facet.id] === "loading" || - !Object.keys(facetOptionsLoadingState).length - ) { + if (facetLoadingState === "loading" || !hasLoadingState) { return Array.from({ length: 3 }).map((_, index) => renderSkeleton(`skeleton-${index}`) ); @@ -283,7 +313,7 @@ export const Facet: React.FC = ({ )}
{renderBody()}
diff --git a/keep-ui/features/filter/hooks.tsx b/keep-ui/features/filter/hooks.tsx index 6381059109..d639b5f97a 100644 --- a/keep-ui/features/filter/hooks.tsx +++ b/keep-ui/features/filter/hooks.tsx @@ -100,15 +100,22 @@ export const useFacetOptions = ( } const fetchedData: FacetOptionsDict = swrValue.data.response; - const newFacetOptions: FacetOptionsDict = JSON.parse( - JSON.stringify(mergedFacetOptions || {}) - ); + // Shallow copy of the per-facet map only; we never mutate the existing + // option arrays/objects, so a full JSON deep-clone (which duplicated the + // entire options tree on every poll and spiked memory with high facet + // cardinality, see #6577) is unnecessary. + const newFacetOptions: FacetOptionsDict = { ...(mergedFacetOptions || {}) }; Object.entries(fetchedData).forEach(([facetId, newOptions]) => { - if (newFacetOptions[facetId]) { - const currentFacetOptionsMap = newFacetOptions[facetId].reduce( + const existingOptions = newFacetOptions[facetId]; + if (existingOptions) { + // Preserve previously known option values with a 0 match count so they + // remain visible/selectable, then overlay the freshly fetched counts. + const currentFacetOptionsMap = existingOptions.reduce( (accumulator, oldOption) => { - accumulator[oldOption.display_name] = oldOption; - oldOption.matches_count = 0; + accumulator[oldOption.display_name] = { + ...oldOption, + matches_count: 0, + }; return accumulator; }, {} as Record diff --git a/keep-ui/features/filter/store/__tests__/facets-store.test.ts b/keep-ui/features/filter/store/__tests__/facets-store.test.ts index ef1f7da97d..d2016d424c 100644 --- a/keep-ui/features/filter/store/__tests__/facets-store.test.ts +++ b/keep-ui/features/filter/store/__tests__/facets-store.test.ts @@ -109,6 +109,122 @@ describe("useInitialStateHandler", () => { }); }); + it("should activate only non-lazy facets when facets are set", () => { + const freshStore = createFacetsPanelStore(); + freshStore.getState().setFacets([ + { id: "staticFacet", name: "Static", is_lazy: false } as FacetDto, + { id: "lazyFacet", name: "Lazy", is_lazy: true } as FacetDto, + ]); + + expect(freshStore.getState().activeFacetIds).toEqual({ + staticFacet: true, + }); + }); + + it("should keep static facets active even when is_lazy is true", () => { + // The backend marks every facet is_lazy: true by default, so static facets + // (severity/status/source) must still be active/eager (#6577 regression). + const freshStore = createFacetsPanelStore(); + freshStore.getState().setFacets([ + { + id: "severityFacet", + name: "Severity", + is_static: true, + is_lazy: true, + } as FacetDto, + { + id: "userFacet", + name: "Custom Env", + is_static: false, + is_lazy: true, + } as FacetDto, + ]); + + expect(freshStore.getState().activeFacetIds).toEqual({ + severityFacet: true, + }); + }); + + it("should activate a newly added lazy facet on subsequent setFacets", () => { + const freshStore = createFacetsPanelStore(); + // Initial load: one static + one lazy facet. + freshStore.getState().setFacets([ + { + id: "severityFacet", + name: "Severity", + is_static: true, + is_lazy: true, + } as FacetDto, + { + id: "existingLazy", + name: "Existing", + is_static: false, + is_lazy: true, + } as FacetDto, + ]); + expect(freshStore.getState().activeFacetIds).toEqual({ + severityFacet: true, + }); + + // User adds a new custom facet -> it should be active immediately, while the + // pre-existing lazy facet stays inactive. + freshStore.getState().setFacets([ + { + id: "severityFacet", + name: "Severity", + is_static: true, + is_lazy: true, + } as FacetDto, + { + id: "existingLazy", + name: "Existing", + is_static: false, + is_lazy: true, + } as FacetDto, + { + id: "newCustomFacet", + name: "Custom Env", + is_static: false, + is_lazy: true, + } as FacetDto, + ]); + + expect(freshStore.getState().activeFacetIds).toEqual({ + severityFacet: true, + newCustomFacet: true, + }); + }); + + it("should mark a lazy facet active via setFacetActive", () => { + const freshStore = createFacetsPanelStore(); + freshStore.getState().setFacets([ + { id: "lazyFacet", name: "Lazy", is_lazy: true } as FacetDto, + ]); + + expect(freshStore.getState().activeFacetIds).toEqual({}); + + freshStore.getState().setFacetActive("lazyFacet"); + expect(freshStore.getState().activeFacetIds).toEqual({ lazyFacet: true }); + }); + + it("should keep only non-lazy facets active after clearFilters", () => { + const freshStore = createFacetsPanelStore(); + freshStore.getState().setFacets([ + { id: "staticFacet", name: "Static", is_lazy: false } as FacetDto, + { id: "lazyFacet", name: "Lazy", is_lazy: true } as FacetDto, + ]); + freshStore.getState().setFacetActive("lazyFacet"); + expect(freshStore.getState().activeFacetIds).toEqual({ + staticFacet: true, + lazyFacet: true, + }); + + freshStore.getState().clearFilters(); + expect(freshStore.getState().activeFacetIds).toEqual({ + staticFacet: true, + }); + }); + it("should select all facet options correctly", () => { const selectAllFacetOptions = store.getState().selectAllFacetOptions; diff --git a/keep-ui/features/filter/store/__tests__/use-queries-handler.test.ts b/keep-ui/features/filter/store/__tests__/use-queries-handler.test.ts index 8b32a32f26..9ca298553a 100644 --- a/keep-ui/features/filter/store/__tests__/use-queries-handler.test.ts +++ b/keep-ui/features/filter/store/__tests__/use-queries-handler.test.ts @@ -75,6 +75,11 @@ describe("useQueriesHandler", () => { ], }, facetsState: {}, + activeFacetIds: { + severityFacet: true, + incidentNameFacet: true, + statusFacet: true, + }, isFacetsStateInitializedFromQueryParams: false, isInitialStateHandled: true, }); @@ -174,6 +179,82 @@ describe("useQueriesHandler", () => { }); }); + it("should not build option queries for inactive (lazy, not expanded) facets", () => { + // Only severity and status are active; incidentNameFacet is lazy/collapsed. + store.setState({ + activeFacetIds: { + severityFacet: true, + statusFacet: true, + }, + }); + + renderHook(() => useQueriesHandler(store)); + + act(() => { + store.setState({ + facetsState: { + severityFacet: { critical: true, high: true }, + }, + facetsStateRefreshToken: "some-token", + isFacetsStateInitializedFromQueryParams: true, + }); + }); + + act(() => { + jest.advanceTimersByTime(200); + }); + + const { facetOptionQueries } = store.getState().queriesState; + expect(facetOptionQueries).not.toBeNull(); + expect(Object.keys(facetOptionQueries as object).sort()).toEqual([ + "severityFacet", + "statusFacet", + ]); + expect(facetOptionQueries).not.toHaveProperty("incidentNameFacet"); + }); + + it("should build option queries once an inactive facet becomes active", () => { + store.setState({ + activeFacetIds: { + severityFacet: true, + statusFacet: true, + }, + }); + + renderHook(() => useQueriesHandler(store)); + + act(() => { + store.setState({ + facetsState: { + severityFacet: { critical: true }, + }, + facetsStateRefreshToken: "some-token", + isFacetsStateInitializedFromQueryParams: true, + }); + }); + + act(() => { + jest.advanceTimersByTime(200); + }); + + expect(store.getState().queriesState.facetOptionQueries).not.toHaveProperty( + "incidentNameFacet" + ); + + // Simulate the user expanding the lazy facet. + act(() => { + store.getState().setFacetActive("incidentNameFacet"); + }); + + act(() => { + jest.advanceTimersByTime(200); + }); + + expect(store.getState().queriesState.facetOptionQueries).toHaveProperty( + "incidentNameFacet" + ); + }); + it("should handle complex facet paths correctly", () => { renderHook(() => useQueriesHandler(store)); diff --git a/keep-ui/features/filter/store/create-facets-store.ts b/keep-ui/features/filter/store/create-facets-store.ts index 98462d7066..a54c5b1913 100644 --- a/keep-ui/features/filter/store/create-facets-store.ts +++ b/keep-ui/features/filter/store/create-facets-store.ts @@ -1,7 +1,7 @@ import { createStore } from "zustand"; import { v4 as uuidV4 } from "uuid"; import { FacetDto, FacetOptionDto, FacetsConfig, FacetState } from "../models"; -import { toFacetState, valueToString } from "./utils"; +import { isLazyFacet, toFacetState, valueToString } from "./utils"; export type FacetsPanelState = { facetsConfig: FacetsConfig | null; @@ -10,6 +10,15 @@ export type FacetsPanelState = { facets: FacetDto[] | null; setFacets: (facets: FacetDto[]) => void; + /** + * IDs of facets whose options should be loaded. Non-lazy facets are active + * immediately; lazy facets become active only once the user expands them or + * they have selected options. This prevents loading options for every lazy + * facet on mount, which froze the page with high facet cardinality (#6577). + */ + activeFacetIds: Record; + setFacetActive: (facetId: string) => void; + facetOptions: Record | null; setFacetOptions: (facetOptions: Record) => void; @@ -61,7 +70,40 @@ export const createFacetsPanelStore = () => setFacetsConfig: (facetsConfig: FacetsConfig) => set({ facetsConfig }), facets: null, - setFacets: (facets: FacetDto[]) => set({ facets }), + setFacets: (facets: FacetDto[]) => { + const previousFacets = state().facets; + const previousActive = state().activeFacetIds || {}; + const activeFacetIds: Record = { ...previousActive }; + const previousFacetIds = new Set( + (previousFacets || []).map((facet) => facet.id) + ); + // Whether this is the very first time facets are loaded. On subsequent + // updates, any facet not seen before is treated as newly added by the + // user (via "Add Facet") and should be active/expanded immediately. + const isInitialLoad = previousFacets === null; + + // Eager facets load options immediately. A facet is eager unless it is + // explicitly lazy AND not static; static facets (severity/status/source) + // must always render their values on load. Only non-static lazy facets + // (high-cardinality user-defined facets) are deferred (#6577). + facets.forEach((facet) => { + const isNewlyAdded = !isInitialLoad && !previousFacetIds.has(facet.id); + if (!isLazyFacet(facet) || isNewlyAdded) { + activeFacetIds[facet.id] = true; + } + }); + set({ facets, activeFacetIds }); + }, + + activeFacetIds: {}, + setFacetActive: (facetId: string) => { + if (state().activeFacetIds?.[facetId]) { + return; + } + set({ + activeFacetIds: { ...(state().activeFacetIds || {}), [facetId]: true }, + }); + }, facetOptions: null, setFacetOptions: (facetOptions: Record) => @@ -87,9 +129,16 @@ export const createFacetsPanelStore = () => facetsState: {}, patchFacetsState: (facetsStatePatch) => { + // Facets that have a (pre)selected state must load their options so the + // selection can be reflected, even if they are lazy and collapsed. + const activeFacetIds = { ...(state().activeFacetIds || {}) }; + Object.keys(facetsStatePatch).forEach((facetId) => { + activeFacetIds[facetId] = true; + }); set({ // So that it only triggers refresh when facetsState is patched once facetsStateRefreshToken: state().facetsStateRefreshToken || uuidV4(), + activeFacetIds, facetsState: { ...(state().facetsState || {}), ...facetsStatePatch, @@ -125,6 +174,7 @@ export const createFacetsPanelStore = () => // So that it only triggers refresh when facetsState is changed once (option is selected\deselected by user) facetsStateRefreshToken: uuidV4(), changedFacetId: facetId, + activeFacetIds: { ...(state().activeFacetIds || {}), [facetId]: true }, dirtyFacetIds: Array.from(new Set(state().dirtyFacetIds).add(facetId)), facetsState: { ...facetsState, @@ -140,6 +190,7 @@ export const createFacetsPanelStore = () => // So that it only triggers refresh when facetsState is changed once (option is selected\deselected by user) facetsStateRefreshToken: uuidV4(), changedFacetId: facetId, + activeFacetIds: { ...(state().activeFacetIds || {}), [facetId]: true }, dirtyFacetIds: Array.from(new Set(state().dirtyFacetIds).add(facetId)), facetsState: { ...facetsState, @@ -155,6 +206,7 @@ export const createFacetsPanelStore = () => // So that it only triggers refresh when facetsState is changed once (option is selected\deselected by user) facetsStateRefreshToken: uuidV4(), changedFacetId: facetId, + activeFacetIds: { ...(state().activeFacetIds || {}), [facetId]: true }, dirtyFacetIds: Array.from(new Set(state().dirtyFacetIds).add(facetId)), facetsState: { ...facetsState, @@ -179,10 +231,19 @@ export const createFacetsPanelStore = () => set({ isInitialStateHandled }), clearFilters: () => { + // Keep only eager facets active so we don't re-load every lazy facet + // after a reset (#6577). + const activeFacetIds: Record = {}; + (state().facets || []).forEach((facet) => { + if (!isLazyFacet(facet)) { + activeFacetIds[facet.id] = true; + } + }); return set({ isInitialStateHandled: false, facetsState: {}, facetsStateRefreshToken: uuidV4(), + activeFacetIds, dirtyFacetIds: [], }); }, diff --git a/keep-ui/features/filter/store/use-queries-handler.ts b/keep-ui/features/filter/store/use-queries-handler.ts index f76fd852a5..cbb7839d97 100644 --- a/keep-ui/features/filter/store/use-queries-handler.ts +++ b/keep-ui/features/filter/store/use-queries-handler.ts @@ -56,6 +56,9 @@ export function useQueriesHandler(store: StoreApi) { const facets = useStore(store, (state) => state.facets); const facetsRef = useRef(facets); facetsRef.current = facets; + const activeFacetIds = useStore(store, (state) => state.activeFacetIds); + const activeFacetIdsRef = useRef(activeFacetIds); + activeFacetIdsRef.current = activeFacetIds; const allFacetOptions = useStore(store, (state) => state.facetOptions); const allFacetOptionsRef = useRef(allFacetOptions); allFacetOptionsRef.current = allFacetOptions; @@ -93,7 +96,14 @@ export function useQueriesHandler(store: StoreApi) { return; } - facets.forEach((facet) => { + const activeFacets = facets.filter( + (facet) => activeFacetIdsRef.current?.[facet.id] + ); + + // Only build option queries for active facets. Lazy facets that haven't + // been expanded (and have no selection) are skipped so we don't fetch + // options for hundreds of facets at once (#6577). + activeFacets.forEach((facet) => { const otherFacetCels = facets .filter((f) => f.id !== facet.id) .map((f) => facetsCelState?.[f.id]) @@ -110,5 +120,10 @@ export function useQueriesHandler(store: StoreApi) { .join(" && "); setQueriesState(filterCel, facetOptionQueries); - }, [facetsCelState, facets, isFacetsStateInitializedFromQueryParams]); + }, [ + facetsCelState, + facets, + activeFacetIds, + isFacetsStateInitializedFromQueryParams, + ]); } diff --git a/keep-ui/features/filter/store/utils.ts b/keep-ui/features/filter/store/utils.ts index 8820401a4e..0282ba7522 100644 --- a/keep-ui/features/filter/store/utils.ts +++ b/keep-ui/features/filter/store/utils.ts @@ -1,3 +1,16 @@ +import { FacetDto } from "../models"; + +/** + * Whether a facet should be deferred (collapsed, options loaded only on + * expand). Only non-static lazy facets qualify — static facets such as + * Severity/Status/Source must always render eagerly. The backend marks every + * facet as `is_lazy: true` by default, so `is_static` is the real + * discriminator here (#6577). + */ +export function isLazyFacet(facet: FacetDto): boolean { + return !!facet.is_lazy && !facet.is_static; +} + export function valueToString(value: any): string { if (typeof value === "string") { /* Escape single-quote because single-quote is used for string literal mark*/ diff --git a/pyproject.toml b/pyproject.toml index 6057fe5845..d00efafa48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "keep" -version = "0.53.0" +version = "0.54.0" description = "Alerting. for developers, by developers." authors = ["Keep Alerting LTD"] packages = [{include = "keep"}]