diff --git a/example-embedded-app/package-lock.json b/example-embedded-app/package-lock.json index b674697..6b698c3 100644 --- a/example-embedded-app/package-lock.json +++ b/example-embedded-app/package-lock.json @@ -23,8 +23,10 @@ "context-api": "0.0.2", "date-fns": "2.30.0", "jsonwebtoken": "^9.0.0", + "ka-table": "^11.0.3", "next": "12.3.4", "nprogress": "0.2.0", + "papaparse": "^5.4.1", "react": "17.0.2", "react-apexcharts": "1.4.0", "react-custom-scrollbars-2": "4.5.0", @@ -4260,6 +4262,14 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/ka-table": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/ka-table/-/ka-table-11.0.3.tgz", + "integrity": "sha512-nrauBKNsD3b5uQEQEagFNvAWieBSSXgGJNwW+WO/foaEpGd5RiIB+Si2hoBAV4uB6lhBxANnU59wydyoYi7ALQ==", + "peerDependencies": { + "react": "^16.8.3 || ^17.0.0-0 || ^18.0.0-0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5267,6 +5277,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", diff --git a/example-embedded-app/package.json b/example-embedded-app/package.json index 89d9ccb..9df9d93 100644 --- a/example-embedded-app/package.json +++ b/example-embedded-app/package.json @@ -23,8 +23,10 @@ "context-api": "0.0.2", "date-fns": "2.30.0", "jsonwebtoken": "^9.0.0", + "ka-table": "^11.0.3", "next": "12.3.4", "nprogress": "0.2.0", + "papaparse": "^5.4.1", "react": "17.0.2", "react-apexcharts": "1.4.0", "react-custom-scrollbars-2": "4.5.0", diff --git a/example-embedded-app/pages/csv-example/CsvDataTable.tsx b/example-embedded-app/pages/csv-example/CsvDataTable.tsx new file mode 100644 index 0000000..c7a0295 --- /dev/null +++ b/example-embedded-app/pages/csv-example/CsvDataTable.tsx @@ -0,0 +1,30 @@ +import { Table } from "ka-table"; +import { DataType, EditingMode, SortingMode } from "ka-table/enums"; +import "ka-table/style.css"; + +function CsvDataTable({ data, table }) { + if (data === null || data.length === 0) { + return null; + } + + const columns = Object.keys(data[0]).map((key) => ({ + key, + title: key, + dataType: DataType.String, + })); + + const rows = data.map((row, index) => ({ ...row, id: index })); + + return ( + + ); +} + +export default CsvDataTable; diff --git a/example-embedded-app/pages/csv-example/PrismaticAvatar.tsx b/example-embedded-app/pages/csv-example/PrismaticAvatar.tsx new file mode 100644 index 0000000..31c2916 --- /dev/null +++ b/example-embedded-app/pages/csv-example/PrismaticAvatar.tsx @@ -0,0 +1,39 @@ +import { CableTwoTone } from "@mui/icons-material"; +import { Avatar } from "@mui/material"; +import config from "prismatic/config"; +import React from "react"; + +function PrismaticAvatar({ avatarUrl, token }) { + const [src, setSrc] = React.useState(""); + + React.useEffect(() => { + let mounted = true; + if (avatarUrl) { + fetch(`${config.prismaticUrl}${avatarUrl}`, { + headers: { Authorization: `Bearer ${token}` }, + }).then((response) => { + response.json().then((data) => { + if (mounted) { + setSrc(data.url); + } + }); + }); + } + + return () => { + mounted = false; + }; + }, []); + + if (!avatarUrl) { + return ( + + + + ); + } + + return src ? : null; +} + +export default PrismaticAvatar; diff --git a/example-embedded-app/pages/csv-example/UploadButtons.tsx b/example-embedded-app/pages/csv-example/UploadButtons.tsx new file mode 100644 index 0000000..7575ca6 --- /dev/null +++ b/example-embedded-app/pages/csv-example/UploadButtons.tsx @@ -0,0 +1,99 @@ +import { + Alert, + Box, + Button, + Container, + LinearProgress, + TextField, +} from "@mui/material"; +import Papa from "papaparse"; +import React, { Dispatch, SetStateAction } from "react"; +import { Instance } from "./getInstances"; + +interface UploadCsvParams { + fileName: string; + uploadUrl: string; + data: unknown[]; + setUploadState: Dispatch>; +} + +function uploadCsv({ + fileName, + uploadUrl, + data, + setUploadState, +}: UploadCsvParams) { + setUploadState("uploading"); + const csvData = Papa.unparse(data.map(({ id, ...rest }) => rest)); + const formData = new FormData(); + formData.append("file", csvData); + formData.append("fileName", fileName); + fetch(uploadUrl, { method: "post", body: formData }) + .then(() => { + setUploadState("success"); + setTimeout(() => setUploadState("idle"), 4000); + }) + .catch(() => { + setUploadState("failed"); + }); +} + +type UploadState = "idle" | "uploading" | "success" | "failed"; + +function ProgressIndicator({ state }: { state: UploadState }) { + switch (state) { + case "idle": + return null; + case "uploading": + return ( + + + + ); + case "success": + return Upload successful; + case "failed": + return Upload failed; + } +} + +function UploadButtons({ + data, + instances, +}: { + data: any; + instances: Instance[]; +}) { + const [fileName, setFileName] = React.useState(""); + const [uploadState, setUploadState] = React.useState("idle"); + + return ( + +
+ { + setFileName(target.value); + }} + label="File Name" + /> + {instances.map((instance) => ( + + ))} + +
+ ); +} + +export default UploadButtons; diff --git a/example-embedded-app/pages/csv-example/csv-example.md b/example-embedded-app/pages/csv-example/csv-example.md new file mode 100644 index 0000000..cc42667 --- /dev/null +++ b/example-embedded-app/pages/csv-example/csv-example.md @@ -0,0 +1,3 @@ +## CSV Load and Upload Example + +This example demonstrates interactivity between an application and a set of Prismatic instances that a customer has deployed. diff --git a/example-embedded-app/pages/csv-example/getInstances.ts b/example-embedded-app/pages/csv-example/getInstances.ts new file mode 100644 index 0000000..cfcb786 --- /dev/null +++ b/example-embedded-app/pages/csv-example/getInstances.ts @@ -0,0 +1,78 @@ +import prismatic from "@prismatic-io/embedded"; + +export interface Instance { + id: string; + enabled: boolean; + flowConfigs: FlowConfigs; + integration: Integration; + webhookUrls: Record; +} + +export interface FlowConfigs { + nodes: FlowConfigsNode[]; +} + +export interface FlowConfigsNode { + flow: Flow; + webhookUrl: string; +} + +export interface Flow { + name: string; +} + +export interface Integration { + id: string; + name: string; + avatarUrl: null | string; + category: Category; +} + +export enum Category { + CSVStores = "CSV Stores", + Communication = "Communication", + Empty = "", +} +const query = `query getInstances { + instances { + nodes { + id + enabled + flowConfigs { + nodes { + flow { + name + } + webhookUrl + } + } + integration { + id + name + avatarUrl + category + } + } + } +} +`; + +const getInstances = (): Promise => { + return prismatic.graphqlRequest({ query }).then((response) => { + const csvInstances = (response.data.instances.nodes as Instance[]).filter( + (instance) => instance.integration.category === "CSV Stores", + ); + // Make webhook URLs more accessible + for (const instance of csvInstances) { + instance.webhookUrls = Object.fromEntries( + instance.flowConfigs.nodes.map((flowConfig) => [ + flowConfig.flow.name, + flowConfig.webhookUrl, + ]), + ); + } + return csvInstances; + }); +}; + +export default getInstances; diff --git a/example-embedded-app/pages/csv-example/index.tsx b/example-embedded-app/pages/csv-example/index.tsx new file mode 100644 index 0000000..e49fea1 --- /dev/null +++ b/example-embedded-app/pages/csv-example/index.tsx @@ -0,0 +1,163 @@ +import Head from "next/head"; + +import Footer from "@/components/Footer"; +import SidebarLayout from "@/layouts/SidebarLayout"; +import usePrismaticAuth from "@/usePrismaticAuth"; +import { + Button, + CardHeader, + Container, + LinearProgress, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from "@mui/material"; +import Papa from "papaparse"; +import React from "react"; +import getInstances, { Instance } from "./getInstances"; +import PrismaticAvatar from "./PrismaticAvatar"; +import CsvDataTable from "./CsvDataTable"; +import { useTable } from "ka-table"; +import UploadButtons from "./UploadButtons"; +import PageTitleWrapper from "@/components/PageTitleWrapper"; + +import csvExampleHelperText from "./csv-example.md"; +import ExampleHeader from "@/components/ExampleHeader"; + +function CsvExample() { + const { authenticated, token } = usePrismaticAuth(); + const [instances, setInstances] = React.useState(); + const [csvFiles, dispatchCsvFiles] = React.useReducer((state, files) => { + return [...state, ...files]; + }, []); + const [csvData, setCsvData] = React.useState([]); + const [showingTable, setShowingTable] = React.useState(false); + const csvDataTable = useTable({ + onDispatch: (_, tableState) => setCsvData(tableState.data), + }); + + // Get enabled relevant instances + React.useEffect(() => { + if (authenticated) { + getInstances().then((r) => setInstances(r)); + } + }, [authenticated]); + + // Display CSV files from each integration: + React.useEffect(() => { + instances?.forEach((instance) => { + fetch(instance.webhookUrls["list"]).then((response) => { + response.json().then((data) => { + dispatchCsvFiles( + data + .filter( + (filename) => + !csvFiles.find( + (csvFile) => + csvFile.filename === filename && + csvFile.integrationName === instance.integration.name, + ), + ) + .map((filename) => ({ + filename, + downloadFlowUrl: instance.webhookUrls["download"], + avatarUrl: instance.integration.avatarUrl, + integrationName: instance.integration.name, + })), + ); + }); + }); + }); + }, [instances]); + + const loadFile = (filename, downloadUrl) => { + fetch(downloadUrl, { + method: "post", + body: JSON.stringify({ filename }), + headers: { "content-type": "application/json" }, + }).then((response) => { + response + .text() + .then((csvText) => + setCsvData( + Papa.parse(csvText.replace(/\n$/, ""), { header: true }).data, + ), + ); + setShowingTable(true); + }); + }; + + return ( + <> + + CSV Editor Example + + + + + + + {csvFiles.length > 0 ? ( + +
+ + + + Location + File + + + + {csvFiles.map((csvFile) => ( + + + + + + + } + title={csvFile.integrationName} + /> + + {csvFile.filename} + + ))} + +
+ + ) : ( + + )} +
+ {showingTable ? ( + + + + + ) : null} + +