From 58721c7a7dd92eb70a19cd7d58bc6157673893f7 Mon Sep 17 00:00:00 2001 From: TuvalSimha Date: Wed, 11 Dec 2024 21:06:51 +0200 Subject: [PATCH 1/4] Draft: Drop yup and formik --- packages/web/app/package.json | 2 +- packages/web/app/src/constants.ts | 4 + .../web/app/src/pages/target-settings.tsx | 688 ++++++++++-------- pnpm-lock.yaml | 20 +- 4 files changed, 410 insertions(+), 304 deletions(-) diff --git a/packages/web/app/package.json b/packages/web/app/package.json index ef0a316b31..e4ebc90c36 100644 --- a/packages/web/app/package.json +++ b/packages/web/app/package.json @@ -138,4 +138,4 @@ "vite" ] } -} +} \ No newline at end of file diff --git a/packages/web/app/src/constants.ts b/packages/web/app/src/constants.ts index 2d9c79b782..3a3358818a 100644 --- a/packages/web/app/src/constants.ts +++ b/packages/web/app/src/constants.ts @@ -1,3 +1,7 @@ export const LAST_VISITED_ORG_KEY = 'lastVisitedOrganization_v2'; export const CHART_PRIMARY_COLOR = 'rgb(234, 179, 8)'; + +export const DEFAULT_RETENTION_DAYS = 30; + +export const MINIMUM_DAYS = 7; diff --git a/packages/web/app/src/pages/target-settings.tsx b/packages/web/app/src/pages/target-settings.tsx index f2c7a40b1b..9b43e12d62 100644 --- a/packages/web/app/src/pages/target-settings.tsx +++ b/packages/web/app/src/pages/target-settings.tsx @@ -1,10 +1,9 @@ -import { ComponentProps, PropsWithoutRef, useCallback, useMemo, useState } from 'react'; +import { ComponentProps, PropsWithoutRef, useCallback, useEffect, useMemo, useState } from 'react'; import clsx from 'clsx'; import { formatISO } from 'date-fns'; -import { useFormik } from 'formik'; +import { use } from 'echarts'; import { useForm } from 'react-hook-form'; import { useMutation, useQuery } from 'urql'; -import * as Yup from 'yup'; import { z } from 'zod'; import { Page, TargetLayout } from '@/components/layouts/target'; import { SchemaEditor } from '@/components/schema-editor'; @@ -35,12 +34,13 @@ import { } from '@/components/ui/page-content-layout'; import { QueryError } from '@/components/ui/query-error'; import { Spinner } from '@/components/ui/spinner'; +import { Switch } from '@/components/ui/switch'; import { TimeAgo } from '@/components/ui/time-ago'; import { useToast } from '@/components/ui/use-toast'; import { Combobox } from '@/components/v2/combobox'; -import { Switch } from '@/components/v2/switch'; import { Table, TBody, Td, Tr } from '@/components/v2/table'; import { Tag } from '@/components/v2/tag'; +import { DEFAULT_RETENTION_DAYS, MINIMUM_DAYS } from '@/constants'; import { env } from '@/env/frontend'; import { FragmentType, graphql, useFragment } from '@/gql'; import { ProjectType } from '@/gql/graphql'; @@ -462,11 +462,34 @@ function floorDate(date: Date): Date { return new Date(Math.floor(date.getTime() / time) * time); } +const conditionalBreakingChangesFormSchema = z.object({ + period: z.preprocess( + value => Number(value), + z + .number({ required_error: 'Period is required' }) + .min(0, 'Period must be at least 0 days') + .transform(value => Math.round(value)), + ), + percentage: z.preprocess( + value => Number(value), + z + .number({ required_error: 'Percentage is required' }) + .min(0, 'Percentage must be at least 0%') + .max(100, 'Percentage must be at most 100%') + .transform(value => Math.round(value)), + ), + targetIds: z.array(z.string()), + excludedClients: z.array(z.string()), +}); + +type ConditionalBreakingChangesFormValues = z.infer; + const ConditionalBreakingChanges = (props: { organizationSlug: string; projectSlug: string; targetSlug: string; }) => { + const { toast } = useToast(); const [targetValidation, setValidation] = useMutation(SetTargetValidationMutation); const [mutation, updateValidation] = useMutation( TargetSettingsPage_UpdateTargetValidationSettingsMutation, @@ -495,254 +518,324 @@ const ConditionalBreakingChanges = (props: { ); const isEnabled = settings?.enabled || false; const possibleTargets = targetSettings.data?.targets.nodes; - const { toast } = useToast(); - const { - handleSubmit, - isSubmitting, - errors, - touched, - values, - handleBlur, - handleChange, - setFieldValue, - setFieldTouched, - } = useFormik({ - enableReinitialize: true, - initialValues: { - percentage: settings?.percentage || 0, - period: settings?.period || 0, + const retentionInDays = + targetSettings.data?.organization?.organization?.rateLimit.retentionInDays ?? + DEFAULT_RETENTION_DAYS; + const defaultDays = + retentionInDays >= DEFAULT_RETENTION_DAYS ? DEFAULT_RETENTION_DAYS : MINIMUM_DAYS; + + const conditionalBreakingChangesForm = useForm({ + mode: 'all', + resolver: zodResolver(conditionalBreakingChangesFormSchema), + defaultValues: { + period: settings?.period ?? defaultDays, + percentage: settings?.percentage ?? 0, targetIds: settings?.targets.map(t => t.id) || [], excludedClients: settings?.excludedClients ?? [], }, - validationSchema: Yup.object().shape({ - percentage: Yup.number().min(0).max(100).required(), - period: Yup.number() - .min(1) - .max(targetSettings.data?.organization?.organization?.rateLimit.retentionInDays ?? 30) - .test('double-precision', 'Invalid precision', num => { - if (typeof num !== 'number') { - return false; - } - - // Round the number to two decimal places - // and check if it is equal to the original number - return Number(num.toFixed(2)) === num; - }) - .required(), - targetIds: Yup.array().of(Yup.string()).min(1), - excludedClients: Yup.array().of(Yup.string()), - }), - onSubmit: values => - updateValidation({ - input: { - organizationSlug: props.organizationSlug, - projectSlug: props.projectSlug, - targetSlug: props.targetSlug, - ...values, - }, - }).then(result => { - if (result.error || result.data?.updateTargetValidationSettings.error) { - toast({ - variant: 'destructive', - title: 'Error', - description: - result.error?.message || result.data?.updateTargetValidationSettings.error?.message, - }); - } else { - toast({ - variant: 'default', - title: 'Success', - description: 'Conditional breaking changes settings updated successfully', - }); - } - }), }); + useEffect(() => { + conditionalBreakingChangesForm.reset({ + period: settings?.period ?? defaultDays, + percentage: settings?.percentage ?? 0, + targetIds: settings?.targets.map(t => t.id) || [], + excludedClients: settings?.excludedClients ?? [], + }); + }, [settings]); + + async function onConditionalBreakingChangesFormSubmit( + data: ConditionalBreakingChangesFormValues, + ) { + await updateValidation({ + input: { + organizationSlug: props.organizationSlug, + projectSlug: props.projectSlug, + targetSlug: props.targetSlug, + percentage: data.percentage, + period: data.period, + targetIds: data.targetIds, + excludedClients: data.excludedClients, + }, + }); + if (mutation.data?.updateTargetValidationSettings.error?.inputErrors.period) { + toast({ + variant: 'destructive', + title: 'Error', + description: mutation.data?.updateTargetValidationSettings.error?.inputErrors.period, + }); + } else { + toast({ + variant: 'default', + title: 'Success', + description: 'Conditional breaking changes settings updated successfully', + }); + } + // }).then(result => { + // if (mutation.data?.updateTargetValidationSettings.error?.inputErrors.period) { + // toast({ + // variant: 'destructive', + // title: 'Error', + // description: + // mutation.data.updateTargetValidationSettings.error.inputErrors.period[0] || + // 'Invalid period', + // }); + // } + // if (result.error || result.data?.updateTargetValidationSettings.error) { + // toast({ + // variant: 'destructive', + // title: 'Error', + // description: + // result.error?.message || result.data?.updateTargetValidationSettings.error?.message, + // }); + // } else { + // toast({ + // variant: 'default', + // title: 'Success', + // description: 'Conditional breaking changes settings updated successfully', + // }); + // } + // }); + } + return ( -
- - - - Conditional Breaking Changes can change the behavior of schema checks, based on real - traffic data sent to Hive. - - - - Learn more - - - - } - > - {targetSettings.fetching ? ( - - ) : ( - { - await setValidation({ - input: { - targetSlug: props.targetSlug, - projectSlug: props.projectSlug, - organizationSlug: props.organizationSlug, - enabled, - }, - }); - }} - disabled={targetValidation.fetching} - /> + + + + Conditional Breaking Changes can change the behavior of schema checks, based on real + traffic data sent to Hive. + + + + Learn more + + + + } + > + {targetSettings.fetching ? ( + + ) : ( + { + await setValidation({ + input: { + targetSlug: props.targetSlug, + projectSlug: props.projectSlug, + organizationSlug: props.organizationSlug, + enabled, + }, + }); + }} + disabled={targetValidation.fetching} + /> + )} + + + -
-
- A schema change is considered as breaking only if it affects more than - - % of traffic in the past - - days. -
-
- {touched.percentage && errors.percentage && ( -
{errors.percentage}
- )} - {mutation.data?.updateTargetValidationSettings.error?.inputErrors.percentage && ( -
- {mutation.data.updateTargetValidationSettings.error.inputErrors.percentage} -
+ > +
+
+ A schema change is considered as breaking only if it affects more than + ( + + + + + + )} + /> + % of traffic in the past + ( + + + + + + )} + /> + days. +
+ {conditionalBreakingChangesForm.formState.errors.period && ( + ( + + + + )} + /> )} - {touched.period && errors.period &&
{errors.period}
} - {mutation.data?.updateTargetValidationSettings.error?.inputErrors.period && ( -
- {mutation.data.updateTargetValidationSettings.error.inputErrors.period} -
+ {conditionalBreakingChangesForm.formState.errors.percentage && ( + ( + + + + )} + /> )} -
-
-
+
-
Allow breaking change for these clients:
+
Schema usage data from these targets:
- Marks a breaking change as safe when it only affects the following clients. + Marks a breaking change as safe when it was not requested in the targets + clients.
-
- {values.targetIds.length > 0 ? ( - setFieldTouched('excludedClients')} - onChange={async options => { - await setFieldValue( - 'excludedClients', - options.map(o => o.value), - ); - }} - disabled={isSubmitting} - /> - ) : ( -
Select targets first
- )} +
+ {possibleTargets?.map(pt => ( +
+ ( + + + { + await conditionalBreakingChangesForm.setValue( + 'targetIds', + isChecked + ? [...field.value, pt.id] + : field.value.filter(value => value !== pt.id), + ); + }} + onBlur={() => + conditionalBreakingChangesForm.setValue('targetIds', field.value) + } + /> + + + )} + /> + {pt.slug} +
+ ))}
- {touched.excludedClients && errors.excludedClients && ( -
{errors.excludedClients}
- )}
-
-
-
Schema usage data from these targets:
-
- Marks a breaking change as safe when it was not requested in the targets clients. -
-
-
- {possibleTargets?.map(pt => ( -
- { - await setFieldValue( - 'targetIds', - isChecked - ? [...values.targetIds, pt.id] - : values.targetIds.filter(value => value !== pt.id), - ); - }} - onBlur={() => setFieldTouched('targetIds', true)} - />{' '} - {pt.slug} +
+
+
Allow breaking change for these clients:
+
+ Marks a breaking change as safe when it only affects the following clients. +
+
+
+ {conditionalBreakingChangesForm.watch('targetIds').length > 0 ? ( + ( + + + + conditionalBreakingChangesForm.setValue( + 'excludedClients', + field.value, + ) + } + onChange={async options => { + await conditionalBreakingChangesForm.setValue( + 'excludedClients', + options.map(o => o.value), + ); + }} + disabled={conditionalBreakingChangesForm.formState.isSubmitting} + /> + + + )} + /> + ) : ( +
+
+
Select a target to enable this option
+
+
+ )}
- ))} + } + /> +
-
- {touched.targetIds && errors.targetIds && ( -
{errors.targetIds}
- )} -
-
-
Example settings
-
Removal of a field is considered breaking if
-
- -
- - 0% - {' '} - - the field was used at least once in past 30 days -
-
- - 10% - {' '} - - the field was requested by more than 10% of all GraphQL operations in recent 30 days +
+
+
Example settings
+
Removal of a field is considered breaking if
+
+
+ + 0% + {' '} + - the field was used at least once in past 30 days +
+
+ + 10% + {' '} + - the field was requested by more than 10% of all GraphQL operations in recent 30 + days +
+
- - {mutation.error && ( - - {mutation.error.graphQLErrors[0]?.message ?? mutation.error.message} - - )} -
- - + + + ); }; @@ -884,6 +977,17 @@ const TargetSettingsPage_UpdateTargetGraphQLEndpointUrl = graphql(` } `); +const GraphQLEndpointUrlFormSchema = z.object({ + enableReinitialize: z.boolean(), + graphqlEndpointUrl: z + .string() + .url('Please enter a valid url') + .max(300, 'Max 300 chars.') + .min(1, 'Please enter a valid url.'), +}); + +type GraphQLEndpointUrlFormValues = z.infer; + function GraphQLEndpointUrl(props: { graphqlEndpointUrl: string | null; organizationSlug: string; @@ -891,44 +995,42 @@ function GraphQLEndpointUrl(props: { targetSlug: string; }) { const { toast } = useToast(); - const [mutation, mutate] = useMutation(TargetSettingsPage_UpdateTargetGraphQLEndpointUrl); - const { handleSubmit, values, handleChange, handleBlur, isSubmitting, errors, touched } = - useFormik({ + const [_, mutate] = useMutation(TargetSettingsPage_UpdateTargetGraphQLEndpointUrl); + + const graphQLEndpointUrlForm = useForm({ + mode: 'onChange', + resolver: zodResolver(GraphQLEndpointUrlFormSchema), + defaultValues: { enableReinitialize: true, - initialValues: { - graphqlEndpointUrl: props.graphqlEndpointUrl || '', + graphqlEndpointUrl: props.graphqlEndpointUrl || '', + }, + }); + + function onGraphQLEndpointUrlFormSubmit(data: GraphQLEndpointUrlFormValues) { + mutate({ + input: { + organizationSlug: props.organizationSlug, + projectSlug: props.projectSlug, + targetSlug: props.targetSlug, + graphqlEndpointUrl: data.graphqlEndpointUrl === '' ? null : data.graphqlEndpointUrl, }, - validationSchema: Yup.object().shape({ - graphqlEndpointUrl: Yup.string() - .url('Please enter a valid url.') - .min(1, 'Please enter a valid url.') - .max(300, 'Max 300 chars.'), - }), - onSubmit: values => - mutate({ - input: { - organizationSlug: props.organizationSlug, - projectSlug: props.projectSlug, - targetSlug: props.targetSlug, - graphqlEndpointUrl: values.graphqlEndpointUrl === '' ? null : values.graphqlEndpointUrl, - }, - }).then(result => { - if (result.data?.updateTargetGraphQLEndpointUrl.error?.message || result.error) { - toast({ - variant: 'destructive', - title: 'Error', - description: - result.data?.updateTargetGraphQLEndpointUrl.error?.message || result.error?.message, - }); - } else { - toast({ - variant: 'default', - title: 'Success', - description: 'GraphQL endpoint url updated successfully', - }); - } - }), + }).then(result => { + if (result.error || result.data?.updateTargetGraphQLEndpointUrl.error) { + toast({ + variant: 'destructive', + title: 'Error', + description: + result.error?.message || result.data?.updateTargetGraphQLEndpointUrl.error?.message, + }); + } else { + toast({ + variant: 'default', + title: 'Success', + description: 'GraphQL endpoint url updated successfully', + }); + } }); + } return ( @@ -953,36 +1055,36 @@ function GraphQLEndpointUrl(props: { } /> -
-
-
- - -
- {touched.graphqlEndpointUrl && (errors.graphqlEndpointUrl || mutation.error) && ( -
- {errors.graphqlEndpointUrl ?? - mutation.error?.graphQLErrors[0]?.message ?? - mutation.error?.message} -
- )} - {mutation.data?.updateTargetGraphQLEndpointUrl.error && ( -
- {mutation.data.updateTargetGraphQLEndpointUrl.error.message} -
- )} + + + ( + + + + + + + )} + /> + -
+
); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa9839c758..c9e9c2e054 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16228,8 +16228,8 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.596.0 - '@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0) + '@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0) + '@aws-sdk/client-sts': 3.596.0 '@aws-sdk/core': 3.592.0 '@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -16336,11 +16336,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso-oidc@3.596.0': + '@aws-sdk/client-sso-oidc@3.596.0(@aws-sdk/client-sts@3.596.0)': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0) + '@aws-sdk/client-sts': 3.596.0 '@aws-sdk/core': 3.592.0 '@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -16379,6 +16379,7 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 transitivePeerDependencies: + - '@aws-sdk/client-sts' - aws-crt '@aws-sdk/client-sso-oidc@3.693.0(@aws-sdk/client-sts@3.693.0)': @@ -16512,11 +16513,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sts@3.596.0(@aws-sdk/client-sso-oidc@3.596.0)': + '@aws-sdk/client-sts@3.596.0': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.596.0 + '@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0) '@aws-sdk/core': 3.592.0 '@aws-sdk/credential-provider-node': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -16555,7 +16556,6 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.8.1 transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - aws-crt '@aws-sdk/client-sts@3.693.0': @@ -16669,7 +16669,7 @@ snapshots: '@aws-sdk/credential-provider-ini@3.596.0(@aws-sdk/client-sso-oidc@3.596.0)(@aws-sdk/client-sts@3.596.0)': dependencies: - '@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0) + '@aws-sdk/client-sts': 3.596.0 '@aws-sdk/credential-provider-env': 3.587.0 '@aws-sdk/credential-provider-http': 3.596.0 '@aws-sdk/credential-provider-process': 3.587.0 @@ -16788,7 +16788,7 @@ snapshots: '@aws-sdk/credential-provider-web-identity@3.587.0(@aws-sdk/client-sts@3.596.0)': dependencies: - '@aws-sdk/client-sts': 3.596.0(@aws-sdk/client-sso-oidc@3.596.0) + '@aws-sdk/client-sts': 3.596.0 '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.1.8 '@smithy/types': 3.6.0 @@ -16963,7 +16963,7 @@ snapshots: '@aws-sdk/token-providers@3.587.0(@aws-sdk/client-sso-oidc@3.596.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.596.0 + '@aws-sdk/client-sso-oidc': 3.596.0(@aws-sdk/client-sts@3.596.0) '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.1.8 '@smithy/shared-ini-file-loader': 3.1.9 From 73c9c3dfc75ffecae3d7b3e65da5cbf8f8d1a4ef Mon Sep 17 00:00:00 2001 From: TuvalSimha Date: Thu, 12 Dec 2024 14:07:39 +0200 Subject: [PATCH 2/4] clean --- packages/web/app/package.json | 2 +- packages/web/app/src/constants.ts | 6 +- .../web/app/src/pages/target-settings.tsx | 117 +++++++++--------- packages/web/app/src/utils.ts | 24 ++++ 4 files changed, 85 insertions(+), 64 deletions(-) diff --git a/packages/web/app/package.json b/packages/web/app/package.json index e4ebc90c36..ef0a316b31 100644 --- a/packages/web/app/package.json +++ b/packages/web/app/package.json @@ -138,4 +138,4 @@ "vite" ] } -} \ No newline at end of file +} diff --git a/packages/web/app/src/constants.ts b/packages/web/app/src/constants.ts index 3a3358818a..238282780c 100644 --- a/packages/web/app/src/constants.ts +++ b/packages/web/app/src/constants.ts @@ -2,6 +2,8 @@ export const LAST_VISITED_ORG_KEY = 'lastVisitedOrganization_v2'; export const CHART_PRIMARY_COLOR = 'rgb(234, 179, 8)'; -export const DEFAULT_RETENTION_DAYS = 30; +export const PRO_RETENTION_DAYS = 90; -export const MINIMUM_DAYS = 7; +export const ENTERPRISE_RETENTION_DAYS = 365; + +export const HOBBY_RETENTION_DAYS = 7; diff --git a/packages/web/app/src/pages/target-settings.tsx b/packages/web/app/src/pages/target-settings.tsx index 9b43e12d62..d1ec5a0d5e 100644 --- a/packages/web/app/src/pages/target-settings.tsx +++ b/packages/web/app/src/pages/target-settings.tsx @@ -40,7 +40,7 @@ import { useToast } from '@/components/ui/use-toast'; import { Combobox } from '@/components/v2/combobox'; import { Table, TBody, Td, Tr } from '@/components/v2/table'; import { Tag } from '@/components/v2/tag'; -import { DEFAULT_RETENTION_DAYS, MINIMUM_DAYS } from '@/constants'; +import { ENTERPRISE_RETENTION_DAYS } from '@/constants'; import { env } from '@/env/frontend'; import { FragmentType, graphql, useFragment } from '@/gql'; import { ProjectType } from '@/gql/graphql'; @@ -49,6 +49,7 @@ import { canAccessTarget, TargetAccessScope } from '@/lib/access/target'; import { subDays } from '@/lib/date-time'; import { useToggle } from '@/lib/hooks'; import { cn } from '@/lib/utils'; +import { resolveRetentionInDaysBasedOrganizationPlan } from '@/utils'; import { zodResolver } from '@hookform/resolvers/zod'; import { Link, useRouter } from '@tanstack/react-router'; @@ -414,6 +415,7 @@ const TargetSettingsPage_TargetSettingsQuery = graphql(` organization(selector: $organizationSelector) { organization { id + plan rateLimit { retentionInDays } @@ -467,7 +469,8 @@ const conditionalBreakingChangesFormSchema = z.object({ value => Number(value), z .number({ required_error: 'Period is required' }) - .min(0, 'Period must be at least 0 days') + .min(1, 'Period must be at least 1 days') + .max(ENTERPRISE_RETENTION_DAYS, `Period must be at most ${ENTERPRISE_RETENTION_DAYS} days`) .transform(value => Math.round(value)), ), percentage: z.preprocess( @@ -491,7 +494,7 @@ const ConditionalBreakingChanges = (props: { }) => { const { toast } = useToast(); const [targetValidation, setValidation] = useMutation(SetTargetValidationMutation); - const [mutation, updateValidation] = useMutation( + const [_, updateValidation] = useMutation( TargetSettingsPage_UpdateTargetValidationSettingsMutation, ); const [targetSettings] = useQuery({ @@ -516,15 +519,16 @@ const ConditionalBreakingChanges = (props: { TargetSettings_TargetValidationSettingsFragment, targetSettings.data?.target?.validationSettings, ); + + const retentionInDaysBasedOrganizationPlan = + targetSettings.data?.organization?.organization?.rateLimit.retentionInDays; + const defaultDays = resolveRetentionInDaysBasedOrganizationPlan( + retentionInDaysBasedOrganizationPlan, + ); + const isEnabled = settings?.enabled || false; const possibleTargets = targetSettings.data?.targets.nodes; - const retentionInDays = - targetSettings.data?.organization?.organization?.rateLimit.retentionInDays ?? - DEFAULT_RETENTION_DAYS; - const defaultDays = - retentionInDays >= DEFAULT_RETENTION_DAYS ? DEFAULT_RETENTION_DAYS : MINIMUM_DAYS; - const conditionalBreakingChangesForm = useForm({ mode: 'all', resolver: zodResolver(conditionalBreakingChangesFormSchema), @@ -536,67 +540,59 @@ const ConditionalBreakingChanges = (props: { }, }); + // Set form values when settings are fetched useEffect(() => { - conditionalBreakingChangesForm.reset({ - period: settings?.period ?? defaultDays, - percentage: settings?.percentage ?? 0, - targetIds: settings?.targets.map(t => t.id) || [], - excludedClients: settings?.excludedClients ?? [], - }); + conditionalBreakingChangesForm.setValue('period', settings?.period ?? defaultDays); + conditionalBreakingChangesForm.setValue('percentage', settings?.percentage ?? 0); + conditionalBreakingChangesForm.setValue('targetIds', settings?.targets.map(t => t.id) || []); + conditionalBreakingChangesForm.setValue('excludedClients', settings?.excludedClients ?? []); }, [settings]); + const orgPlan = targetSettings.data?.organization?.organization?.plan; + async function onConditionalBreakingChangesFormSubmit( data: ConditionalBreakingChangesFormValues, ) { - await updateValidation({ - input: { - organizationSlug: props.organizationSlug, - projectSlug: props.projectSlug, - targetSlug: props.targetSlug, - percentage: data.percentage, - period: data.period, - targetIds: data.targetIds, - excludedClients: data.excludedClients, - }, - }); - if (mutation.data?.updateTargetValidationSettings.error?.inputErrors.period) { + // This is a workaround for the issue with zod's transform function + if (data.period > defaultDays) { + conditionalBreakingChangesForm.setError('period', { + message: `Retention period must be less than or equal to ${defaultDays} days based on ${orgPlan} plan`, + type: 'maxLength', + }); + return; + } + try { + const result = await updateValidation({ + input: { + organizationSlug: props.organizationSlug, + projectSlug: props.projectSlug, + targetSlug: props.targetSlug, + percentage: data.percentage, + period: data.period, + targetIds: data.targetIds, + excludedClients: data.excludedClients, + }, + }); + if (result.data?.updateTargetValidationSettings.error) { + toast({ + variant: 'destructive', + title: 'Error', + description: result.data.updateTargetValidationSettings.error.message, + }); + } else { + toast({ + variant: 'default', + title: 'Success', + description: 'Conditional breaking changes updated successfully', + }); + } + } catch (error) { toast({ variant: 'destructive', title: 'Error', - description: mutation.data?.updateTargetValidationSettings.error?.inputErrors.period, - }); - } else { - toast({ - variant: 'default', - title: 'Success', - description: 'Conditional breaking changes settings updated successfully', + description: 'Failed to update conditional breaking changes', }); } - // }).then(result => { - // if (mutation.data?.updateTargetValidationSettings.error?.inputErrors.period) { - // toast({ - // variant: 'destructive', - // title: 'Error', - // description: - // mutation.data.updateTargetValidationSettings.error.inputErrors.period[0] || - // 'Invalid period', - // }); - // } - // if (result.error || result.data?.updateTargetValidationSettings.error) { - // toast({ - // variant: 'destructive', - // title: 'Error', - // description: - // result.error?.message || result.data?.updateTargetValidationSettings.error?.message, - // }); - // } else { - // toast({ - // variant: 'default', - // title: 'Success', - // description: 'Conditional breaking changes settings updated successfully', - // }); - // } - // }); } return ( @@ -652,7 +648,7 @@ const ConditionalBreakingChanges = (props: { ( + render={({ field }) => ( = T extends false | '' | 0 | null | undefined ? never : T; // from lodash export function truthy(value: T): value is Truthy { @@ -25,3 +27,25 @@ export function useChartStyles() { // }, // ); } + +export function resolveRetentionInDaysBasedOrganizationPlan( + value: number | null | undefined, +): number { + if (value == null) { + return HOBBY_RETENTION_DAYS; + } + + if (value < HOBBY_RETENTION_DAYS) { + return HOBBY_RETENTION_DAYS; + } + + if (value > HOBBY_RETENTION_DAYS && value <= PRO_RETENTION_DAYS) { + return PRO_RETENTION_DAYS; + } + + if (value > PRO_RETENTION_DAYS) { + return ENTERPRISE_RETENTION_DAYS; + } + + return HOBBY_RETENTION_DAYS; +} From aebfc3ede0e036b13d33dc1de1a8679f2856ded5 Mon Sep 17 00:00:00 2001 From: TuvalSimha Date: Sat, 14 Dec 2024 20:42:41 +0200 Subject: [PATCH 3/4] fix --- .../web/app/src/pages/target-settings.tsx | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/web/app/src/pages/target-settings.tsx b/packages/web/app/src/pages/target-settings.tsx index d1ec5a0d5e..16b73c484a 100644 --- a/packages/web/app/src/pages/target-settings.tsx +++ b/packages/web/app/src/pages/target-settings.tsx @@ -1,7 +1,7 @@ import { ComponentProps, PropsWithoutRef, useCallback, useEffect, useMemo, useState } from 'react'; import clsx from 'clsx'; import { formatISO } from 'date-fns'; -import { use } from 'echarts'; +import { InfoIcon } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { useMutation, useQuery } from 'urql'; import { z } from 'zod'; @@ -36,6 +36,7 @@ import { QueryError } from '@/components/ui/query-error'; import { Spinner } from '@/components/ui/spinner'; import { Switch } from '@/components/ui/switch'; import { TimeAgo } from '@/components/ui/time-ago'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { useToast } from '@/components/ui/use-toast'; import { Combobox } from '@/components/v2/combobox'; import { Table, TBody, Td, Tr } from '@/components/v2/table'; @@ -556,7 +557,7 @@ const ConditionalBreakingChanges = (props: { // This is a workaround for the issue with zod's transform function if (data.period > defaultDays) { conditionalBreakingChangesForm.setError('period', { - message: `Retention period must be less than or equal to ${defaultDays} days based on ${orgPlan} plan`, + message: `Period must be at most ${defaultDays} days`, type: 'maxLength', }); return; @@ -681,6 +682,26 @@ const ConditionalBreakingChanges = (props: { )} /> +
+ + + + + + +

+ You can customize Conditional Breaking Change date range, +
+ based on your data retention and your Hive plan. +
+ Your plan: {orgPlan}. +
+ Date retention: {defaultDays} days. +

+
+
+
+
days.
{conditionalBreakingChangesForm.formState.errors.period && ( From 6f2bd70d353140a9d42623de1f1e5293196e6b7a Mon Sep 17 00:00:00 2001 From: TuvalSimha Date: Sun, 15 Dec 2024 10:56:59 +0200 Subject: [PATCH 4/4] clean --- .../web/app/src/pages/target-settings.tsx | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/web/app/src/pages/target-settings.tsx b/packages/web/app/src/pages/target-settings.tsx index 16b73c484a..e9d6b12e5d 100644 --- a/packages/web/app/src/pages/target-settings.tsx +++ b/packages/web/app/src/pages/target-settings.tsx @@ -644,7 +644,7 @@ const ConditionalBreakingChanges = (props: { )} >
-
+
A schema change is considered as breaking only if it affects more than % of traffic in the past - ( - - - - - - )} - /> -
+
@@ -702,6 +685,23 @@ const ConditionalBreakingChanges = (props: {
+ ( + + + + + + )} + /> days.
{conditionalBreakingChangesForm.formState.errors.period && (