Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#16 show data in frontend #17

Merged
merged 14 commits into from
Jan 25, 2024
459 changes: 293 additions & 166 deletions app/package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@faker-js/faker": "^8.3.1",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-table": "^8.11.7",
"@tanstack/react-virtual": "^3.0.2",
"@temporalio/activity": "^1.9.0",
Expand All @@ -31,8 +32,8 @@
"dotenv": "^16.3.2",
"env-var": "^7.4.1",
"fuse.js": "^6.6.2",
"libphonenumber-js": "^1.10.54",
"immer": "^10.0.3",
"libphonenumber-js": "^1.10.54",
"lodash": "^4.17.21",
"lucide-react": "^0.312.0",
"minio": "^7.1.3",
Expand Down
19 changes: 19 additions & 0 deletions app/src/app/CreateTestImport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,32 @@ export const CreateTestImporter = () => {
key: "email",
label: "E-Mail",
type: "text",
validations: [
{
type: "email",
},
{ type: "unique" },
],
},
{
key: "work role",
keyAlternatives: ["position"],
label: "Role",
type: "text",
},
{
key: "department",
label: "Department",
keyAlternatives: ["abteilung"],
type: "text",
validations: [
{
type: "enum",

values: ["IT", "HR", "Support"],
},
],
},
],
}),
});
Expand Down
31 changes: 31 additions & 0 deletions app/src/app/api/importer/[slug]/ImporterDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ export interface ImporterStatus {
isImporting: boolean;
dataMappingRecommendations: DataMappingRecommendation[] | null;
dataMapping: DataMapping[] | null;

sourceData: {
bucket: string;
fileReference: string;
} | null;

validations: { bucket: string; fileReference: string } | null;
}

