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

feat(products): improve products page #722

Merged
merged 11 commits into from
Oct 2, 2022
107 changes: 107 additions & 0 deletions components/CreateDatasetStorageSubscription.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useQueryClient } from "react-query";

import type { AsError, UnitDetail } from "@squonk/account-server-client";
import {
getGetProductsQueryKey,
useCreateUnitProduct,
} from "@squonk/account-server-client/product";

import { Box, Button } from "@mui/material";
import { captureException } from "@sentry/nextjs";
import { Field, Form, Formik } from "formik";
import { TextField } from "formik-mui";
import * as yup from "yup";

import { useEnqueueError } from "../hooks/useEnqueueStackError";
import { useGetStorageCost } from "../hooks/useGetStorageCost";
import { coinsFormatter } from "../utils/app/coins";
import { getBillingDay } from "../utils/app/products";
import { getErrorMessage } from "../utils/next/orvalError";

export interface CreateDatasetStorageSubscriptionProps {
unit: UnitDetail;
}

const initialValues = {
allowance: 1000,
billingDay: getBillingDay(),
name: "Dataset Storage",
};

export const CreateDatasetStorageSubscription = ({
unit,
}: CreateDatasetStorageSubscriptionProps) => {
const { mutateAsync: createProduct } = useCreateUnitProduct();
const { enqueueError, enqueueSnackbar } = useEnqueueError<AsError>();
const queryClient = useQueryClient();
const cost = useGetStorageCost();
return (
<Formik
initialValues={initialValues}
validationSchema={yup.object().shape({
name: yup.string().trim().required("A name is required"),
limit: yup.number().min(1).integer().required("A limit is required"),
allowance: yup.number().min(1).integer().required("An allowance is required"),
billingDay: yup.number().min(1).max(28).integer().required("A billing day is required"),
})}
onSubmit={async ({ allowance, billingDay, name }) => {
try {
await createProduct({
unitId: unit.id,
data: {
allowance,
billing_day: billingDay,
limit: allowance, // TODO: we will implement this properly later
name,
type: "DATA_MANAGER_STORAGE_SUBSCRIPTION",
},
});
enqueueSnackbar("Created product", { variant: "success" });
queryClient.invalidateQueries(getGetProductsQueryKey());
} catch (error) {
enqueueError(getErrorMessage(error));
captureException(error);
}
}}
>
{({ submitForm, isSubmitting, isValid, values }) => {
return (
<Form>
<Box alignItems="baseline" display="flex" flexWrap="wrap" gap={2}>
<Field
autoFocus
component={TextField}
label="Name"
name="name"
sx={{ maxWidth: 150 }}
/>
<Field
component={TextField}
label="Billing Day"
max={28}
min={1}
name="billingDay"
sx={{ maxWidth: 80 }}
type="number"
/>
<Field
component={TextField}
label="Allowance"
min={1}
name="allowance"
sx={{ maxWidth: 100 }}
type="number"
/>
{cost && (
<span>Cost: {coinsFormatter.format(cost * values.allowance).slice(1)}C</span>
)}
<Button disabled={isSubmitting || !isValid} onClick={submitForm}>
Create
</Button>
</Box>
</Form>
);
}}
</Formik>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { HTMLProps } from "react";

import { toLocalTimeString } from "./utils";
import { toLocalTimeString } from "../utils/app/datetime";

export interface BaseLocalTimeProps extends HTMLProps<HTMLSpanElement> {
/**
Expand Down
2 changes: 0 additions & 2 deletions components/LocalTime/index.ts

This file was deleted.

2 changes: 1 addition & 1 deletion components/labels/NewLabelButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const NewLabelButton = ({ datasetId }: NewLabelButtonProps) => {
>
{({ submitForm, isSubmitting, isValid }) => (
<Form>
<Box alignItems="baseline" display="flex" gap={(theme) => theme.spacing(1)}>
<Box alignItems="baseline" display="flex" gap={1}>
<Field autoFocus component={LowerCaseTextField} label="Name" name="label" />
<Field component={TextField} label="Value" name="value" />
<Button disabled={isSubmitting || !isValid} onClick={submitForm}>
Expand Down
84 changes: 84 additions & 0 deletions components/products/AdjustProjectProduct.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useState } from "react";
import { useQueryClient } from "react-query";

import type { ProductDetail } from "@squonk/account-server-client";
import {
getGetProductQueryKey,
getGetProductsQueryKey,
usePatchProduct,
} from "@squonk/account-server-client/product";

import EditIcon from "@mui/icons-material/Edit";
import { Box, IconButton } from "@mui/material";
import { Field } from "formik";
import { TextField } from "formik-mui";

import { useEnqueueError } from "../../hooks/useEnqueueStackError";
import { useGetStorageCost } from "../../hooks/useGetStorageCost";
import { coinsFormatter } from "../../utils/app/coins";
import { getErrorMessage } from "../../utils/next/orvalError";
import { FormikModalWrapper } from "../modals/FormikModalWrapper";

export interface AdjustProjectProductProps {
product: ProductDetail;
allowance: number;
}

export const AdjustProjectProduct = ({ product, allowance }: AdjustProjectProductProps) => {
const [open, setOpen] = useState(false);
const cost = useGetStorageCost();
const { mutateAsync: adjustProduct } = usePatchProduct();
const { enqueueError, enqueueSnackbar } = useEnqueueError();
const queryClient = useQueryClient();

const initialValues = { name: product.name, allowance };

return (
<>
<IconButton onClick={() => setOpen(true)}>
<EditIcon />
</IconButton>
<FormikModalWrapper
id={`adjust-${product.id}`}
initialValues={initialValues}
open={open}
submitText="Submit"
title={`Adjust Product - ${product.name}`}
onClose={() => setOpen(false)}
onSubmit={async (values) => {
try {
await adjustProduct({ productId: product.id, data: values });
await Promise.allSettled([
queryClient.invalidateQueries(getGetProductsQueryKey()),
queryClient.invalidateQueries(getGetProductQueryKey(product.id)),
]);
enqueueSnackbar("Updated product", { variant: "success" });
} catch (error) {
enqueueError(getErrorMessage(error));
}
}}
>
{({ values }) => (
<Box alignItems="baseline" display="flex" flexWrap="wrap" gap={2} m={2}>
<Field
autoFocus
component={TextField}
label="Name"
name="name"
sx={{ maxWidth: 150 }}
/>
<Field
component={TextField}
label="Allowance"
min={1}
name="allowance"
sx={{ maxWidth: 100 }}
type="number"
/>
{cost && <span>Cost: {coinsFormatter.format(cost * values.allowance).slice(1)}C</span>}
</Box>
)}
</FormikModalWrapper>
</>
);
};
59 changes: 59 additions & 0 deletions components/products/DeleteProductButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useQueryClient } from "react-query";

import type { ProductDetail } from "@squonk/account-server-client";
import {
getGetProductQueryKey,
getGetProductsQueryKey,
useDeleteProduct,
} from "@squonk/account-server-client/product";

import DeleteForeverIcon from "@mui/icons-material/DeleteForever";
import { IconButton, Tooltip } from "@mui/material";

import { useEnqueueError } from "../../hooks/useEnqueueStackError";
import { getErrorMessage } from "../../utils/next/orvalError";
import { WarningDeleteButton } from "../WarningDeleteButton";

export interface DeleteProductButtonProps {
product: ProductDetail;
disabled?: boolean;
tooltip: string;
}

export const DeleteProductButton = ({
product,
disabled = false,
tooltip,
}: DeleteProductButtonProps) => {
const { mutateAsync: deleteProduct } = useDeleteProduct();
const { enqueueError, enqueueSnackbar } = useEnqueueError();
const queryClient = useQueryClient();
return (
<Tooltip title={tooltip}>
<span>
<WarningDeleteButton
modalId={`delete-${product.id}`}
title="Delete Product"
onDelete={async () => {
try {
await deleteProduct({ productId: product.id });
await Promise.allSettled([
queryClient.invalidateQueries(getGetProductsQueryKey()),
queryClient.invalidateQueries(getGetProductQueryKey(product.id)),
]);
enqueueSnackbar("Product deleted", { variant: "success" });
} catch (error) {
enqueueError(getErrorMessage(error));
}
}}
>
{({ openModal }) => (
<IconButton disabled={disabled} onClick={openModal}>
<DeleteForeverIcon />
</IconButton>
)}
</WarningDeleteButton>
</span>
</Tooltip>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import ShareIcon from "@mui/icons-material/Share";
import { IconButton, Tooltip } from "@mui/material";
import { useSnackbar } from "notistack";

import { projectURL } from "../../utils/app/routes";

export interface CopyProjectURLProps {
/**
* Project to copy the permalink to the clipboard
Expand All @@ -20,12 +22,7 @@ export const CopyProjectURL = ({ project }: CopyProjectURLProps) => {
sx={{ p: "1px" }}
onClick={async () => {
project.project_id &&
(await navigator.clipboard.writeText(
window.location.origin +
(process.env.NEXT_PUBLIC_BASE_PATH ?? "") +
"/project?" +
new URLSearchParams([["project", project.project_id]]).toString(),
));
(await navigator.clipboard.writeText(projectURL(project.project_id)));
enqueueSnackbar("Copied URL to clipboard", { variant: "info" });
}}
>
Expand Down
4 changes: 3 additions & 1 deletion components/projects/CreateProjectButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ export const CreateProjectButton = ({

return (
<>
<Button onClick={() => setOpen(true)}>{buttonText}</Button>
<Button variant="outlined" onClick={() => setOpen(true)}>
{buttonText}
</Button>

<CreateProjectForm
modal={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import type { ProjectDetail } from "@squonk/data-manager-client";
import EditIcon from "@mui/icons-material/Edit";
import { IconButton, Tooltip, Typography } from "@mui/material";

import { ModalWrapper } from "../../../../components/modals/ModalWrapper";
import { useKeycloakUser } from "../../../../hooks/useKeycloakUser";
import { useKeycloakUser } from "../../../hooks/useKeycloakUser";
import { ModalWrapper } from "../../modals/ModalWrapper";
import { PrivateProjectToggle } from "./PrivateProjectToggle";
import { ProjectEditors } from "./ProjectEditors";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {

import { FormControlLabel, Switch } from "@mui/material";

import type { ProjectId } from "../../../../hooks/projectHooks";
import { useEnqueueError } from "../../../../hooks/useEnqueueStackError";
import { getErrorMessage } from "../../../../utils/next/orvalError";
import type { ProjectId } from "../../../hooks/projectHooks";
import { useEnqueueError } from "../../../hooks/useEnqueueStackError";
import { getErrorMessage } from "../../../utils/next/orvalError";

export interface PrivateProjectToggleProps {
projectId: ProjectId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
useRemoveEditorFromProject,
} from "@squonk/data-manager-client/project";

import { ManageEditors } from "../../../../components/ManageEditors";
import { useEnqueueError } from "../../../../hooks/useEnqueueStackError";
import { useKeycloakUser } from "../../../../hooks/useKeycloakUser";
import { useEnqueueError } from "../../../hooks/useEnqueueStackError";
import { useKeycloakUser } from "../../../hooks/useKeycloakUser";
import { ManageEditors } from "../../ManageEditors";

export interface ProjectEditorsProps {
/**
Expand Down
18 changes: 18 additions & 0 deletions components/projects/OpenProjectButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import OpenInBrowserIcon from "@mui/icons-material/OpenInBrowser";
import { IconButton, Tooltip } from "@mui/material";

import { projectURL } from "../../utils/app/routes";

export interface OpenProjectButtonProps {
projectId: string;
}

export const OpenProjectButton = ({ projectId }: OpenProjectButtonProps) => {
return (
<Tooltip title="Open project in a new tab">
<IconButton href={projectURL(projectId)} size="small" sx={{ p: "1px" }} target="_blank">
<OpenInBrowserIcon />
</IconButton>
</Tooltip>
);
};
12 changes: 6 additions & 6 deletions components/projects/ProjectSelection.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { Grid } from "@mui/material";

import { OrganisationAutocomplete } from "../userContext/OrganisationAutocomplete";
import { ProjectAutocomplete } from "../userContext/ProjectAutocomplete";
import { UnitAutocomplete } from "../userContext/UnitAutocomplete";
import { SelectOrganisation } from "../userContext/SelectOrganisation";
import { SelectProject } from "../userContext/SelectProject";
import { SelectUnit } from "../userContext/SelectUnit";

export const ProjectSelection = () => {
return (
<Grid container spacing={1}>
<Grid container item alignItems="center" sm={6}>
<OrganisationAutocomplete />
<SelectOrganisation />
</Grid>
<Grid container item alignItems="center" sm={6}>
<UnitAutocomplete />
<SelectUnit />
</Grid>
<Grid container item alignItems="center" sm={12}>
<ProjectAutocomplete size="medium" />
<SelectProject size="medium" />
</Grid>
</Grid>
);
Expand Down
2 changes: 1 addition & 1 deletion components/results/DateTimeListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import utc from "dayjs/plugin/utc";

dayjs.extend(utc);

import { DATE_FORMAT, TIME_FORMAT } from "../LocalTime/utils";
import { DATE_FORMAT, TIME_FORMAT } from "../../utils/app/datetime";

export interface DateTimeListItemProps {
/**
Expand Down
Loading