diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(job)/jobs/JobTable.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(job)/jobs/JobTable.tsx index de5f2ffe0..ae9771582 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(job)/jobs/JobTable.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(job)/jobs/JobTable.tsx @@ -1,5 +1,6 @@ "use client"; +import type { JobCondition } from "@ctrlplane/validators/jobs"; import React from "react"; import { IconFilter, IconLoader2 } from "@tabler/icons-react"; import _ from "lodash"; @@ -20,9 +21,9 @@ import { JobStatusReadable } from "@ctrlplane/validators/jobs"; import { NoFilterMatch } from "~/app/[workspaceSlug]/(app)/_components/filter/NoFilterMatch"; import { JobConditionBadge } from "~/app/[workspaceSlug]/(app)/_components/job-condition/JobConditionBadge"; import { JobConditionDialog } from "~/app/[workspaceSlug]/(app)/_components/job-condition/JobConditionDialog"; -import { useJobFilter } from "~/app/[workspaceSlug]/(app)/_components/job-condition/useJobFilter"; import { useJobDrawer } from "~/app/[workspaceSlug]/(app)/_components/job-drawer/useJobDrawer"; import { JobTableStatusIcon } from "~/app/[workspaceSlug]/(app)/_components/JobTableStatusIcon"; +import { useFilter } from "~/app/[workspaceSlug]/(app)/_components/useFilter"; import { api } from "~/trpc/react"; type JobTableProps = { @@ -30,7 +31,7 @@ type JobTableProps = { }; export const JobTable: React.FC = ({ workspaceId }) => { - const { filter, setFilter } = useJobFilter(); + const { filter, setFilter } = useFilter(); const { setJobId } = useJobDrawer(); const allReleaseJobTriggers = api.job.config.byWorkspaceId.list.useQuery( { workspaceId }, @@ -38,7 +39,7 @@ export const JobTable: React.FC = ({ workspaceId }) => { ); const releaseJobTriggers = api.job.config.byWorkspaceId.list.useQuery( - { workspaceId, filter, limit: 100 }, + { workspaceId, filter: filter ?? undefined, limit: 100 }, { refetchInterval: 10_000, placeholderData: (prev) => prev }, ); @@ -91,7 +92,7 @@ export const JobTable: React.FC = ({ workspaceId }) => { setFilter(undefined)} + onClear={() => setFilter(null)} /> )} diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/layout.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/layout.tsx index 325c94a8b..ffe5969d7 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/layout.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/layout.tsx @@ -171,7 +171,10 @@ export default function TargetLayout({ )} {pathname.includes(`/${params.workspaceSlug}/target-views`) && ( - + diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/TargetPageContent.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/TargetPageContent.tsx index 49da553ee..b39e490bf 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/TargetPageContent.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/(targets)/targets/TargetPageContent.tsx @@ -80,7 +80,7 @@ export const TargetPageContent: React.FC<{ view: schema.ResourceView | null; }> = ({ workspace, view }) => { const [search, setSearch] = React.useState(""); - const { filter, setFilter, setView } = useTargetFilter(); + const { filter, setFilter } = useTargetFilter(); useDebounce( () => { @@ -113,13 +113,13 @@ export const TargetPageContent: React.FC<{ limit: 0, }); const targets = api.resource.byWorkspaceId.list.useQuery( - { workspaceId, filter }, + { workspaceId, filter: filter ?? undefined }, { placeholderData: (prev) => prev }, ); - const onFilterChange = (condition: ResourceCondition | undefined) => { + const onFilterChange = (condition: ResourceCondition | null) => { const cond = condition ?? defaultCondition; - if (isEmptyCondition(cond)) setFilter(undefined); + if (isEmptyCondition(cond)) setFilter(null); if (!isEmptyCondition(cond)) setFilter(cond); }; @@ -173,7 +173,7 @@ export const TargetPageContent: React.FC<{ setFilter(v.filter, v.id)} > @@ -207,7 +207,7 @@ export const TargetPageContent: React.FC<{ setFilter(undefined)} + onClear={() => setFilter(null)} /> )} {targets.data != null && targets.data.total > 0 && ( diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/job-condition/JobConditionDialog.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/job-condition/JobConditionDialog.tsx index 5437b47a6..fa449f2fe 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/job-condition/JobConditionDialog.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/job-condition/JobConditionDialog.tsx @@ -14,14 +14,15 @@ import { import { MAX_DEPTH_ALLOWED } from "@ctrlplane/validators/conditions"; import { defaultCondition, + isEmptyCondition, isValidJobCondition, } from "@ctrlplane/validators/jobs"; import { JobConditionRender } from "./JobConditionRender"; type JobConditionDialogProps = { - condition?: JobCondition; - onChange: (condition: JobCondition | undefined) => void; + condition: JobCondition | null; + onChange: (condition: JobCondition | null) => void; children: React.ReactNode; }; @@ -73,9 +74,13 @@ export const JobConditionDialog: React.FC = ({ ); return; } - onChange(localCondition); setOpen(false); setError(null); + if (isEmptyCondition(localCondition)) { + onChange(null); + return; + } + onChange(localCondition); }} > Save diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/job-condition/useJobFilter.ts b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/job-condition/useJobFilter.ts deleted file mode 100644 index 3326eb988..000000000 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/job-condition/useJobFilter.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { JobCondition } from "@ctrlplane/validators/jobs"; -import { useCallback, useMemo } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; -import LZString from "lz-string"; - -export const useJobFilter = () => { - const urlParams = useSearchParams(); - const router = useRouter(); - - const filter = useMemo(() => { - const filterJson = urlParams.get("job-filter"); - if (filterJson == null) return undefined; - try { - return JSON.parse(LZString.decompressFromEncodedURIComponent(filterJson)); - } catch { - return undefined; - } - }, [urlParams]); - - const setFilter = useCallback( - (filter: JobCondition | undefined) => { - if (filter == null) { - const query = new URLSearchParams(window.location.search); - query.delete("job-filter"); - router.replace(`?${query.toString()}`); - return; - } - - const filterJson = LZString.compressToEncodedURIComponent( - JSON.stringify(filter), - ); - const query = new URLSearchParams(window.location.search); - query.set("job-filter", filterJson); - router.replace(`?${query.toString()}`); - }, - [router], - ); - - return { filter, setFilter }; -}; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/release-channel-drawer/Overview.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/release-channel-drawer/Overview.tsx index 898b5ed9e..3c71b7280 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/release-channel-drawer/Overview.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/release-channel-drawer/Overview.tsx @@ -46,6 +46,7 @@ const getFinalFilter = (filter: ReleaseCondition | null) => const getReleaseFilterUrl = ( workspaceSlug: string, + releaseChannelId: string, systemSlug?: string, deploymentSlug?: string, filter?: ReleaseCondition, @@ -56,7 +57,7 @@ const getReleaseFilterUrl = ( const filterHash = LZString.compressToEncodedURIComponent( JSON.stringify(filter), ); - return `${baseUrl}/releases?filter=${filterHash}`; + return `${baseUrl}/releases?filter=${filterHash}&release-channel-id=${releaseChannelId}`; }; const schema = z.object({ @@ -108,9 +109,7 @@ export const Overview: React.FC = ({ releaseChannel }) => { .then(() => utils.deployment.releaseChannel.byId.invalidate(releaseChannel.id), ) - .then(() => { - if (paramFilter != null) setFilter(releaseFilter); - }) + .then(() => paramFilter != null && setFilter(releaseFilter ?? null)) .then(() => router.refresh()); }); @@ -125,6 +124,7 @@ export const Overview: React.FC = ({ releaseChannel }) => { const releases = releasesQ.data; const releaseFilterUrl = getReleaseFilterUrl( workspaceSlug, + releaseChannel.id, systemSlug, deploymentSlug, filter, diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/release-channel-drawer/ReleaseChannelDropdown.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/release-channel-drawer/ReleaseChannelDropdown.tsx index 2bcd06a3d..8ba60e01f 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/release-channel-drawer/ReleaseChannelDropdown.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/release-channel-drawer/ReleaseChannelDropdown.tsx @@ -22,8 +22,7 @@ import { } from "@ctrlplane/ui/dropdown-menu"; import { api } from "~/trpc/react"; -import { useReleaseFilter } from "../release-condition/useReleaseFilter"; -import { useReleaseChannelDrawer } from "./useReleaseChannelDrawer"; +import { useQueryParams } from "../useQueryParams"; type DeleteReleaseChannelDialogProps = { releaseChannelId: string; @@ -37,16 +36,22 @@ const DeleteReleaseChannelDialog: React.FC = ({ children, }) => { const [open, setOpen] = useState(false); - const { removeReleaseChannelId } = useReleaseChannelDrawer(); - const { setFilter } = useReleaseFilter(); + const { setParams } = useQueryParams(); const router = useRouter(); const deleteReleaseChannel = api.deployment.releaseChannel.delete.useMutation(); const onDelete = () => deleteReleaseChannel .mutateAsync(releaseChannelId) - .then(() => removeReleaseChannelId()) - .then(() => setFilter(undefined, null)) + .then(() => + // setting params one at a time does not work and only makes one change + // so we directly set all params to null at once + setParams({ + "release-channel-id": null, + release_channel_id: null, + filter: null, + }), + ) .then(() => router.refresh()) .then(() => setOpen(false)); diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/release-condition/ReleaseConditionDialog.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/release-condition/ReleaseConditionDialog.tsx index ba0e358c4..99b03f0a2 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/release-condition/ReleaseConditionDialog.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/release-condition/ReleaseConditionDialog.tsx @@ -26,6 +26,7 @@ import { import { Tabs, TabsContent, TabsList, TabsTrigger } from "@ctrlplane/ui/tabs"; import { defaultCondition, + isEmptyCondition, isValidReleaseCondition, MAX_DEPTH_ALLOWED, } from "@ctrlplane/validators/releases"; @@ -36,10 +37,10 @@ import { ReleaseConditionRender } from "./ReleaseConditionRender"; import { useReleaseFilter } from "./useReleaseFilter"; type ReleaseConditionDialogProps = { - condition?: ReleaseCondition; + condition: ReleaseCondition | null; deploymentId?: string; onChange: ( - condition: ReleaseCondition | undefined, + condition: ReleaseCondition | null, releaseChannelId?: string | null, ) => void; releaseChannels?: SCHEMA.ReleaseChannel[]; @@ -58,16 +59,17 @@ export const ReleaseConditionDialog: React.FC = ({ const { setFilter, releaseChannelId } = useReleaseFilter(); const [localReleaseChannelId, setLocalReleaseChannelId] = useState< - string | undefined + string | null >(releaseChannelId); - const [localCondition, setLocalCondition] = useState< - ReleaseCondition | undefined - >(condition ?? defaultCondition); + const [localCondition, setLocalCondition] = useState( + condition ?? defaultCondition, + ); const isLocalConditionValid = localCondition == null || isValidReleaseCondition(localCondition); + const filter = localCondition ?? undefined; const releasesQ = api.release.list.useQuery( - { deploymentId: deploymentId ?? "", filter: localCondition, limit: 5 }, + { deploymentId: deploymentId ?? "", filter, limit: 5 }, { enabled: deploymentId != null && isLocalConditionValid }, ); const releases = releasesQ.data; @@ -107,14 +109,14 @@ export const ReleaseConditionDialog: React.FC = ({