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

[generic issuance] pipeline CRUD app with minimal UI #1451

Merged
merged 14 commits into from
Feb 1, 2024
3 changes: 3 additions & 0 deletions apps/generic-issuance-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
"clean": "rm -rf node_modules public/js tsconfig.tsbuildinfo"
},
"dependencies": {
"@pcd/passport-interface": "0.10.0",
"@stytch/react": "^15.0.0",
"@stytch/vanilla-js": "^4.4.2",
"axios": "^1.6.7",
"dotenv": "^16.4.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^9.1.0",
"react-router-dom": "^6.9.0",
"styled-components": "^5.3.6"
},
Expand Down
4 changes: 3 additions & 1 deletion apps/generic-issuance-client/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { createHashRouter, RouterProvider } from "react-router-dom";
import { GlobalStyle } from "./components/GlobalStyle";
import Dashboard from "./pages/Dashboard";
import Home from "./pages/Home";
import Pipeline from "./pages/Pipeline";

const stytch = new StytchUIClient(process.env.STYTCH_PUBLIC_TOKEN);

const router = createHashRouter([
{ path: "/", element: <Home /> },
{ path: "/dashboard", element: <Dashboard /> }
{ path: "/dashboard", element: <Dashboard /> },
{ path: "/pipelines/:id", element: <Pipeline /> }
]);

