@@ -111,14 +118,7 @@ export default function JobIdLayout(props: LayoutProps) {
);
}
- const sidebarNavItems = getSidebarNavItems(
- account?.name ?? '',
- data?.job,
- !isSystemConfigLoading && systemAppConfigData?.isMetricsServiceEnabled,
- !isSystemConfigLoading && systemAppConfigData?.isJobHooksEnabled
- );
-
- const badgeValue = getBadgeText(data.job.source?.options);
+ const badgeValue = getBadgeText(data.job.jobType, data.job.source?.options);
return (
@@ -178,66 +178,133 @@ export default function JobIdLayout(props: LayoutProps) {
);
}
-function getBadgeText(
+function getLabeledJobType(
+ jobTypeConfig?: JobTypeConfig,
options?: JobSourceOptions
-): 'Sync Job' | 'Generate Job' | 'AI Generate Job' {
+): 'Sync Job' | 'Generate Job' | 'AI Generate Job' | 'PII Detect Job' {
switch (options?.config.case) {
case 'generate':
return 'Generate Job';
case 'aiGenerate':
return 'AI Generate Job';
default:
- return 'Sync Job';
+ switch (jobTypeConfig?.jobType.case) {
+ case 'sync':
+ return 'Sync Job';
+ case 'piiDetect':
+ return 'PII Detect Job';
+ default:
+ return 'Sync Job';
+ }
}
}
+const getBadgeText = getLabeledJobType;
+
interface SidebarNav {
title: string;
href: string;
}
-function getSidebarNavItems(
- accountName: string,
- job?: Job,
- isMetricsServiceEnabled?: boolean,
- isJobHooksEnabled?: boolean
-): SidebarNav[] {
- if (!job) {
+function useGetSidebarNavItems(job?: Job): SidebarNav[] {
+ const { account } = useAccount();
+ const { data: systemAppConfigData, isLoading: isSystemConfigLoading } =
+ useGetSystemAppConfig();
+
+ if (!account || !job) {
return [{ title: 'Overview', href: `` }];
}
- const basePath = `/${accountName}/jobs/${job.id}`;
-
- const nav = [
- {
- title: 'Overview',
- href: `${basePath}`,
- },
- {
- title: 'Source',
- href: `${basePath}/source`,
- },
- {
- title: 'Destinations',
- href: `${basePath}/destinations`,
- },
- ];
-
- if (isJobSubsettable(job)) {
- nav.push({
- title: 'Subsets',
- href: `${basePath}/subsets`,
- });
- }
+ const badgeText = getLabeledJobType(job.jobType, job.source?.options);
+ const basePath = `/${account.name}/jobs/${job.id}`;
+ const isMetricsServiceEnabled =
+ !isSystemConfigLoading && systemAppConfigData?.isMetricsServiceEnabled;
+ const isJobHooksEnabled =
+ !isSystemConfigLoading && systemAppConfigData?.isJobHooksEnabled;
- if (isMetricsServiceEnabled) {
- nav.push({
- title: 'Usage',
- href: `${basePath}/usage`,
- });
- }
+ switch (badgeText) {
+ case 'Sync Job': {
+ const nav = [
+ { title: 'Overview', href: basePath },
+ { title: 'Source', href: `${basePath}/source` },
+ { title: 'Destinations', href: `${basePath}/destinations` },
+ ];
+ if (isJobSubsettable(job)) {
+ nav.push({
+ title: 'Subsets',
+ href: `${basePath}/subsets`,
+ });
+ }
+ if (isMetricsServiceEnabled) {
+ nav.push({
+ title: 'Usage',
+ href: `${basePath}/usage`,
+ });
+ }
+ if (isJobHooksEnabled) {
+ nav.push({
+ title: 'Hooks',
+ href: `${basePath}/hooks`,
+ });
+ }
- if (isJobHooksEnabled) {
- nav.push({ title: 'Hooks', href: `${basePath}/hooks` });
- }
+ return nav;
+ }
+
+ case 'Generate Job': {
+ const nav = [
+ { title: 'Overview', href: basePath },
+ { title: 'Source', href: `${basePath}/source` },
+ { title: 'Destinations', href: `${basePath}/destinations` },
+ ];
+ if (isMetricsServiceEnabled) {
+ nav.push({
+ title: 'Usage',
+ href: `${basePath}/usage`,
+ });
+ }
+ if (isJobHooksEnabled) {
+ nav.push({
+ title: 'Hooks',
+ href: `${basePath}/hooks`,
+ });
+ }
- return nav;
+ return nav;
+ }
+ case 'AI Generate Job': {
+ const nav = [
+ { title: 'Overview', href: basePath },
+ { title: 'Source', href: `${basePath}/source` },
+ { title: 'Destinations', href: `${basePath}/destinations` },
+ ];
+ if (isMetricsServiceEnabled) {
+ nav.push({
+ title: 'Usage',
+ href: `${basePath}/usage`,
+ });
+ }
+ if (isJobHooksEnabled) {
+ nav.push({
+ title: 'Hooks',
+ href: `${basePath}/hooks`,
+ });
+ }
+ return nav;
+ }
+ case 'PII Detect Job': {
+ const nav = [
+ { title: 'Overview', href: basePath },
+ { title: 'Source', href: `${basePath}/source` },
+ ];
+ if (isMetricsServiceEnabled) {
+ nav.push({
+ title: 'Usage',
+ href: `${basePath}/usage`,
+ });
+ }
+ return nav;
+ }
+ default: {
+ return [{ title: 'Overview', href: basePath }];
+ }
+ }
}
From f53f8e649269cae9b9d480080b954b4d4395662f Mon Sep 17 00:00:00 2001
From: Nick Z <2420177+nickzelei@users.noreply.github.com>
Date: Wed, 12 Mar 2025 11:58:40 -0700
Subject: [PATCH 07/19] Filling out remaining form
---
.../components/PiiDetectConnectionCard.tsx | 170 ++++++++++++++++++
.../components/SourceConnectionCard.tsx | 6 +-
.../app/(mgmt)/[account]/jobs/[id]/util.ts | 4 +
.../new/job/piidetect/schema/FormInputs.tsx | 75 +++++++-
.../new/job/piidetect/schema/page.tsx | 22 ++-
.../new/job/piidetect/schema/stores.ts | 59 +++++-
.../apps/web/components/forms/FormHeader.tsx | 7 +-
7 files changed, 336 insertions(+), 7 deletions(-)
create mode 100644 frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/source/components/PiiDetectConnectionCard.tsx
diff --git a/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/source/components/PiiDetectConnectionCard.tsx b/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/source/components/PiiDetectConnectionCard.tsx
new file mode 100644
index 0000000000..3274950b21
--- /dev/null
+++ b/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/source/components/PiiDetectConnectionCard.tsx
@@ -0,0 +1,170 @@
+import { FilterPatternTableIdentifier } from '@/app/(mgmt)/[account]/new/job/job-form-validations';
+import {
+ DataSampling,
+ TableScanFilterMode,
+ TableScanFilterPatterns,
+ UserPrompt,
+} from '@/app/(mgmt)/[account]/new/job/piidetect/schema/FormInputs';
+import { usePiiDetectionSchemaStore } from '@/app/(mgmt)/[account]/new/job/piidetect/schema/stores';
+import ButtonText from '@/components/ButtonText';
+import { useAccount } from '@/components/providers/account-provider';
+import Spinner from '@/components/Spinner';
+import { Button } from '@/components/ui/button';
+import { useQuery } from '@connectrpc/connect-query';
+import {
+ Connection,
+ ConnectionDataService,
+ ConnectionService,
+ JobService,
+} from '@neosync/sdk';
+import { FormEvent, ReactElement, useEffect, useMemo } from 'react';
+import { getConnectionIdFromSource } from './util';
+
+interface Props {
+ jobId: string;
+}
+
+export default function PiiDetectConnectionCard({
+ jobId,
+}: Props): ReactElement {
+ const { account } = useAccount();
+ const {
+ data,
+ refetch: mutate,
+ isLoading: isJobDataLoading,
+ } = useQuery(JobService.method.getJob, { id: jobId }, { enabled: !!jobId });
+ const sourceConnectionId = getConnectionIdFromSource(data?.job?.source);
+
+ const { data: connectionsData } = useQuery(
+ ConnectionService.method.getConnections,
+ { accountId: account?.id },
+ { enabled: !!account?.id }
+ );
+ const connections = connectionsData?.connections ?? [];
+ const connectionsRecord = connections.reduce(
+ (record, conn) => {
+ record[conn.id] = conn;
+ return record;
+ },
+ {} as Record
+ );
+
+ const {
+ data: connectionSchemaDataResp,
+ isPending,
+ isFetching,
+ } = useQuery(
+ ConnectionDataService.method.getConnectionSchema,
+ { connectionId: sourceConnectionId },
+ { enabled: !!sourceConnectionId }
+ );
+
+ const availableSchemas = useMemo(() => {
+ if (isPending || !connectionSchemaDataResp) {
+ return [];
+ }
+ const uniqueSchemas = new Set();
+ connectionSchemaDataResp?.schemas?.forEach((schema) => {
+ uniqueSchemas.add(schema.schema);
+ });
+ return Array.from(uniqueSchemas);
+ }, [connectionSchemaDataResp, isPending, isFetching]);
+
+ const availableTableIdentifiers = useMemo(() => {
+ if (isPending || !connectionSchemaDataResp) {
+ return [];
+ }
+ const uniqueTableIdentifiers = new Map<
+ string,
+ FilterPatternTableIdentifier
+ >();
+ connectionSchemaDataResp?.schemas?.forEach((schema) => {
+ uniqueTableIdentifiers.set(`${schema.schema}.${schema.table}`, {
+ schema: schema.schema,
+ table: schema.table,
+ });
+ });
+ return Array.from(uniqueTableIdentifiers.values());
+ }, [connectionSchemaDataResp, isPending, isFetching]);
+
+ const {
+ formData,
+ setFormData,
+ errors,
+ setErrors,
+ isSubmitting,
+ setSubmitting,
+ sourcedFromRemote,
+ setFromRemoteJob: setFromRemote,
+ } = usePiiDetectionSchemaStore();
+
+ useEffect(() => {
+ if (sourcedFromRemote || isJobDataLoading || !data?.job) {
+ return;
+ }
+ setFromRemote(data.job);
+ }, [sourcedFromRemote, isJobDataLoading, data?.job, setFromRemote]);
+
+ async function onSubmit(e: FormEvent) {
+ e.preventDefault();
+ if (isSubmitting) {
+ return;
+ }
+
+ // todo
+ }
+
+ return (
+
+ );
+}
diff --git a/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/source/components/SourceConnectionCard.tsx b/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/source/components/SourceConnectionCard.tsx
index 73e48cd381..e8d7e5cd11 100644
--- a/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/source/components/SourceConnectionCard.tsx
+++ b/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/source/components/SourceConnectionCard.tsx
@@ -2,10 +2,11 @@
import { useQuery } from '@connectrpc/connect-query';
import { JobService } from '@neosync/sdk';
import { ReactElement } from 'react';
-import { isAiDataGenJob, isDataGenJob } from '../../util';
+import { isAiDataGenJob, isDataGenJob, isPiiDetectJob } from '../../util';
import AiDataGenConnectionCard from './AiDataGenConnectionCard';
import DataGenConnectionCard from './DataGenConnectionCard';
import DataSyncConnectionCard from './DataSyncConnectionCard';
+import PiiDetectConnectionCard from './PiiDetectConnectionCard';
import SchemaPageSkeleton from './SchemaPageSkeleton';
interface Props {
@@ -28,5 +29,8 @@ export default function SourceConnectionCard({ jobId }: Props): ReactElement {
if (isAiDataGenJob(data?.job)) {
return ;
}
+ if (isPiiDetectJob(data?.job)) {
+ return ;
+ }
return ;
}
diff --git a/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/util.ts b/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/util.ts
index 539d26b713..44dfc608ba 100644
--- a/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/util.ts
+++ b/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/util.ts
@@ -7,3 +7,7 @@ export function isDataGenJob(job?: Job): boolean {
export function isAiDataGenJob(job?: Job): boolean {
return job?.source?.options?.config.case === 'aiGenerate';
}
+
+export function isPiiDetectJob(job?: Job): boolean {
+ return job?.jobType?.jobType.case === 'piiDetect';
+}
diff --git a/frontend/apps/web/app/(mgmt)/[account]/new/job/piidetect/schema/FormInputs.tsx b/frontend/apps/web/app/(mgmt)/[account]/new/job/piidetect/schema/FormInputs.tsx
index a6b817250b..eca73ea4a4 100644
--- a/frontend/apps/web/app/(mgmt)/[account]/new/job/piidetect/schema/FormInputs.tsx
+++ b/frontend/apps/web/app/(mgmt)/[account]/new/job/piidetect/schema/FormInputs.tsx
@@ -10,15 +10,77 @@ import {
CardHeader,
CardTitle,
} from '@/components/ui/card';
+import { Textarea } from '@/components/ui/textarea';
import { ToggleGroup } from '@/components/ui/toggle-group';
import { TableIcon } from '@radix-ui/react-icons';
import { ReactElement, useCallback, useMemo } from 'react';
import {
+ DataSamplingFormValue,
FilterPatternTableIdentifier,
TableScanFilterModeFormValue,
TableScanFilterPatternsFormValue,
} from '../../job-form-validations';
+interface UserPromptProps {
+ error?: string;
+ value: string;
+ onChange(value: string): void;
+}
+
+export function UserPrompt(props: UserPromptProps): ReactElement {
+ const { error, value, onChange } = props;
+
+ return (
+
+
+
+ );
+}
+
+interface DataSamplingProps {
+ errors?: Record;
+ value: DataSamplingFormValue;
+ onChange(value: DataSamplingFormValue): void;
+}
+
+export function DataSampling(props: DataSamplingProps): ReactElement {
+ const { errors, value, onChange } = props;
+
+ return (
+
+
+ {
+ onChange({ isEnabled: value === 'enabled' });
+ }}
+ value={value.isEnabled ? 'enabled' : 'disabled'}
+ >
+ Enabled
+ Disabled
+
+
+
+ );
+}
+
interface TableScanFilterModeProps {
error?: string;
value: TableScanFilterModeFormValue;
@@ -33,9 +95,10 @@ export function TableScanFilterMode(
return (
- Select tables to scan for PII.
+ {cardDescription}
@@ -310,3 +372,64 @@ export function TableScanFilterPatternTables(
);
}
+
+function useTableCardDescription(mode: TableScanFilterModeFormValue): string {
+ return useMemo(() => {
+ switch (mode) {
+ case 'include_all':
+ return 'Select all tables to scan for PII.';
+ case 'include':
+ return 'Select tables to scan for PII.';
+ case 'exclude':
+ return 'Select tables to exclude from PII scanning.';
+ }
+ }, [mode]);
+}
+
+function useGetTableLeftEmptyStates(
+ mode: TableScanFilterModeFormValue
+): EmptyStateMessage {
+ return useMemo(() => {
+ switch (mode) {
+ case 'include_all':
+ return {
+ noOptions: 'Unable to load tables or found none',
+ noSelected: 'All tables have been added!',
+ };
+ case 'include':
+ return {
+ noOptions: 'Unable to load tables or found none',
+ noSelected: 'All tables available have been included!',
+ };
+ case 'exclude':
+ return {
+ noOptions: 'Unable to load tables or found none',
+ noSelected: 'All tables available have been excluded!',
+ };
+ }
+ }, [mode]);
+}
+
+function useGetTableRightEmptyStates(
+ mode: TableScanFilterModeFormValue
+): EmptyStateMessage {
+ return useMemo(() => {
+ switch (mode) {
+ case 'include_all':
+ return {
+ noOptions: 'Unable to load tables or found none',
+ noSelected: 'All tables have been added!',
+ };
+ case 'include':
+ return {
+ noOptions: 'Unable to load tables or found none',
+ noSelected: 'Add tables to scan for PII!',
+ };
+ case 'exclude':
+ return {
+ noOptions: 'Unable to load tables or found none',
+ noSelected: 'Add tables to exclude from PII scanning!',
+ };
+ }
+ }, [mode]);
+}
diff --git a/frontend/apps/web/components/DualListBox/DualListBox.tsx b/frontend/apps/web/components/DualListBox/DualListBox.tsx
index d2c7362e2b..6f9bdbdffa 100644
--- a/frontend/apps/web/components/DualListBox/DualListBox.tsx
+++ b/frontend/apps/web/components/DualListBox/DualListBox.tsx
@@ -35,7 +35,7 @@ interface Props {
rightEmptyState?: EmptyStateMessage;
}
-interface EmptyStateMessage {
+export interface EmptyStateMessage {
noOptions?: string;
noSelected?: string;
}
From 8514cb0b9f931473663436bf9e36f7495e041e51 Mon Sep 17 00:00:00 2001
From: Nick Z <2420177+nickzelei@users.noreply.github.com>
Date: Wed, 12 Mar 2025 13:14:50 -0700
Subject: [PATCH 10/19] updates descripts
---
.../[account]/new/job/piidetect/schema/FormInputs.tsx | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/frontend/apps/web/app/(mgmt)/[account]/new/job/piidetect/schema/FormInputs.tsx b/frontend/apps/web/app/(mgmt)/[account]/new/job/piidetect/schema/FormInputs.tsx
index 693477da9a..6aee1ee347 100644
--- a/frontend/apps/web/app/(mgmt)/[account]/new/job/piidetect/schema/FormInputs.tsx
+++ b/frontend/apps/web/app/(mgmt)/[account]/new/job/piidetect/schema/FormInputs.tsx
@@ -1,5 +1,3 @@
-import { ToggleGroupItem } from '@/components/ui/toggle-group';
-
import DualListBox, {
EmptyStateMessage,
Option,
@@ -14,7 +12,7 @@ import {
CardTitle,
} from '@/components/ui/card';
import { Textarea } from '@/components/ui/textarea';
-import { ToggleGroup } from '@/components/ui/toggle-group';
+import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { TableIcon } from '@radix-ui/react-icons';
import { ReactElement, useCallback, useMemo } from 'react';
import {
@@ -64,7 +62,7 @@ export function DataSampling(props: DataSamplingProps): ReactElement {
@@ -99,7 +97,7 @@ export function TableScanFilterMode(
@@ -151,7 +149,7 @@ export function TableScanFilterPatterns(
Date: Wed, 12 Mar 2025 13:42:25 -0700
Subject: [PATCH 11/19] adds source id, but disables for now
---
.../components/PiiDetectConnectionCard.tsx | 106 ++++++++++++++----
.../[account]/new/job/job-form-validations.ts | 10 ++
.../new/job/piidetect/schema/FormInputs.tsx | 77 +++++++++++++
.../new/job/piidetect/schema/stores.ts | 48 ++++++++
4 files changed, 219 insertions(+), 22 deletions(-)
diff --git a/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/source/components/PiiDetectConnectionCard.tsx b/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/source/components/PiiDetectConnectionCard.tsx
index e331c68da5..159a254875 100644
--- a/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/source/components/PiiDetectConnectionCard.tsx
+++ b/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/source/components/PiiDetectConnectionCard.tsx
@@ -1,24 +1,36 @@
import {
+ EditPiiDetectionJobFormValues,
FilterPatternTableIdentifier,
- PiiDetectionSchemaFormValues,
} from '@/app/(mgmt)/[account]/new/job/job-form-validations';
import {
DataSampling,
+ SourceConnectionId,
TableScanFilterMode,
TableScanFilterPatterns,
UserPrompt,
} from '@/app/(mgmt)/[account]/new/job/piidetect/schema/FormInputs';
-import { usePiiDetectionSchemaStore } from '@/app/(mgmt)/[account]/new/job/piidetect/schema/stores';
+import { useEditPiiDetectionSchemaStore } from '@/app/(mgmt)/[account]/new/job/piidetect/schema/stores';
import ButtonText from '@/components/ButtonText';
+import { useAccount } from '@/components/providers/account-provider';
import Spinner from '@/components/Spinner';
import { Button } from '@/components/ui/button';
import { useMutation, useQuery } from '@connectrpc/connect-query';
-import { ConnectionDataService, JobService } from '@neosync/sdk';
-import { FormEvent, ReactElement, useEffect, useMemo } from 'react';
+import {
+ Connection,
+ ConnectionDataService,
+ ConnectionService,
+ JobService,
+} from '@neosync/sdk';
+import {
+ FormEvent,
+ ReactElement,
+ useCallback,
+ useEffect,
+ useMemo,
+} from 'react';
import { toast } from 'sonner';
import { ValidationError } from 'yup';
-import { toPiiDetectJobTypeConfig } from '../../../util';
-import { getConnectionIdFromSource } from './util';
+import { toJobSource, toPiiDetectJobTypeConfig } from '../../../util';
interface Props {
jobId: string;
@@ -32,20 +44,30 @@ export default function PiiDetectConnectionCard({
refetch: mutate,
isLoading: isJobDataLoading,
} = useQuery(JobService.method.getJob, { id: jobId }, { enabled: !!jobId });
- const sourceConnectionId = getConnectionIdFromSource(data?.job?.source);
const { mutateAsync: updateJobSourceConnection } = useMutation(
JobService.method.updateJobSourceConnection
);
+ const {
+ formData,
+ setFormData,
+ errors,
+ setErrors,
+ isSubmitting,
+ setSubmitting,
+ sourcedFromRemote,
+ setFromRemoteJob: setFromRemote,
+ } = useEditPiiDetectionSchemaStore();
+
const {
data: connectionSchemaDataResp,
isPending,
isFetching,
} = useQuery(
ConnectionDataService.method.getConnectionSchema,
- { connectionId: sourceConnectionId },
- { enabled: !!sourceConnectionId }
+ { connectionId: formData.sourceId },
+ { enabled: !!formData.sourceId }
);
const availableSchemas = useMemo(() => {
@@ -76,17 +98,6 @@ export default function PiiDetectConnectionCard({
return Array.from(uniqueTableIdentifiers.values());
}, [connectionSchemaDataResp, isPending, isFetching]);
- const {
- formData,
- setFormData,
- errors,
- setErrors,
- isSubmitting,
- setSubmitting,
- sourcedFromRemote,
- setFromRemoteJob: setFromRemote,
- } = usePiiDetectionSchemaStore();
-
useEffect(() => {
if (sourcedFromRemote || isJobDataLoading || !data?.job) {
return;
@@ -94,6 +105,8 @@ export default function PiiDetectConnectionCard({
setFromRemote(data.job);
}, [sourcedFromRemote, isJobDataLoading, data?.job, setFromRemote]);
+ const getConnectionById = useGetConnectionById();
+
async function onSubmit(e: FormEvent) {
e.preventDefault();
const job = data?.job;
@@ -105,7 +118,7 @@ export default function PiiDetectConnectionCard({
setSubmitting(true);
setErrors({});
- const validatedData = await PiiDetectionSchemaFormValues.validate(
+ const validatedData = await EditPiiDetectionJobFormValues.validate(
formData,
{
abortEarly: false,
@@ -116,7 +129,16 @@ export default function PiiDetectConnectionCard({
id: job.id,
mappings: [],
virtualForeignKeys: [],
- source: job.source,
+ source: toJobSource(
+ {
+ connect: {
+ destinations: [],
+ sourceId: validatedData.sourceId,
+ sourceOptions: {},
+ },
+ },
+ getConnectionById
+ ),
jobType: {
jobType: {
case: 'piiDetect',
@@ -146,6 +168,12 @@ export default function PiiDetectConnectionCard({
return (
);
}
+
+function useGetConnectionById(): (id: string) => Connection | undefined {
+ const connectionsRecord = useGetConnectionsRecord();
+ return useCallback(
+ (id: string) => connectionsRecord[id],
+ [connectionsRecord]
+ );
+}
+
+function useGetConnectionsRecord(): Record {
+ const { account } = useAccount();
+ const {
+ data: connectionsData,
+ isLoading,
+ isPending,
+ } = useQuery(
+ ConnectionService.method.getConnections,
+ { accountId: account?.id },
+ { enabled: !!account?.id }
+ );
+ return useMemo(() => {
+ if (isLoading || isPending || !connectionsData) {
+ return {};
+ }
+ const connections = connectionsData?.connections ?? [];
+ return connections.reduce(
+ (record, conn) => {
+ record[conn.id] = conn;
+ return record;
+ },
+ {} as Record
+ );
+ }, [connectionsData, isLoading, isPending]);
+}
diff --git a/frontend/apps/web/app/(mgmt)/[account]/new/job/job-form-validations.ts b/frontend/apps/web/app/(mgmt)/[account]/new/job/job-form-validations.ts
index 18879f1429..87ff28f314 100644
--- a/frontend/apps/web/app/(mgmt)/[account]/new/job/job-form-validations.ts
+++ b/frontend/apps/web/app/(mgmt)/[account]/new/job/job-form-validations.ts
@@ -516,6 +516,16 @@ export const CreatePiiDetectionJobFormValues = Yup.object()
export type CreatePiiDetectionJobFormValues = Yup.InferType<
typeof CreatePiiDetectionJobFormValues
>;
+
+export const EditPiiDetectionJobFormValues = Yup.object()
+ .shape({
+ sourceId: Yup.string().required('Source is required').uuid(),
+ })
+ .concat(PiiDetectionSchemaFormValues);
+export type EditPiiDetectionJobFormValues = Yup.InferType<
+ typeof EditPiiDetectionJobFormValues
+>;
+
export interface DefineFormValuesContext {
accountId: string;
isJobNameAvailable: UseMutateAsyncFunction<
diff --git a/frontend/apps/web/app/(mgmt)/[account]/new/job/piidetect/schema/FormInputs.tsx b/frontend/apps/web/app/(mgmt)/[account]/new/job/piidetect/schema/FormInputs.tsx
index 6aee1ee347..49577a7f78 100644
--- a/frontend/apps/web/app/(mgmt)/[account]/new/job/piidetect/schema/FormInputs.tsx
+++ b/frontend/apps/web/app/(mgmt)/[account]/new/job/piidetect/schema/FormInputs.tsx
@@ -4,6 +4,7 @@ import DualListBox, {
} from '@/components/DualListBox/DualListBox';
import FormErrorMessage from '@/components/FormErrorMessage';
import FormHeader from '@/components/forms/FormHeader';
+import { useAccount } from '@/components/providers/account-provider';
import {
Card,
CardContent,
@@ -11,10 +12,20 @@ import {
CardHeader,
CardTitle,
} from '@/components/ui/card';
+import {
+ Select,
+ SelectContent,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
+import { splitConnections } from '@/libs/utils';
+import { useQuery } from '@connectrpc/connect-query';
+import { ConnectionService } from '@neosync/sdk';
import { TableIcon } from '@radix-ui/react-icons';
import { ReactElement, useCallback, useMemo } from 'react';
+import ConnectionSelectContent from '../../connect/ConnectionSelectContent';
import {
DataSamplingFormValue,
FilterPatternTableIdentifier,
@@ -22,6 +33,72 @@ import {
TableScanFilterPatternsFormValue,
} from '../../job-form-validations';
+interface SourceConnectionIdProps {
+ error?: string;
+ value: string;
+ onChange(value: string): void;
+ isDisabled?: boolean;
+}
+
+export function SourceConnectionId(
+ props: SourceConnectionIdProps
+): ReactElement {
+ const { error, value, onChange, isDisabled } = props;
+
+ const { account } = useAccount();
+ const {
+ data: connectionsResp,
+ isLoading,
+ isPending,
+ } = useQuery(
+ ConnectionService.method.getConnections,
+ {
+ accountId: account?.id,
+ },
+ { enabled: !!account?.id }
+ );
+
+ const connections = useMemo(() => {
+ if (isLoading || isPending || !connectionsResp) {
+ return { postgres: [], mysql: [], mssql: [] };
+ }
+ return splitConnections(connectionsResp.connections);
+ }, [connectionsResp, isLoading, isPending]);
+
+ return (
+
+
+
+
+
+ );
+}
+
interface UserPromptProps {
error?: string;
value: string;
diff --git a/frontend/apps/web/app/(mgmt)/[account]/new/job/piidetect/schema/stores.ts b/frontend/apps/web/app/(mgmt)/[account]/new/job/piidetect/schema/stores.ts
index 8b9cd3a39f..6feb5d4652 100644
--- a/frontend/apps/web/app/(mgmt)/[account]/new/job/piidetect/schema/stores.ts
+++ b/frontend/apps/web/app/(mgmt)/[account]/new/job/piidetect/schema/stores.ts
@@ -1,7 +1,9 @@
+import { getConnectionIdFromSource } from '@/app/(mgmt)/[account]/jobs/[id]/source/components/util';
import { BaseHookStore } from '@/util/zustand.stores.util';
import { Job } from '@neosync/sdk';
import { create } from 'zustand';
import {
+ EditPiiDetectionJobFormValues,
PiiDetectionSchemaFormValues,
TableScanFilterFormValue,
} from '../../job-form-validations';
@@ -22,6 +24,13 @@ function getInitialFormState(): PiiDetectionSchemaFormValues {
};
}
+function getInitialEditFormState(): EditPiiDetectionJobFormValues {
+ return {
+ ...getInitialFormState(),
+ sourceId: '',
+ };
+}
+
interface PiiDetectionSchemaStore
extends BaseHookStore {
sourcedFromRemote: boolean;
@@ -55,6 +64,45 @@ export const usePiiDetectionSchemaStore = create(
})
);
+interface EditPiiDetectionSchemaStore
+ extends BaseHookStore {
+ sourcedFromRemote: boolean;
+ setFromRemoteJob(job: Job): void;
+}
+
+export const useEditPiiDetectionSchemaStore =
+ create((set) => ({
+ formData: getInitialEditFormState(),
+ errors: {},
+ isSubmitting: false,
+ sourcedFromRemote: false,
+ setFromRemoteJob: (job) =>
+ set({
+ formData: getEditFormStateFromJob(job),
+ sourcedFromRemote: true,
+ isSubmitting: false,
+ errors: {},
+ }),
+ setFormData: (data) =>
+ set((state) => ({ formData: { ...state.formData, ...data } })),
+ setErrors: (errors) => set({ errors }),
+ setSubmitting: (isSubmitting) => set({ isSubmitting }),
+ resetForm: () =>
+ set({
+ formData: getInitialEditFormState(),
+ errors: {},
+ isSubmitting: false,
+ sourcedFromRemote: false,
+ }),
+ }));
+
+function getEditFormStateFromJob(job: Job): EditPiiDetectionJobFormValues {
+ return {
+ ...getFormStateFromJob(job),
+ sourceId: getConnectionIdFromSource(job.source) ?? '',
+ };
+}
+
function getFormStateFromJob(job: Job): PiiDetectionSchemaFormValues {
if (!job || job.jobType?.jobType.case !== 'piiDetect') {
return {
From 6d375d6bc1b5b22cc9e1471be305e0710a643549 Mon Sep 17 00:00:00 2001
From: Nick Z <2420177+nickzelei@users.noreply.github.com>
Date: Wed, 12 Mar 2025 13:45:49 -0700
Subject: [PATCH 12/19] allow changing source id but resets patterns
---
.../source/components/PiiDetectConnectionCard.tsx | 15 +++++++++++++--
.../new/job/piidetect/schema/FormInputs.tsx | 4 +---
2 files changed, 14 insertions(+), 5 deletions(-)
diff --git a/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/source/components/PiiDetectConnectionCard.tsx b/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/source/components/PiiDetectConnectionCard.tsx
index 159a254875..e58b290a4e 100644
--- a/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/source/components/PiiDetectConnectionCard.tsx
+++ b/frontend/apps/web/app/(mgmt)/[account]/jobs/[id]/source/components/PiiDetectConnectionCard.tsx
@@ -170,9 +170,20 @@ export default function PiiDetectConnectionCard({