export interface DataMappingRecommendation {
Expand All @@ -51,3 +58,27 @@ export interface DataMapping {
targetColumn: string | null;
sourceColumn: string;
}

export interface DataSetPatch {
rowId: number;
/**
* target column
*/
column: string;
newValue: string | number | null;
}

export interface DataValidation {
rowId: number;
column: string;
errors: ValidationError[];
}

export interface ValidationError {
type: "required" | "unique" | "regex" | "phone" | "email";
message: string;
}

export type SourceData = {
rowId: number;
} & Record<string, string | number | null>;
7 changes: 3 additions & 4 deletions app/src/app/api/importer/[slug]/mappings/route.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { getTemporalWorkflowClient } from "@/lib/temporalClient";
import { NextRequest, NextResponse } from "next/server";
import { DataMapping } from "../ImporterDto";

export async function PUT(
_req: NextRequest,
{ params, body }: { params: { slug: string }; body: DataMapping[] }
req: NextRequest,
{ params }: { params: { slug: string } }
) {
const { slug: importerId } = params;
const client = getTemporalWorkflowClient();
const handle = client.getHandle(importerId);
const mappings = body;
const mappings = await req.json();
await handle.executeUpdate("importer:update-mapping", {
args: [{ mappings }],
});
Expand Down
14 changes: 14 additions & 0 deletions app/src/app/api/importer/[slug]/patches/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { getTemporalWorkflowClient } from "@/lib/temporalClient";
import { NextRequest, NextResponse } from "next/server";
import { DataSetPatch } from "../ImporterDto";

export async function GET(
_req: NextRequest,
{ params }: { params: { slug: string } }
) {
const { slug: importerId } = params;
const client = getTemporalWorkflowClient();
const handle = client.getHandle(importerId);
const result = await handle.query<DataSetPatch[]>("importer:patches");
return NextResponse.json(result);
}
35 changes: 35 additions & 0 deletions app/src/app/api/importer/[slug]/source-data/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import minioClient from "@/lib/minioClient";
import { getTemporalWorkflowClient } from "@/lib/temporalClient";
import { NextRequest, NextResponse } from "next/server";
import { ImporterStatus } from "../ImporterDto";

export async function GET(
_req: NextRequest,
{ params }: { params: { slug: string } },
res: NextResponse
) {
const { slug: importerId } = params;
const client = getTemporalWorkflowClient();
const handle = client.getHandle(importerId);
const result = await handle.query<ImporterStatus>("importer:status");
Jank1310 marked this conversation as resolved.
Show resolved Hide resolved
if (!result.sourceData) {
return new NextResponse(undefined, { status: 404 });
}
const stream = await minioClient.getObject(
result.sourceData.bucket,
result.sourceData.fileReference
);
// TODO find better to directly return stream...
const buffers = [];
// node.js readable streams implement the async iterator protocol
for await (const data of stream) {
buffers.push(data);
}
const fileAsBuffer = Buffer.concat(buffers);
return new Response(fileAsBuffer, {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
}
34 changes: 34 additions & 0 deletions app/src/app/api/importer/[slug]/validations/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import minioClient from "@/lib/minioClient";
import { getTemporalWorkflowClient } from "@/lib/temporalClient";
import { NextRequest, NextResponse } from "next/server";
import { ImporterStatus } from "../ImporterDto";

export async function GET(
_req: NextRequest,
{ params }: { params: { slug: string } }
) {
const { slug: importerId } = params;
const client = getTemporalWorkflowClient();
const handle = client.getHandle(importerId);
const result = await handle.query<ImporterStatus>("importer:status");
Jank1310 marked this conversation as resolved.
Show resolved Hide resolved
if (!result.validations) {
return new NextResponse("not found", { status: 404 });
}
const stream = await minioClient.getObject(
result.validations.bucket,
result.validations.fileReference
);
// TODO find better to directly return stream...
const buffers = [];
// node.js readable streams implement the async iterator protocol
for await (const data of stream) {
buffers.push(data);
}
const fileAsBuffer = Buffer.concat(buffers);
return new Response(fileAsBuffer, {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
}
15 changes: 14 additions & 1 deletion app/src/app/importer/[id]/mapping/ShowMappings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,27 @@ interface Mapping {

const ShowMappings = ({ importerDto: initialImporterDto }: Props) => {
const { push } = useRouter();
const [enablePolling, setEnablePolling] = React.useState(false);
const { importer } = useGetImporter(
initialImporterDto.importerId,
undefined,
enablePolling ? 1000 : undefined,
initialImporterDto
);
const {
status: { dataMappingRecommendations },
} = importer;

React.useEffect(() => {
if (
!dataMappingRecommendations ||
dataMappingRecommendations.length === 0
) {
setEnablePolling(true);
} else {
setEnablePolling(false);
}
}, [dataMappingRecommendations]);

const [currentMappings, setCurrentMappings] = React.useState<Mapping[]>([]);
React.useEffect(() => {
const newMapping =
Expand Down
48 changes: 45 additions & 3 deletions app/src/app/importer/[id]/validate/Validation.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,60 @@
import { ImporterDto } from "@/app/api/importer/[slug]/ImporterDto";
"use client";
import {
DataSetPatch,
DataValidation,
ImporterDto,
SourceData,
} from "@/app/api/importer/[slug]/ImporterDto";
import { useGetImporter } from "@/components/hooks/useGetImporter";
import { useGetPatches } from "@/components/hooks/useGetPatches";
import { useGetSourceData } from "@/components/hooks/useGetSourceData";
import { useGetValidations } from "@/components/hooks/useGetValidations";
import { Button } from "@/components/ui/button";
import { ChevronRightCircleIcon } from "lucide-react";
import ValidationTable from "./ValidationTable";

type Props = {
initialImporterDto: ImporterDto;
initialSourceData: SourceData[];
initialPatches: DataSetPatch[];
initialValidation: DataValidation[];
};

const Validation = ({ initialImporterDto }: Props) => {
const Validation = ({
initialImporterDto,
initialSourceData,
initialPatches,
initialValidation,
}: Props) => {
const { importer } = useGetImporter(
initialImporterDto.importerId,
undefined,
initialImporterDto
);
const { data: sourceData } = useGetSourceData(
initialImporterDto.importerId,
initialSourceData
);
const { patches } = useGetPatches(
initialImporterDto.importerId,
undefined,
initialPatches
);
const { validations } = useGetValidations(
initialImporterDto.importerId,
undefined,
initialValidation
);
return (
<div>
<h1 className="text-3xl font-bold mb-4">Validate your data</h1>
<div className="mb-4">
<ValidationTable importerDto={initialImporterDto} />
<ValidationTable
importerDto={importer}
data={sourceData}
validations={validations}
patches={patches}
/>
</div>
<div className="flex justify-end">
<Button>
Expand Down
Loading
Loading