createRoot(document.getElementById("root") as HTMLElement).render(
Expand Down
100 changes: 86 additions & 14 deletions apps/generic-issuance-client/src/pages/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,63 @@
import { useStytch, useStytchUser } from "@stytch/react";
import { ReactNode, useEffect, useState } from "react";
import axios from "axios";
import { ReactNode, useCallback, useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { ZUPASS_SERVER_URL } from "../constants";

const SAMPLE_CREATE_PIPELINE_TEXT = JSON.stringify(
{
type: "Lemonade",
editorUserIds: [],
options: {
lemonadeApiKey: "your-lemonade-api-key",
events: []
}
},
null,
2
);

export default function Dashboard(): ReactNode {
const stytchClient = useStytch();
const { user } = useStytchUser();
const [isLoggingOut, setLoggingOut] = useState(false);
const [userPingMessage, setUserPingMessage] = useState("");
// TODO: After MVP, replace with RTK hooks or a more robust state management.
rrrliu marked this conversation as resolved.
Show resolved Hide resolved
const [pipelines, setPipelines] = useState([]);
const [isCreatingPipeline, setCreatingPipeline] = useState(false);
const [newPipelineRaw, setNewPipelineRaw] = useState(
SAMPLE_CREATE_PIPELINE_TEXT
);
const [error, setError] = useState("");

useEffect(() => {
setUserPingMessage("Pinging server...");
fetch(new URL("generic-issuance/api/user/ping", ZUPASS_SERVER_URL).href, {
const fetchAllPipelines = useCallback(() => {
fetch(new URL(`generic-issuance/api/pipelines`, ZUPASS_SERVER_URL).href, {
rrrliu marked this conversation as resolved.
Show resolved Hide resolved
credentials: "include"
})
.then((res) => res.json())
.then((message) => setUserPingMessage(`JWT valid, received ${message}.`))
.catch((e) => setUserPingMessage(`Error: ${e}`));
.then((data) => setPipelines(data))
.catch((e) => alert(e));
}, []);

useEffect(() => {
fetchAllPipelines();
}, [fetchAllPipelines]);

const createPipeline = async (): Promise<void> => {
if (!newPipelineRaw) return;
try {
await axios.put(
new URL("generic-issuance/api/pipelines", ZUPASS_SERVER_URL).href,
JSON.parse(newPipelineRaw),
{ withCredentials: true }
);
} catch (e) {
alert(e);
rrrliu marked this conversation as resolved.
Show resolved Hide resolved
}
await fetchAllPipelines();

setCreatingPipeline(false);
};

if (!user) {
window.location.href = "/";
}
Expand All @@ -36,20 +75,53 @@ export default function Dashboard(): ReactNode {
<p>
Congrats - you are now logged in as <b>{user.emails?.[0]?.email}.</b>
</p>
{userPingMessage && <p>{userPingMessage}</p>}
<button
onClick={async (): Promise<void> => {
setLoggingOut(true);
try {
await stytchClient.session.revoke();
} catch (e) {
setError(e);
setLoggingOut(false);
if (confirm("Are you sure you want to log out?")) {
setLoggingOut(true);
try {
await stytchClient.session.revoke();
} catch (e) {
setError(e);
setLoggingOut(false);
}
}
}}
>
Log out
</button>

<h2>My Pipelines</h2>
{!pipelines.length && <p>No pipelines right now - go create some!</p>}
{!!pipelines.length && (
<ol>
{pipelines.map((p) => (
<Link to={`/pipelines/${p.id}`}>
<li key={p.id}>
id: {p.id}, type: {p.type}
</li>
</Link>
))}
</ol>
)}
<p>
<button onClick={(): void => setCreatingPipeline((curr) => !curr)}>
{isCreatingPipeline ? "Minimize 🔼" : "Create new pipeline 🔽"}
</button>
{isCreatingPipeline && (
<div>
<textarea
rows={10}
cols={50}
value={newPipelineRaw}
onChange={(e): void => setNewPipelineRaw(e.target.value)}
/>
<div>
<button onClick={createPipeline}>Create new pipeline</button>
</div>
</div>
)}
</p>
</div>
);
}
119 changes: 119 additions & 0 deletions apps/generic-issuance-client/src/pages/Pipeline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { useStytchUser } from "@stytch/react";
import axios from "axios";
import { ReactNode, useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import { ZUPASS_SERVER_URL } from "../constants";

function format(obj: object): string {
return JSON.stringify(obj, null, 2);
}

export default function Pipeline(): ReactNode {
const params = useParams();
const { user } = useStytchUser();
const { id } = params;
// TODO: After MVP, replace with RTK hooks or a more robust state management.
const [savedPipeline, setSavedPipeline] = useState();
const [textareaValue, setTextareaValue] = useState("");
const [queryLoading, setQueryLoading] = useState(true);
const [saveLoading, setSaveLoading] = useState(false);

async function savePipeline(): Promise<void> {
setSaveLoading(true);
try {
const { data } = await axios.put(
new URL(`generic-issuance/api/pipelines`, ZUPASS_SERVER_URL).href,
JSON.parse(textareaValue),
{
withCredentials: true
}
);
setSavedPipeline(data);
setTextareaValue(format(data));
} catch (e) {
alert(`An error occured while saving: ${e}`);
} finally {
setSaveLoading(false);
}
}

async function deletePipeline(): Promise<void> {
if (confirm("Are you sure you would like to delete this pipeline?")) {
try {
await axios.delete(
new URL(`generic-issuance/api/pipelines/${id}`, ZUPASS_SERVER_URL)
.href,
{
withCredentials: true
}
);
window.location.href = "/";
} catch (e) {
alert(`An error occured while deleting: ${e}`);
}
}
}

useEffect(() => {
async function fetchPipeline(): Promise<void> {
const res = await fetch(
new URL(`generic-issuance/api/pipelines/${id}`, ZUPASS_SERVER_URL).href,
{
credentials: "include"
}
);
const data = await res.json();
setSavedPipeline(data);
setTextareaValue(format(data));
setQueryLoading(false);
}
fetchPipeline();
}, [id]);

if (!user) {
window.location.href = "/";
}

if (queryLoading) {
return <div>Loading...</div>;
}

if (!savedPipeline) {
return (
<div>
This pipeline id is invalid or you do not have access to this pipeline.
</div>
);
}

const hasEdits = format(savedPipeline) !== textareaValue;

return (
<div>
<p>
<textarea
cols={50}
rows={30}
value={textareaValue}
onChange={(e): void => setTextareaValue(e.target.value)}
/>
</p>
<p>
{hasEdits && (
<button disabled={saveLoading} onClick={savePipeline}>
{saveLoading ? "Saving..." : "Save changes"}
</button>
)}
{!hasEdits && <button disabled>All changes saved ✅</button>}
</p>
<p>
<button onClick={deletePipeline}>Delete pipeline</button>
</p>
<p>
<Link to="/dashboard">
<button>Return to all pipelines</button>
</Link>
</p>
</div>
);
}
19 changes: 16 additions & 3 deletions apps/generic-issuance-client/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,25 @@
"resolveJsonModule": true,
"downlevelIteration": true,
"jsx": "react-jsx",
"lib": ["ES2015", "DOM"],
"lib": [
"ES2015",
"DOM"
],
"esModuleInterop": true,
// To allow for mocha and jest to work together:
// https://stackoverflow.com/a/65568463
"skipLibCheck": true
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"include": [
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
],
"references": [
{
"path": "../../packages/lib/passport-interface"
}
]
}
2 changes: 2 additions & 0 deletions apps/passport-server/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as path from "path";
import urljoin from "url-join";
import { MockPipelineAtomDB } from "../test/generic-issuance/MockPipelineAtomDB";
import { MockPipelineDefinitionDB } from "../test/generic-issuance/MockPipelineDefinitionDB";
import { MockPipelineUserDB } from "../test/generic-issuance/MockPipelineUserDB";
import { getDevconnectPretixAPI } from "./apis/devconnect/devconnectPretixAPI";
import { IEmailAPI, sendgridSendEmail } from "./apis/emailAPI";
import { getHoneycombAPI } from "./apis/honeycombAPI";
Expand Down Expand Up @@ -52,6 +53,7 @@ export async function startApplication(
publicResourcesDir: path.join(process.cwd(), "public"),
gitCommitHash: await getCommitHash(),
// TODO: remove these once we have settled on a db schema for these
pipelineUserDB: new MockPipelineUserDB(),
pipelineAtomDB: new MockPipelineAtomDB(),
pipelineDefinitionDB: new MockPipelineDefinitionDB()
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { PipelineDefinition } from "../../services/generic-issuance/pipelines/ty
*/
export interface IPipelineDefinitionDB {
loadPipelineDefinitions(): Promise<PipelineDefinition[]>;
clearDefinition(definitionID: string): Promise<void>;
clearAllDefinitions(): Promise<void>;
getDefinition(definitionID: string): Promise<PipelineDefinition | undefined>;
setDefinition(definition: PipelineDefinition): Promise<void>;
Expand Down
Loading
Loading