diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-condition/ComparisonConditionRender.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-condition/ComparisonConditionRender.tsx index dace4b76..bde8edac 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-condition/ComparisonConditionRender.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-condition/ComparisonConditionRender.tsx @@ -30,7 +30,11 @@ import { SelectTrigger, SelectValue, } from "@ctrlplane/ui/select"; -import { ColumnOperator } from "@ctrlplane/validators/conditions"; +import { + ColumnOperator, + DateOperator, + FilterType, +} from "@ctrlplane/validators/conditions"; import { doesConvertingToComparisonRespectMaxDepth, isComparisonCondition, @@ -317,6 +321,18 @@ export const ComparisonConditionRender: React.FC< > Provider + + addCondition({ + type: FilterType.CreatedAt, + operator: DateOperator.Before, + value: new Date().toISOString(), + }) + } + > + Created at + + {depth < 2 && ( diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-condition/ResourceCreatedAtConditionRender.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-condition/ResourceCreatedAtConditionRender.tsx new file mode 100644 index 00000000..043b0b96 --- /dev/null +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-condition/ResourceCreatedAtConditionRender.tsx @@ -0,0 +1,34 @@ +import type { + CreatedAtCondition, + DateOperatorType, +} from "@ctrlplane/validators/conditions"; +import type { DateValue } from "@internationalized/date"; + +import type { TargetConditionRenderProps } from "./target-condition-props"; +import { DateConditionRender } from "../filter/DateConditionRender"; + +export const ResourceCreatedAtConditionRender: React.FC< + TargetConditionRenderProps +> = ({ condition, onChange, className }) => { + const setDate = (t: DateValue) => + onChange({ + ...condition, + value: t + .toDate(Intl.DateTimeFormat().resolvedOptions().timeZone) + .toISOString(), + }); + + const setOperator = (operator: DateOperatorType) => + onChange({ ...condition, operator }); + + return ( + + ); +}; diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-condition/TargetConditionBadge.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-condition/TargetConditionBadge.tsx index f0ae6d8f..a6d1a5d9 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-condition/TargetConditionBadge.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-condition/TargetConditionBadge.tsx @@ -1,4 +1,7 @@ -import type { MetadataCondition } from "@ctrlplane/validators/conditions"; +import type { + CreatedAtCondition, + MetadataCondition, +} from "@ctrlplane/validators/conditions"; import type { ComparisonCondition, IdentifierCondition, @@ -8,6 +11,7 @@ import type { ResourceCondition, } from "@ctrlplane/validators/resources"; import React from "react"; +import { format } from "date-fns"; import _ from "lodash"; import { cn } from "@ctrlplane/ui"; @@ -17,9 +21,10 @@ import { HoverCardContent, HoverCardTrigger, } from "@ctrlplane/ui/hover-card"; -import { ColumnOperator } from "@ctrlplane/validators/conditions"; +import { ColumnOperator, DateOperator } from "@ctrlplane/validators/conditions"; import { isComparisonCondition, + isCreatedAtCondition, isIdentifierCondition, isKindCondition, isMetadataCondition, @@ -44,6 +49,10 @@ const operatorVerbs = { [ColumnOperator.StartsWith]: "starts with", [ColumnOperator.EndsWith]: "ends with", [ColumnOperator.Contains]: "contains", + [DateOperator.Before]: "before", + [DateOperator.After]: "after", + [DateOperator.BeforeOrOn]: "before or on", + [DateOperator.AfterOrOn]: "after or on", }; const ConditionBadge: React.FC<{ @@ -208,6 +217,20 @@ const StringifiedProviderCondition: React.FC<{ ); }; +const StringifiedCreatedAtCondition: React.FC<{ + condition: CreatedAtCondition; +}> = ({ condition }) => ( + + created + + {operatorVerbs[condition.operator]} + + + {format(condition.value, "MMM d, yyyy, h:mma")} + + +); + const StringifiedTargetCondition: React.FC<{ condition: ResourceCondition; depth?: number; @@ -242,6 +265,9 @@ const StringifiedTargetCondition: React.FC<{ if (isProviderCondition(condition)) return ; + + if (isCreatedAtCondition(condition)) + return ; }; export const TargetConditionBadge: React.FC<{ diff --git a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-condition/TargetConditionRender.tsx b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-condition/TargetConditionRender.tsx index ccad6dc2..befaccf0 100644 --- a/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-condition/TargetConditionRender.tsx +++ b/apps/webservice/src/app/[workspaceSlug]/(app)/_components/target-condition/TargetConditionRender.tsx @@ -3,6 +3,7 @@ import React from "react"; import { isComparisonCondition, + isCreatedAtCondition, isIdentifierCondition, isKindCondition, isMetadataCondition, @@ -16,6 +17,7 @@ import { IdentifierConditionRender } from "./IdentifierConditionRender"; import { KindConditionRender } from "./KindConditionRender"; import { NameConditionRender } from "./NameConditionRender"; import { ProviderConditionRender } from "./ProviderConditionRender"; +import { ResourceCreatedAtConditionRender } from "./ResourceCreatedAtConditionRender"; import { TargetMetadataConditionRender } from "./TargetMetadataConditionRender"; /** @@ -80,5 +82,14 @@ export const TargetConditionRender: React.FC< /> ); + if (isCreatedAtCondition(condition)) + return ( + + ); + return null; }; diff --git a/packages/db/src/schema/resource.ts b/packages/db/src/schema/resource.ts index ecb59e0f..8ba698af 100644 --- a/packages/db/src/schema/resource.ts +++ b/packages/db/src/schema/resource.ts @@ -1,4 +1,7 @@ -import type { MetadataCondition } from "@ctrlplane/validators/conditions"; +import type { + CreatedAtCondition, + MetadataCondition, +} from "@ctrlplane/validators/conditions"; import type { IdentifierCondition, NameCondition, @@ -7,8 +10,12 @@ import type { import type { InferInsertModel, InferSelectModel, SQL } from "drizzle-orm"; import { exists, + gt, + gte, ilike, like, + lt, + lte, not, notExists, or, @@ -33,6 +40,8 @@ import { z } from "zod"; import { ColumnOperator, ComparisonOperator, + DateOperator, + FilterType, MetadataOperator, } from "@ctrlplane/validators/conditions"; import { @@ -243,6 +252,16 @@ const buildNameCondition = (tx: Tx, cond: NameCondition): SQL => { return sql`${resource.name} ~ ${cond.value}`; }; +const buildCreatedAtCondition = (tx: Tx, cond: CreatedAtCondition): SQL => { + const date = new Date(cond.value); + if (cond.operator === DateOperator.Before) + return lt(resource.createdAt, date); + if (cond.operator === DateOperator.After) return gt(resource.createdAt, date); + if (cond.operator === DateOperator.BeforeOrOn) + return lte(resource.createdAt, date); + return gte(resource.createdAt, date); +}; + const buildCondition = (tx: Tx, cond: ResourceCondition): SQL => { if (cond.type === ResourceFilterType.Metadata) return buildMetadataCondition(tx, cond); @@ -254,6 +273,8 @@ const buildCondition = (tx: Tx, cond: ResourceCondition): SQL => { return eq(resource.providerId, cond.value); if (cond.type === ResourceFilterType.Identifier) return buildIdentifierCondition(tx, cond); + if (cond.type === FilterType.CreatedAt) + return buildCreatedAtCondition(tx, cond); if (cond.conditions.length === 0) return sql`FALSE`; diff --git a/packages/validators/src/resources/conditions/comparison-condition.ts b/packages/validators/src/resources/conditions/comparison-condition.ts index a5e85a19..5060b3a2 100644 --- a/packages/validators/src/resources/conditions/comparison-condition.ts +++ b/packages/validators/src/resources/conditions/comparison-condition.ts @@ -1,11 +1,17 @@ import { z } from "zod"; -import type { MetadataCondition } from "../../conditions/index.js"; +import type { + CreatedAtCondition, + MetadataCondition, +} from "../../conditions/index.js"; import type { IdentifierCondition } from "./identifier-condition.js"; import type { KindCondition } from "./kind-condition.js"; import type { NameCondition } from "./name-condition.js"; import type { ProviderCondition } from "./provider-condition.js"; -import { metadataCondition } from "../../conditions/index.js"; +import { + createdAtCondition, + metadataCondition, +} from "../../conditions/index.js"; import { identifierCondition } from "./identifier-condition.js"; import { kindCondition } from "./kind-condition.js"; import { nameCondition } from "./name-condition.js"; @@ -24,6 +30,7 @@ export const comparisonCondition: z.ZodType = z.lazy(() => nameCondition, providerCondition, identifierCondition, + createdAtCondition, ]), ), }), @@ -40,5 +47,6 @@ export type ComparisonCondition = { | NameCondition | ProviderCondition | IdentifierCondition + | CreatedAtCondition >; }; diff --git a/packages/validators/src/resources/conditions/resource-condition.ts b/packages/validators/src/resources/conditions/resource-condition.ts index 71ee1799..f3c7868f 100644 --- a/packages/validators/src/resources/conditions/resource-condition.ts +++ b/packages/validators/src/resources/conditions/resource-condition.ts @@ -1,12 +1,19 @@ import { z } from "zod"; -import type { MetadataCondition } from "../../conditions/index.js"; +import type { + CreatedAtCondition, + MetadataCondition, +} from "../../conditions/index.js"; import type { ComparisonCondition } from "./comparison-condition.js"; import type { IdentifierCondition } from "./identifier-condition.js"; import type { KindCondition } from "./kind-condition.js"; import type { NameCondition } from "./name-condition.js"; import type { ProviderCondition } from "./provider-condition.js"; -import { metadataCondition } from "../../conditions/index.js"; +import { + createdAtCondition, + FilterType, + metadataCondition, +} from "../../conditions/index.js"; import { comparisonCondition } from "./comparison-condition.js"; import { identifierCondition } from "./identifier-condition.js"; import { kindCondition } from "./kind-condition.js"; @@ -19,7 +26,8 @@ export type ResourceCondition = | KindCondition | NameCondition | ProviderCondition - | IdentifierCondition; + | IdentifierCondition + | CreatedAtCondition; export const resourceCondition = z.union([ comparisonCondition, @@ -28,6 +36,7 @@ export const resourceCondition = z.union([ nameCondition, providerCondition, identifierCondition, + createdAtCondition, ]); export enum ResourceOperator { @@ -104,6 +113,10 @@ export const isIdentifierCondition = ( ): condition is IdentifierCondition => condition.type === ResourceFilterType.Identifier; +export const isCreatedAtCondition = ( + condition: ResourceCondition, +): condition is CreatedAtCondition => condition.type === FilterType.CreatedAt; + export const isValidTargetCondition = ( condition: ResourceCondition, ): boolean => {