Skip to content
Snippets Groups Projects
Commit 0b2a07be824c authored by Arnaud Vergnet's avatar Arnaud Vergnet :sun_with_face:
Browse files

feat(frontend): allow creating and fetching recipes

parent dfa2a1d7ca78
No related branches found
No related tags found
1 merge request!14feat(frontend): finish connecting to backend
import { useClient } from "@/context/ClientContext";
import { DataService, Project } from "@/types";
import { DataService, Project, Recipe } from "@/types";
import { Transaction } from "@cubicweb/client";
import { useRouter } from "next/navigation";
export function useApiLogin() {
......@@ -33,7 +34,12 @@
const client = useClient();
const rql =
"INSERT ImportProcedure X: X name %(name)s, X sparql_endpoint %(sparql_endpoint)s, X activated %(activated)s";
return (data: Omit<Project, "eid">) => client.execute(rql, data);
return (data: Omit<Project, "import_recipes">) =>
client.execute(rql, {
name: data.name,
sparql_endpoint: data.sparql_endpoint,
activated: data.activated,
});
}
export function useApiUpdateProject() {
......@@ -41,7 +47,13 @@
const rql =
"SET X name %(name)s, X sparql_endpoint %(sparql_endpoint)s, X activated %(activated)s" +
"WHERE X is ImportProcedure, X eid %(eid)s";
return (data: Project) => client.execute(rql, data);
return (data: Omit<Project, "import_recipes">) =>
client.execute(rql, {
eid: data.eid,
name: data.name,
sparql_endpoint: data.sparql_endpoint,
activated: data.activated,
});
}
export function useApiUpdateProjectState() {
......@@ -75,5 +87,32 @@
}
}
export function useApiCreateRecipe() {
const client = useClient();
const insertRql =
"INSERT ImportRecipe X: X name %(name)s, X process_type %(process_type)s";
const setDataServiceRql =
"SET X dataservice %(dataservice)s WHERE X eid %(eid)s";
const setProjectRql =
"SET X import_recipes %(import_recipes)s WHERE X eid %(project_eid)s";
return (projectEid: number, data: Omit<Recipe, "eid">) => {
const transaction = new Transaction();
const insertQuery = transaction.push(insertRql, {
name: data.name,
process_type: data.process_type,
});
const eidRef = insertQuery.ref().row(0).column(0);
transaction.push(setDataServiceRql, {
dataservice: data.dataservice,
eid: eidRef,
});
transaction.push(setProjectRql, {
import_recipes: eidRef,
project_eid: projectEid,
});
return client.executeTransaction(transaction);
};
}
export function useApiGetProject(): (eid: number) => Promise<Project> {
const client = useClient();
......@@ -78,6 +117,6 @@
export function useApiGetProject(): (eid: number) => Promise<Project> {
const client = useClient();
const rql =
const projectRql =
"Any X, ATTR_NAME, ATTR_SPARQL_ENDPOINT, ATTR_ACTIVATED " +
"WHERE X is ImportProcedure, X eid %(eid)s, X name ATTR_NAME, X sparql_endpoint ATTR_SPARQL_ENDPOINT, X activated ATTR_ACTIVATED";
......@@ -81,5 +120,10 @@
"Any X, ATTR_NAME, ATTR_SPARQL_ENDPOINT, ATTR_ACTIVATED " +
"WHERE X is ImportProcedure, X eid %(eid)s, X name ATTR_NAME, X sparql_endpoint ATTR_SPARQL_ENDPOINT, X activated ATTR_ACTIVATED";
const recipeListRql =
"Any X, ATTR_NAME, ATTR_PROCESS_TYPE , REL_DATASERVICE " +
"GROUPBY X, ATTR_NAME, ATTR_PROCESS_TYPE, REL_DATASERVICE " +
"WHERE X is ImportRecipe, X name ATTR_NAME, X process_type ATTR_PROCESS_TYPE, X dataservice REL_DATASERVICE, O import_recipes X, O eid %(eid)s";
return async (eid: number) => {
try {
......@@ -84,6 +128,12 @@
return async (eid: number) => {
try {
const result = await client.execute(rql, { eid });
if (result.length === 0) {
const transaction = new Transaction();
const projectQuery = transaction.push(projectRql, { eid });
const recipeListQuery = transaction.push(recipeListRql, { eid });
const result = await client.executeTransaction(transaction);
const projectResult = result.resolveQuery(projectQuery.ref());
const recipeListResult = result.resolveQuery(recipeListQuery.ref());
if (projectResult.length === 0) {
throw new UnknownEidError(eid);
}
......@@ -88,8 +138,18 @@
throw new UnknownEidError(eid);
}
const r = result[0];
const recipeListJsonResult: Array<Recipe> = recipeListResult.map(
(r) =>
({
eid: r[0],
name: r[1],
process_type: r[2],
dataservice: r[3],
}) as Recipe,
);
const r = projectResult[0];
return {
eid: r[0],
name: r[1],
sparql_endpoint: r[2],
activated: r[3],
......@@ -91,8 +151,9 @@
return {
eid: r[0],
name: r[1],
sparql_endpoint: r[2],
activated: r[3],
import_recipes: recipeListJsonResult,
} as Project;
} catch (e) {
if (e && typeof e === "object" && "title" in e) {
......
......@@ -8,7 +8,7 @@
import { ProjectForm } from "@/components/ProjectForm";
import { CardList } from "@/components/cards/CardList";
import { RecipeCard } from "@/components/cards/RecetteCard";
import { IMPORT_PROCESSES, RECIPES } from "@/constants/constants";
import { IMPORT_PROCESSES } from "@/constants/constants";
import {
Box,
Button,
......@@ -29,7 +29,7 @@
}) {
const parsedEid = parseInt(eid);
const { data, loading, error } = useGetProject(parsedEid);
const { data, loading, error, refresh } = useGetProject(parsedEid);
const [selectedRecetteEid, setSelectedRecetteEid] = useState<
number | undefined
>();
......@@ -55,6 +55,7 @@
if (!data) {
return <ErrorScreen />;
}
return (
<Container>
<BreadcrumbsMenu page="Projet" />
......@@ -85,7 +86,7 @@
</Box>
</Stack>
<CardList loading={false} emptyText="Aucune recette">
{RECIPES.map((d, i) => (
{data.import_recipes?.map((d, i) => (
<RecipeCard
key={i}
name={d.name}
......@@ -122,5 +123,6 @@
}}
/>
<RecipeModal
projectEid={parsedEid}
open={recipeModalOpen}
onClose={() => setRecipeModalOpen(false)}
......@@ -125,5 +127,6 @@
open={recipeModalOpen}
onClose={() => setRecipeModalOpen(false)}
onSuccess={refresh}
/>
</Container>
);
......
import { DATA_SERVICES } from "@/constants/constants";
import { DataService } from "@/types";
import { useApiCreateRecipe, useApiGetDataServiceList } from "@/api/cubicweb";
import { DataService, Recipe } from "@/types";
import { LoadingButton } from "@mui/lab";
import {
Autocomplete,
Button,
......@@ -11,5 +12,6 @@
TextField,
} from "@mui/material";
import { useEffect, useState } from "react";
import { Controller, SubmitHandler, useForm } from "react-hook-form";
interface RecipeModalProps {
......@@ -14,4 +16,5 @@
interface RecipeModalProps {
projectEid: number;
open: boolean;
onClose: () => void;
......@@ -16,4 +19,5 @@
open: boolean;
onClose: () => void;
onSuccess: () => void;
}
......@@ -18,6 +22,11 @@
}
export function RecipeModal({ open, onClose }: RecipeModalProps) {
export function RecipeModal({
projectEid,
open,
onClose,
onSuccess,
}: RecipeModalProps) {
const [data, setData] = useState<Array<DataService> | undefined>();
const [loading, setLoading] = useState<boolean>(true);
const [saving, setSaving] = useState(false);
......@@ -21,9 +30,18 @@
const [data, setData] = useState<Array<DataService> | undefined>();
const [loading, setLoading] = useState<boolean>(true);
const [saving, setSaving] = useState(false);
const getDataServiceList = useApiGetDataServiceList();
const createRecipe = useApiCreateRecipe();
const { handleSubmit, control, reset } = useForm<Recipe>({
defaultValues: {
name: "",
process_type: "default",
},
});
useEffect(() => {
if (open) {
setLoading(true);
setTimeout(() => {
setLoading(false);
......@@ -24,9 +42,9 @@
useEffect(() => {
if (open) {
setLoading(true);
setTimeout(() => {
setLoading(false);
setData(DATA_SERVICES);
getDataServiceList().then((d) => setData(d));
}, 1000);
} else {
......@@ -31,4 +49,5 @@
}, 1000);
} else {
reset();
setData(undefined);
}
......@@ -33,4 +52,4 @@
setData(undefined);
}
}, [open]);
}, [open, reset]);
......@@ -36,6 +55,8 @@
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
// TODO send data
const onSubmit: SubmitHandler<Recipe> = async (data) => {
setSaving(true);
console.log(data);
await createRecipe(projectEid, data);
onSuccess();
onClose();
setSaving(false);
......@@ -40,6 +61,6 @@
onClose();
setSaving(false);
}
};
return (
<Dialog
......@@ -51,7 +72,7 @@
}}
PaperProps={{
component: "form",
onSubmit: onSubmit,
onSubmit: handleSubmit(onSubmit),
}}
aria-labelledby="form-dialog-title"
aria-describedby="form-dialog-description"
......@@ -60,9 +81,18 @@
<DialogTitle id="form-dialog-title">Création recette</DialogTitle>
<DialogContent>
<Stack paddingTop={1} spacing={2}>
<Autocomplete
options={data ?? []}
loading={loading}
renderInput={(params) => (
<TextField {...params} label="Data service" />
<Controller
name="name"
control={control}
rules={{ required: true }}
render={({ field, fieldState: { error } }) => (
<TextField
label="Nom"
disabled={saving}
{...field}
error={error !== undefined}
helperText={
error?.type === "required" ? "Champ requis" : error?.message
}
/>
)}
......@@ -68,3 +98,2 @@
)}
getOptionLabel={(option) => option.name}
/>
......@@ -70,10 +99,62 @@
/>
<Autocomplete
options={[{ label: "default" }, { label: "default-dry-run" }]}
renderInput={(params) => (
<TextField {...params} label="Process type" />
<Controller
name="dataservice"
control={control}
rules={{ required: true }}
render={({ field, fieldState: { error } }) => (
<Autocomplete
disabled={saving}
{...field}
inputValue={data?.find((d) => d.eid === field.value)?.name}
onChange={(_, newValue) => field.onChange(newValue)}
options={data ? data.map((d) => d.eid) : []}
loading={loading}
renderInput={(params) => (
<TextField
{...params}
label="Data service"
error={error !== undefined}
helperText={
error?.type === "required"
? "Champ requis"
: error?.message
}
/>
)}
getOptionLabel={
data
? (option) => data.find((d) => d.eid === option)?.name ?? ""
: () => ""
}
/>
)}
/>
<Controller
name="process_type"
control={control}
rules={{ required: true }}
render={({ field, fieldState: { error } }) => (
<Autocomplete
disabled={saving}
{...field}
inputValue={field.value}
onChange={(_, newValue) => field.onChange(newValue)}
options={["default", "default-dryrun"]}
renderInput={(params) => (
<TextField
{...params}
label="Process type"
error={error !== undefined}
helperText={
error?.type === "required"
? "Champ requis"
: error?.message
}
/>
)}
/>
)}
/>
</Stack>
</DialogContent>
<DialogActions>
......@@ -75,10 +156,14 @@
)}
/>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Annuler</Button>
<Button type="submit">Valider</Button>
<Button onClick={onClose} disabled={saving}>
Annuler
</Button>
<LoadingButton type="submit" loading={saving}>
Valider
</LoadingButton>
</DialogActions>
</Dialog>
);
......
import { UnknownEidError, useApiGetProject } from "@/api/cubicweb";
import { Project } from "@/types";
import { useState, useEffect } from "react";
import { useState, useEffect, useMemo } from "react";
import { useHandleAuthErrors } from "./useHandleAuthErrors";
export function useGetProject(eid: number) {
......@@ -10,4 +10,26 @@
const handleAuthErrors = useHandleAuthErrors();
const getProject = useApiGetProject();
const refresh = useMemo(() => {
return () => {
setLoading(true);
setError(undefined);
if (!isNaN(eid)) {
getProject(eid)
.then((result) => {
setLoading(false);
setData(result);
})
.catch(handleAuthErrors)
.catch((e) => {
if (e instanceof UnknownEidError) {
setError(`Impossible de trouver le projet '${eid}'`);
} else {
setError(`Une erreur inconnue est survenue`);
}
});
}
};
}, [eid, handleAuthErrors]);
useEffect(() => {
......@@ -13,21 +35,5 @@
useEffect(() => {
setLoading(true);
setError(undefined);
if (!isNaN(eid)) {
getProject(eid)
.then((result) => {
setLoading(false);
setData(result);
})
.catch(handleAuthErrors)
.catch((e) => {
if (e instanceof UnknownEidError) {
setError(`Impossible de trouver le projet '${eid}'`);
} else {
setError(`Une erreur inconnue est survenue`);
}
});
}
}, [eid, handleAuthErrors]);
return { loading, data, error };
refresh();
}, [refresh]);
return { loading, data, error, refresh };
}
......@@ -3,6 +3,7 @@
name: string;
sparql_endpoint: string;
activated: boolean;
import_recipes?: Recipe[];
};
export type DataService = {
......@@ -16,5 +17,6 @@
export type Recipe = {
eid: number;
name: string;
dataservice: number;
process_type: "default" | "default-dryrun";
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment