Bridge integration

This commit is contained in:
Darren Clarke 2024-05-09 07:42:44 +02:00
parent 42a5e09c94
commit 162390008b
56 changed files with 776 additions and 591 deletions

View file

@ -1,4 +1,5 @@
export default function Page() { import { Home } from "bridge-ui";
return <h1>Home</h1>;
}
export default function Page() {
return <Home />;
}

View file

@ -17,7 +17,7 @@ import {
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import Image from "next/image"; import Image from "next/image";
import LinkLogo from "@/app/_images/link-logo-small.png"; import LinkLogo from "@/app/_images/link-logo-small.png";
import { colors } from "ui"; import { colors, fonts } from "ui";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
type LoginProps = { type LoginProps = {
@ -34,6 +34,7 @@ export const Login: FC<LoginProps> = ({ session }) => {
const params = useSearchParams(); const params = useSearchParams();
const error = params.get("error"); const error = params.get("error");
const { darkGray, cdrLinkOrange, white } = colors; const { darkGray, cdrLinkOrange, white } = colors;
const { poppins } = fonts;
const buttonStyles = { const buttonStyles = {
borderRadius: 500, borderRadius: 500,
width: "100%", width: "100%",
@ -102,7 +103,7 @@ export const Login: FC<LoginProps> = ({ session }) => {
fontWeight: 700, fontWeight: 700,
mt: 1, mt: 1,
ml: 0.5, ml: 0.5,
fontFamily: "Poppins", fontFamily: poppins.style.fontFamily,
}} }}
> >
CDR Bridge CDR Bridge

View file

@ -1,4 +1,3 @@
FROM node:20 AS base FROM node:20 AS base
FROM base AS builder FROM base AS builder

View file

@ -17,7 +17,7 @@ import {
import { signIn } from "next-auth/react"; import { signIn } from "next-auth/react";
import Image from "next/image"; import Image from "next/image";
import LinkLogo from "public/link-logo-small.png"; import LinkLogo from "public/link-logo-small.png";
import { colors } from "app/_styles/theme"; import { colors, fonts } from "ui";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
type LoginProps = { type LoginProps = {
@ -34,6 +34,7 @@ export const Login: FC<LoginProps> = ({ session }) => {
const params = useSearchParams(); const params = useSearchParams();
const error = params.get("error"); const error = params.get("error");
const { darkGray, cdrLinkOrange, white } = colors; const { darkGray, cdrLinkOrange, white } = colors;
const { poppins } = fonts;
const buttonStyles = { const buttonStyles = {
borderRadius: 500, borderRadius: 500,
width: "100%", width: "100%",
@ -102,7 +103,7 @@ export const Login: FC<LoginProps> = ({ session }) => {
fontWeight: 700, fontWeight: 700,
mt: 1, mt: 1,
ml: 0.5, ml: 0.5,
fontFamily: "Poppins", fontFamily: poppins.style.fontFamily,
}} }}
> >
CDR Link CDR Link

View file

@ -12,7 +12,7 @@ export const InternalLayout: FC<PropsWithChildren> = ({ children }) => {
<Sidebar open={open} setOpen={setOpen} /> <Sidebar open={open} setOpen={setOpen} />
<Grid <Grid
item item
sx={{ ml: open ? "270px" : "100px", width: "100%", height: "100vh" }} sx={{ ml: open ? "270px" : "70px", width: "100%", height: "100vh" }}
> >
{children as any} {children as any}
</Grid> </Grid>

View file

@ -3,7 +3,7 @@ import { usePathname, useRouter } from "next/navigation";
import useSWR from "swr"; import useSWR from "swr";
import { Grid, Box, TextField, Autocomplete } from "@mui/material"; import { Grid, Box, TextField, Autocomplete } from "@mui/material";
import { searchQuery } from "@/app/_graphql/searchQuery"; import { searchQuery } from "@/app/_graphql/searchQuery";
import { colors } from "@/app/_styles/theme"; import { colors } from "ui";
type SearchResultProps = { type SearchResultProps = {
props: any; props: any;

View file

@ -35,10 +35,10 @@ import LinkLogo from "public/link-logo-small.png";
import { useSession, signOut } from "next-auth/react"; import { useSession, signOut } from "next-auth/react";
import { getTicketOverviewCountsQuery } from "app/_graphql/getTicketOverviewCountsQuery"; import { getTicketOverviewCountsQuery } from "app/_graphql/getTicketOverviewCountsQuery";
import { SearchBox } from "./SearchBox"; import { SearchBox } from "./SearchBox";
import { fonts } from "app/_styles/theme"; import { fonts } from "ui";
const openWidth = 270; const openWidth = 270;
const closedWidth = 100; const closedWidth = 70;
const MenuItem = ({ const MenuItem = ({
name, name,
@ -49,6 +49,7 @@ const MenuItem = ({
selected = false, selected = false,
open = true, open = true,
badge, badge,
depth = 0,
target = "_self", target = "_self",
}: any) => { }: any) => {
const { roboto } = fonts; const { roboto } = fonts;
@ -90,8 +91,8 @@ const MenuItem = ({
width: 30, width: 30,
height: "28px", height: "28px",
position: "relative", position: "relative",
ml: "9px", ml: "8px",
mr: "1px", mr: "2px",
}} }}
> >
<Box <Box
@ -114,9 +115,21 @@ const MenuItem = ({
border: "solid 1px #fff", border: "solid 1px #fff",
borderColor: "transparent transparent transparent #fff", borderColor: "transparent transparent transparent #fff",
borderRadius: "60px", borderRadius: "60px",
rotate: "-35deg", rotate: "-50deg",
}} }}
/> />
{depth > 0 && (
<Box
sx={{
width: depth * 22,
height: "1px",
backgroundColor: "white",
position: "absolute",
left: "26px",
top: "14px",
}}
/>
)}
</Box> </Box>
)} )}
{open && ( {open && (
@ -132,6 +145,7 @@ const MenuItem = ({
border: 0, border: 0,
textAlign: "left", textAlign: "left",
color: "white", color: "white",
ml: depth * 3,
}} }}
> >
{name} {name}
@ -220,7 +234,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
<Box <Box
sx={{ sx={{
position: "absolute", position: "absolute",
top: 20, top: 24,
right: open ? -8 : -16, right: open ? -8 : -16,
color: "#1C75FD", color: "#1C75FD",
rotate: open ? "90deg" : "-90deg", rotate: open ? "90deg" : "-90deg",
@ -231,8 +245,8 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
> >
<ExpandCircleDownIcon <ExpandCircleDownIcon
sx={{ sx={{
width: 30, width: 24,
height: 30, height: 24,
background: "white", background: "white",
borderRadius: 500, borderRadius: 500,
}} }}
@ -338,9 +352,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
}} }}
/> />
</Grid> </Grid>
<Grid item> <Grid item>{open && <SearchBox />}</Grid>
<SearchBox />
</Grid>
<Grid <Grid
item item
container container
@ -558,7 +570,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
<> <>
<MenuItem <MenuItem
name="Admin" name="Admin"
href="/admin/zammad" href="/admin/bridge"
Icon={SettingsIcon} Icon={SettingsIcon}
iconSize={20} iconSize={20}
open={open} open={open}
@ -570,6 +582,56 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
onClick={undefined} onClick={undefined}
> >
<List component="div" disablePadding> <List component="div" disablePadding>
<MenuItem
name="CDR Bridge"
href="/admin/bridge"
selected={pathname.endsWith("/admin/bridge")}
open={open}
/>
<Collapse
in={pathname.startsWith("/admin/bridge")}
timeout="auto"
unmountOnExit
onClick={undefined}
>
<List component="div" disablePadding>
<MenuItem
name="WhatsApp"
href="/admin/bridge/whatsapp"
depth={1}
selected={pathname.endsWith("/admin/bridge/whatsapp")}
open={open}
/>
<MenuItem
name="Signal"
href="/admin/bridge/signal"
depth={1}
selected={pathname.endsWith("/admin/bridge/signal")}
open={open}
/>
<MenuItem
name="Facebook"
href="/admin/bridge/facebook"
depth={1}
selected={pathname.endsWith("/admin/bridge/facebook")}
open={open}
/>
<MenuItem
name="Voice"
href="/admin/bridge/voice"
depth={1}
selected={pathname.endsWith("/admin/bridge/voice")}
open={open}
/>
<MenuItem
name="Webhooks"
href="/admin/bridge/webhooks"
depth={1}
selected={pathname.endsWith("/admin/bridge/webhooks")}
open={open}
/>
</List>
</Collapse>
<MenuItem <MenuItem
name="Zammad Settings" name="Zammad Settings"
href="/admin/zammad" href="/admin/zammad"
@ -578,16 +640,6 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
selected={pathname.endsWith("/admin/zammad")} selected={pathname.endsWith("/admin/zammad")}
open={open} open={open}
/> />
{false && roles.includes("bridge") && (
<MenuItem
name="Bridge"
href="/admin/bridge"
Icon={FeaturedPlayListIcon}
iconSize={0}
selected={pathname.endsWith("/admin/bridge")}
open={open}
/>
)}
{roles.includes("label_studio") && ( {roles.includes("label_studio") && (
<MenuItem <MenuItem
name="Label Studio" name="Label Studio"

View file

@ -0,0 +1,11 @@
import { Create } from "bridge-ui";
type PageProps = {
params: { segment: string[] };
};
export default function Page({ params: { segment } }: PageProps) {
const service = segment[0];
return <Create service={service} />;
}

View file

@ -0,0 +1,27 @@
import { db } from "bridge-common";
import { serviceConfig, Detail } from "bridge-ui";
type Props = {
params: { segment: string[] };
};
export default async function Page({ params: { segment } }: Props) {
const service = segment[0];
const id = segment?.[1];
if (!id) return null;
const {
[service]: { table },
} = serviceConfig;
const row = await db
.selectFrom(table)
.selectAll()
.where("id", "=", id)
.executeTakeFirst();
if (!row) return null;
return <Detail service={service} row={row} />;
}

View file

@ -0,0 +1,27 @@
import { db } from "bridge-common";
import { serviceConfig, Edit } from "bridge-ui";
type PageProps = {
params: { segment: string[] };
};
export default async function Page({ params: { segment } }: PageProps) {
const service = segment[0];
const id = segment?.[1];
if (!id) return null;
const {
[service]: { table },
} = serviceConfig;
const row = await db
.selectFrom(table)
.selectAll()
.where("id", "=", id)
.executeTakeFirst();
if (!row) return null;
return <Edit service={service} row={row} />;
}

View file

@ -0,0 +1,3 @@
import { ServiceLayout } from "bridge-ui";
export default ServiceLayout;

View file

@ -0,0 +1,22 @@
import { db } from "bridge-common";
import { serviceConfig, List } from "bridge-ui";
type PageProps = {
params: {
segment: string[];
};
};
export default async function Page({ params: { segment } }: PageProps) {
const service = segment[0];
if (!service) return null;
const config = serviceConfig[service];
if (!config) return null;
const rows = await db.selectFrom(config.table).selectAll().execute();
return <List service={service} rows={rows} />;
}

View file

@ -1,6 +1,5 @@
// import { Admin } from "./_components/Admin"; import { Home } from "bridge-ui";
import { Box } from "@mui/material";
export default function Page() { export default function Page() {
return <Box />; return <Home />;
} }

View file

@ -11,7 +11,10 @@ export const DocsWrapper: FC = () => (
sx={{ height: "100%", width: "100%" }} sx={{ height: "100%", width: "100%" }}
direction="column" direction="column"
> >
<Grid item sx={{ height: "100vh", width: "100%" }}> <Grid
item
sx={{ height: "calc(100vh + 120px)", width: "100%", mt: "-121px" }}
>
<Iframe <Iframe
id="docs" id="docs"
url={"https://digiresilience.org/docs/link/about/"} url={"https://digiresilience.org/docs/link/about/"}

View file

@ -5,7 +5,7 @@ export const dynamic = "force-dynamic";
export default async function Page() { export default async function Page() {
return ( return (
<LeafcutterWrapper> <LeafcutterWrapper>
<Home visualizations={{}} /> <Home visualizations={[]} showWelcome={false} />
</LeafcutterWrapper> </LeafcutterWrapper>
); );
} }

View file

@ -1,17 +1,11 @@
"use client"; "use client";
import { FC, useState } from "react"; import { FC } from "react";
import { import { useFormState } from "react-dom";
Grid, import { Grid } from "@mui/material";
Button, import { Dialog, Button, TextField, Autocomplete } from "ui";
Dialog, import { createTicketAction } from "app/_actions/tickets";
DialogActions, import useSWR from "swr";
DialogContent,
TextField,
Autocomplete,
} from "@mui/material";
import useSWR, { useSWRConfig } from "swr";
import { createTicketMutation } from "app/_graphql/createTicketMutation";
interface TicketCreateDialogProps { interface TicketCreateDialogProps {
open: boolean; open: boolean;
@ -22,133 +16,83 @@ export const TicketCreateDialog: FC<TicketCreateDialogProps> = ({
open, open,
closeDialog, closeDialog,
}) => { }) => {
const [kind, setKind] = useState("note"); const initialState = {
const [customerID, setCustomerID] = useState(""); messages: [],
const [groupID, setGroupID] = useState(""); errors: [],
const [ownerID, setOwnerID] = useState(""); values: {
const [priorityID, setPriorityID] = useState(""); customerId: "",
const [stateID, setStateID] = useState(""); groupId: "",
const [tags, setTags] = useState([]); ownerId: "",
const [title, setTitle] = useState(""); priorityId: "",
const [body, setBody] = useState(""); stateId: "",
const backgroundColor = "#1982FC"; tags: [],
const color = "white"; title: "",
const { fetcher } = useSWRConfig();
const ticket = {
customerId: customerID,
groupId: groupID,
ownerId: ownerID,
priorityId: priorityID,
stateId: stateID,
tags,
title,
article: { article: {
body, body: "",
type: kind, type: "note",
internal: kind === "note", internal: true,
},
}, },
}; };
const [formState, formAction] = useFormState(
createTicketAction,
initialState,
);
const { data: users, error: usersError }: any = useSWR({ const { data: users, error: usersError }: any = useSWR({
url: "/api/v1/users", url: "/api/v1/users",
method: "GET", method: "GET",
}); });
console.log({ users, usersError });
const customers = const customers =
users?.filter((user: any) => user.role_ids.includes(3)) ?? []; users?.filter((user: any) => user.role_ids.includes(3)) ?? [];
const formattedCustomers = customers.map((customer: any) => ({ const formattedCustomers = customers.map((customer: any) => ({
label: customer.login, label: customer.login,
id: `${customer.id}`, id: `${customer.id}`,
})); }));
const createTicket = async () => {
await fetcher({
document: createTicketMutation,
variables: {
input: {
ticket,
},
},
});
closeDialog();
setBody("");
};
return ( return (
<Dialog open={open} maxWidth="md" fullWidth> <Dialog
<DialogContent> title="Create Ticket"
<Grid container direction="column" spacing={2}> open={open}
<Grid item> onClose={closeDialog}
<TextField formAction={formAction}
label={"Title"} buttons={
fullWidth
value={title}
onChange={(e: any) => setTitle(e.target.value)}
/>
</Grid>
<Grid item>
<Autocomplete
disablePortal
options={formattedCustomers}
value={customerID}
sx={{ width: 300 }}
onChange={(e: any) => setCustomerID(e.target.value.id)}
renderInput={(params) => (
<TextField {...params} label="Customer" />
)}
/>
</Grid>
<Grid item>
<TextField
label={"Details"}
multiline
rows={10}
fullWidth
value={body}
onChange={(e: any) => setBody(e.target.value)}
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions sx={{ px: 3, pt: 0, pb: 3 }}>
<Grid container justifyContent="space-between"> <Grid container justifyContent="space-between">
<Grid item> <Grid item>
<Button <Button
sx={{ text="Cancel"
backgroundColor: "white", kind="secondary"
color: "#666",
fontFamily: "Poppins, sans-serif",
fontWeight: 700,
borderRadius: 2,
textTransform: "none",
}}
onClick={() => { onClick={() => {
setBody("");
closeDialog(); closeDialog();
}} }}
> />
Cancel
</Button>
</Grid> </Grid>
<Grid item> <Grid item>
<Button <Button text="Save" type="submit" kind="primary" />
sx={{ </Grid>
backgroundColor, </Grid>
color, }
fontFamily: "Poppins, sans-serif",
fontWeight: 700,
borderRadius: 2,
textTransform: "none",
px: 3,
}}
onClick={createTicket}
> >
Create Ticket <Grid container direction="column" spacing={3}>
</Button> <Grid item>
<Autocomplete
name="customerId"
label="Customer"
options={formattedCustomers}
formState={formState}
/>
</Grid>
<Grid item>
<TextField name="title" label="Title" formState={formState} />
</Grid>
<Grid item>
<TextField
name="details"
label="Details"
lines={10}
formState={formState}
/>
</Grid> </Grid>
</Grid> </Grid>
</DialogActions>
</Dialog> </Dialog>
); );
}; };

View file

@ -4,8 +4,7 @@ import { FC, useState } from "react";
import { Grid, Box } from "@mui/material"; import { Grid, Box } from "@mui/material";
import { GridColDef } from "@mui/x-data-grid-pro"; import { GridColDef } from "@mui/x-data-grid-pro";
import { StyledDataGrid } from "app/(main)/_components/StyledDataGrid"; import { StyledDataGrid } from "app/(main)/_components/StyledDataGrid";
import { Button } from "app/_components/Button"; import { Button, List, typography } from "ui";
import { typography } from "app/_styles/theme";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { TicketCreateDialog } from "./TicketCreateDialog"; import { TicketCreateDialog } from "./TicketCreateDialog";
@ -26,83 +25,61 @@ export const TicketList: FC<TicketListProps> = ({ title, tickets }) => {
{ {
field: "title", field: "title",
headerName: "Title", headerName: "Title",
flex: 5, flex: 3,
}, },
{ {
field: "customer", field: "customer",
headerName: "Sender", headerName: "Sender",
valueGetter: (params: any) => params.row?.customer?.fullname, valueGetter: (value: any) => value?.fullname,
flex: 2, flex: 1,
}, },
{ {
field: "createdAt", field: "createdAt",
headerName: "Created At", headerName: "Created At",
valueGetter: (params: any) => valueGetter: (value: any) => new Date(value).toLocaleString(),
new Date(params.row?.createdAt).toLocaleString(),
flex: 1, flex: 1,
}, },
{ {
field: "updatedAt", field: "updatedAt",
headerName: "Updated At", headerName: "Updated At",
valueGetter: (params: any) => valueGetter: (value: any) => new Date(value).toLocaleString(),
new Date(params.row?.updatedAt).toLocaleString(),
flex: 1, flex: 1,
}, },
{ {
field: "group", field: "group",
headerName: "Group", headerName: "Group",
valueGetter: (params: any) => params.row?.group?.name, valueGetter: (value: any) => value?.name,
flex: 1, flex: 1,
}, },
]; ];
const rowClick = ({ row }) => { const onRowClick = (id: any) => {
router.push(`/tickets/${row.internalId}`); router.push(`/tickets/${id}`);
}; };
return ( return (
<> <>
<Box sx={{ height: "100vh", backgroundColor: "#ddd", p: 3 }}> <List
<Grid container direction="column"> title={title}
<Grid rows={tickets}
item columns={gridColumns}
container onRowClick={onRowClick}
direction="row" getRowID={(row: any) => {
justifyContent="space-between" console.log({ row });
alignItems="center" return row.internalId;
>
<Grid item>
<Box
sx={{
backgroundColor: "#ddd",
px: "8px",
pb: "16px",
...typography.h4,
fontSize: 24,
}} }}
> buttons={
{title} <Grid container direction="row-reverse" alignItems="center">
</Box>
</Grid>
<Grid item> <Grid item>
<Button <Button
href={""}
onClick={() => setDialogOpen(true)} onClick={() => setDialogOpen(true)}
text="Create" text="Create"
color="#1982FC" color="primary"
/> />
</Grid> </Grid>
</Grid> </Grid>
<Grid item> }
<StyledDataGrid
name={title}
columns={gridColumns}
rows={tickets}
onRowClick={rowClick}
/> />
</Grid>
</Grid>
</Box>
<TicketCreateDialog <TicketCreateDialog
open={dialogOpen} open={dialogOpen}
closeDialog={() => setDialogOpen(false)} closeDialog={() => setDialogOpen(false)}

View file

@ -108,6 +108,6 @@ export const ZammadOverview: FC<ZammadOverviewProps> = ({ name }) => {
}, [name]); }, [name]);
const shouldRender = tickets && !error; const shouldRender = tickets && !error;
console.log({ shouldRender, tickets, error });
return shouldRender && <TicketList title={name} tickets={tickets} />; return shouldRender && <TicketList title={name} tickets={tickets} />;
}; };

View file

@ -1,18 +1,12 @@
"use client"; "use client";
import { FC, useEffect, useState } from "react"; import { FC, useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
import { getTicketQuery } from "app/_graphql/getTicketQuery"; import { getTicketQuery } from "app/_graphql/getTicketQuery";
import { getTicketArticlesQuery } from "app/_graphql/getTicketArticlesQuery"; import { getTicketArticlesQuery } from "app/_graphql/getTicketArticlesQuery";
import { import { Grid, Box, Typography } from "@mui/material";
Grid, import { Button, fonts, colors } from "ui";
Box,
Typography,
Button,
// Dialog,
// DialogActions,
// DialogContent,
} from "@mui/material";
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
import { import {
MainContainer, MainContainer,
@ -28,6 +22,8 @@ interface TicketDetailProps {
} }
export const TicketDetail: FC<TicketDetailProps> = ({ id }) => { export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
const { poppins, roboto } = fonts;
const { veryLightGray, lightGray } = colors;
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [articleKind, setArticleKind] = useState("note"); const [articleKind, setArticleKind] = useState("note");
const { data: ticketData, error: ticketError }: any = useSWR( const { data: ticketData, error: ticketError }: any = useSWR(
@ -52,7 +48,6 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
}); });
const closeDialog = () => setDialogOpen(false); const closeDialog = () => setDialogOpen(false);
console.log({ recentViewData, recentViewError });
const ticket = ticketData?.ticket; const ticket = ticketData?.ticket;
const ticketArticles = ticketArticlesData?.ticketArticles; const ticketArticles = ticketArticlesData?.ticketArticles;
@ -79,13 +74,19 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
> >
<Typography <Typography
variant="h5" variant="h5"
sx={{ fontFamily: "Poppins", fontWeight: 700 }} sx={{
fontFamily: poppins.style.fontFamily,
fontWeight: 700,
}}
> >
{ticket.title} {ticket.title}
</Typography> </Typography>
<Typography <Typography
variant="h6" variant="h6"
sx={{ fontFamily: "Roboto", fontWeight: 400 }} sx={{
fontFamily: roboto.style.fontFamily,
fontWeight: 400,
}}
>{`Ticket #${ticket.number} (created ${new Date( >{`Ticket #${ticket.number} (created ${new Date(
ticket.createdAt, ticket.createdAt,
).toLocaleDateString()})`}</Typography> ).toLocaleDateString()})`}</Typography>
@ -118,8 +119,8 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
<Box <Box
sx={{ sx={{
height: 80, height: 80,
background: "#eeeeee", background: veryLightGray,
borderTop: "1px solid #ddd", borderTop: `1px solid ${lightGray}`,
position: "absolute", position: "absolute",
bottom: 0, bottom: 0,
width: "100%", width: "100%",
@ -128,59 +129,31 @@ export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
> >
<Grid <Grid
container container
spacing={4} spacing={6}
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="center"
alignContent="center" alignContent="center"
sx={{ height: "100%", pt: 6 }}
> >
<Grid item> <Grid item>
<Button <Button
variant="contained" text="Write note to agent"
disableElevation color="#FFB620"
sx={{
fontFamily: "Poppins, sans-serif",
fontWeight: 700,
borderRadius: 2,
textTransform: "none",
backgroundColor: "#1982FC",
padding: "6px 30px",
margin: "20px 0px",
whiteSpace: "nowrap",
py: "10px",
mt: 2,
}}
onClick={() => {
setArticleKind(firstArticleKind);
setDialogOpen(true);
}}
>
Reply to ticket
</Button>
</Grid>
<Grid item>
<Button
variant="contained"
disableElevation
sx={{
fontFamily: "Poppins, sans-serif",
fontWeight: 700,
borderRadius: 2,
textTransform: "none",
color: "black",
backgroundColor: "#FFB620",
padding: "6px 30px",
margin: "20px 0px",
whiteSpace: "nowrap",
py: "10px",
mt: 2,
}}
onClick={() => { onClick={() => {
setArticleKind("note"); setArticleKind("note");
setDialogOpen(true); setDialogOpen(true);
}} }}
> />
Write note to agent </Grid>
</Button> <Grid item>
<Button
text="Reply to ticket"
kind="primary"
onClick={() => {
setArticleKind(firstArticleKind);
setDialogOpen(true);
}}
/>
</Grid> </Grid>
</Grid> </Grid>
</Box> </Box>

View file

@ -1,35 +1,23 @@
"use client"; "use client";
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import { import { Grid, Box, MenuItem } from "@mui/material";
Grid, import { useFormState } from "react-dom";
Box, import { Select, Button } from "ui";
// Typography,
// TextField,
// Stack,
Select,
MenuItem,
} from "@mui/material";
import { MuiChipsInput } from "mui-chips-input"; import { MuiChipsInput } from "mui-chips-input";
import useSWR, { useSWRConfig } from "swr"; import useSWR, { useSWRConfig } from "swr";
import { getTicketQuery } from "app/_graphql/getTicketQuery"; import { getTicketQuery } from "app/_graphql/getTicketQuery";
import { updateTicketMutation } from "app/_graphql/updateTicketMutation";
import { updateTagsMutation } from "app/_graphql/updateTagsMutation";
import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css";
import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { updateTicketAction } from "app/_actions/tickets";
interface TicketEditProps { interface TicketEditProps {
id: string; id: string;
} }
export const TicketEdit: FC<TicketEditProps> = ({ id }) => { export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
const [selectedGroup, setSelectedGroup] = useState(""); const selectedTags = [];
const [selectedOwner, setSelectedOwner] = useState(""); const pendingVisible = false;
const [selectedPriority, setSelectedPriority] = useState(""); const pendingDate = new Date();
const [selectedState, setSelectedState] = useState("");
const [pendingDate, setPendingDate] = useState(new Date());
const [pendingVisible, setPendingVisible] = useState(false);
const [selectedTags, setSelectedTags] = useState([]);
const handleDelete = () => { const handleDelete = () => {
console.info("You clicked the delete icon."); console.info("You clicked the delete icon.");
}; };
@ -58,24 +46,28 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
const ticket = ticketData?.ticket; const ticket = ticketData?.ticket;
if (ticket) { if (ticket) {
const groupID = ticket.group.id?.split("/").pop(); const groupID = ticket.group.id?.split("/").pop();
setSelectedGroup(groupID); // setSelectedGroup(groupID);
const ownerID = ticket.owner.id?.split("/").pop(); const ownerID = ticket.owner.id?.split("/").pop();
setSelectedOwner(ownerID); // setSelectedOwner(ownerID);
const priorityID = ticket.priority.id?.split("/").pop(); const priorityID = ticket.priority.id?.split("/").pop();
setSelectedPriority(priorityID); // setSelectedPriority(priorityID);
const stateID = ticket.state.id?.split("/").pop(); const stateID = ticket.state.id?.split("/").pop();
setSelectedState(stateID); // setSelectedState(stateID);
setSelectedTags(ticket.tags); // setSelectedTags(ticket.tags);
} }
}, [ticketData, ticketError]); }, [ticketData, ticketError]);
/*
useEffect(() => { useEffect(() => {
const stateName = filteredStates?.find( const stateName = filteredStates?.find(
(state: any) => state.id === selectedState, (state: any) => state.id === selectedState,
)?.name; )?.name;
setPendingVisible(stateName?.includes("pending") ?? false); setPendingVisible(stateName?.includes("pending") ?? false);
}, [selectedState]); }, [selectedState]);
*/
const updateTicket = async (input: any) => { const updateTicket = async (input: any) => {
/*
console.log({ input }); console.log({ input });
const res = await fetcher({ const res = await fetcher({
document: updateTicketMutation, document: updateTicketMutation,
@ -85,8 +77,10 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
}, },
}); });
console.log({ res }); console.log({ res });
*/
}; };
const updateTags = async (tags: string[]) => { const updateTags = async (tags: string[]) => {
/*
console.log({ tags }); console.log({ tags });
const res = await fetcher({ const res = await fetcher({
document: updateTagsMutation, document: updateTagsMutation,
@ -96,18 +90,50 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
}, },
}); });
console.log({ res }); console.log({ res });
*/
}; };
const initialState = {
messages: [],
errors: [],
values: {
customer: "",
group: "",
owner: "",
priority: "",
state: "",
tags: [],
title: "",
article: {
body: "",
type: "note",
internal: true,
},
},
};
const [formState, formAction] = useFormState(
updateTicketAction,
initialState,
);
const shouldRender = ticketData && !ticketError; const shouldRender = ticketData && !ticketError;
return ( return (
shouldRender && ( shouldRender && (
<Box sx={{ height: "100vh", background: "#ddd", p: 2 }}> <Box sx={{ height: "100vh", background: "#ddd", p: 2 }}>
<form action={formAction}>
<Grid container direction="column" spacing={3}> <Grid container direction="column" spacing={3}>
<Grid item> <Grid item>
<Box sx={{ m: 1 }}>Group</Box> <Box sx={{ m: 1 }}>Group</Box>
<Select <Select
defaultValue={selectedGroup} name="group"
value={selectedGroup} label="Group"
formState={formState}
getOptions={() =>
groups?.map((group: any) => ({
value: group.id,
label: group.name,
})) ?? []
}
/*
onChange={(e: any) => { onChange={(e: any) => {
const newGroup = e.target.value; const newGroup = e.target.value;
setSelectedGroup(newGroup); setSelectedGroup(newGroup);
@ -115,45 +141,43 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
groupId: `gid://zammad/Group/${newGroup}`, groupId: `gid://zammad/Group/${newGroup}`,
}); });
}} }}
size="small" */
sx={{ />
width: "100%",
backgroundColor: "white",
}}
>
{groups?.map((group: any) => (
<MenuItem key={group.id} value={`${group.id}`}>
{group.name}
</MenuItem>
))}
</Select>
</Grid> </Grid>
<Grid item> <Grid item>
<Box sx={{ m: 1, mt: 0 }}>Owner</Box> <Box sx={{ m: 1, mt: 0 }}>Owner</Box>
<Select <Select
value={selectedOwner} name="owner"
label="Owner"
formState={formState}
getOptions={() =>
agents?.map((user: any) => ({
value: user.id,
label: `${user.firstname} ${user.lastname}`,
})) ?? []
}
/*
onChange={(e: any) => { onChange={(e: any) => {
const newOwner = e.target.value; const newOwner = e.target.value;
setSelectedOwner(newOwner); setSelectedOwner(newOwner);
updateTicket({ ownerId: `gid://zammad/User/${newOwner}` }); updateTicket({ ownerId: `gid://zammad/User/${newOwner}` });
}} }}
size="small" */
sx={{ />
width: "100%",
backgroundColor: "white",
}}
>
{agents?.map((user: any) => (
<MenuItem key={user.id} value={`${user.id}`}>
{user.firstname} {user.lastname}
</MenuItem>
))}
</Select>
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<Box sx={{ m: 1, mt: 0 }}>State</Box> <Box sx={{ m: 1, mt: 0 }}>State</Box>
<Select <Select
value={selectedState} name="state"
label="State"
formState={formState}
getOptions={() =>
filteredStates?.map((state: any) => ({
value: state.id,
label: state.name,
})) ?? []
}
/*
onChange={(e: any) => { onChange={(e: any) => {
const newState = e.target.value; const newState = e.target.value;
setSelectedState(newState); setSelectedState(newState);
@ -162,64 +186,57 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
pendingTime: pendingDate.toISOString(), pendingTime: pendingDate.toISOString(),
}); });
}} }}
size="small" */
sx={{ />
width: "100%",
backgroundColor: "white",
}}
>
{filteredStates?.map((state: any) => (
<MenuItem key={state.id} value={state.id}>
{state.name}
</MenuItem>
))}
</Select>
</Grid> </Grid>
<Grid <Grid
item item
xs={12} xs={12}
sx={{ display: pendingVisible ? "inherit" : "none" }} sx={{ display: pendingVisible ? "inherit" : "none" }}
> >
<DatePicker {/* <DatePicker
label="Pending Date" label="Pending Date"
value={pendingDate} value={pendingDate}
onChange={(newValue: any) => { onChange={(newValue: any) => {
console.log(newValue); console.log(newValue);
setPendingDate(newValue); setPendingDate(newValue);
updateTicket({ updateTicket({
pendingTime: newValue.toISOString(), pendingTime: newValue.toISOString(),
}); })
}} }}
slotProps={{ textField: { size: "small" } }} slotProps={{ textField: { size: "small" } }}
sx={{ sx={{
width: "100%", width: "100%",
backgroundColor: "white", backgroundColor: "white",
}} }}
/> /> */}
</Grid> </Grid>
<Grid item> <Grid item>
<Box sx={{ m: 1, mt: 0 }}>Priority</Box> <Box sx={{ m: 1, mt: 0 }}>Priority</Box>
<Select <Select
value={selectedPriority} name="priority"
label="Priority"
formState={formState}
getOptions={() =>
priorities?.map((priority: any) => ({
value: priority.id,
label: priority.name,
})) ?? []
}
/*
onChange={(e: any) => { onChange={(e: any) => {
const newPriority = e.target.value; const newPriority = e.target.value;
setSelectedPriority(newPriority); setSelectedPriority(newPriority);
updateTicket({ updateTicket({
priorityId: `gid://zammad/Ticket::Priority/${newPriority}`, priorityId: `gid://zammad/Ticket::Priority/${newPriority}`,
}); });
}} }}
size="small" */
sx={{ />
width: "100%",
backgroundColor: "white",
}}
>
{priorities?.map((priority: any) => (
<MenuItem key={priority.id} value={priority.id}>
{priority.name}
</MenuItem>
))}
</Select>
</Grid> </Grid>
<Grid item> <Grid item>
@ -228,12 +245,20 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
sx={{ backgroundColor: "white", width: "100%" }} sx={{ backgroundColor: "white", width: "100%" }}
value={selectedTags} value={selectedTags}
onChange={(tags: any) => { onChange={(tags: any) => {
/*
setSelectedTags(tags); setSelectedTags(tags);
updateTags(tags); updateTags(tags);
*/
}} }}
/> />
</Grid> </Grid>
<Grid item container direction="row-reverse">
<Grid item>
<Button text="Save" kind="primary" type="submit" />
</Grid> </Grid>
</Grid>
</Grid>
</form>
</Box> </Box>
) )
); );

View file

@ -0,0 +1,113 @@
"use server";
import { revalidatePath } from "next/cache";
import { createTicketMutation } from "app/_graphql/createTicketMutation";
import { updateTicketMutation } from "app/_graphql/updateTicketMutation";
import { updateTagsMutation } from "app/_graphql/updateTagsMutation";
// import { executeMutation } from "app/_lib/graphql";
// import { AddAssetMutation } from "../_graphql/AddAssetMutation";
export const createTicketAction = async (
currentState: any,
formData: FormData,
) => {
/*
const createTicket = async () => {
await fetcher({
document: createTicketMutation,
variables: {
input: {
ticket,
},
},
});
closeDialog();
setBody("");
};
*/
try {
const ticket = {
title: formData.get("title"),
};
const result = await executeMutation({
project,
mutation: AddAssetMutation,
variables: {
input: asset,
},
});
revalidatePath(`/${project}/assets`);
return {
...currentState,
values: { ...asset, ...result.addAsset, project },
success: true,
};
} catch (e: any) {
return { success: false, message: e?.message ?? "Unknown error" };
}
};
export const updateTicketAction = async (
currentState: any,
formData: FormData,
) => {
try {
const { id, project } = currentState.values;
const updatedTicket = {
title: formData.get("title"),
};
await executeMutation({
project,
mutation: UpdateAssetMutation,
variables: {
id,
input: updatedAsset,
},
});
revalidatePath(`/${project}/assets/${id}`);
return {
...currentState,
values: { ...currentState.values, ...updatedAsset, id, project },
success: true,
};
} catch (e: any) {
return { success: false, message: e?.message ?? "Unknown error" };
}
};
export const updateTicketTagsAction = async (
currentState: any,
formData: FormData,
) => {
try {
const { id, project } = currentState.values;
const updatedTicket = {
title: formData.get("title"),
};
await executeMutation({
project,
mutation: UpdateAssetMutation,
variables: {
id,
input: updatedAsset,
},
});
revalidatePath(`/${project}/assets/${id}`);
return {
...currentState,
values: { ...currentState.values, ...updatedAsset, id, project },
success: true,
};
} catch (e: any) {
return { success: false, message: e?.message ?? "Unknown error" };
}
};

View file

@ -0,0 +1,3 @@
"use server";
const fetchUsersAction = async () => {};

View file

@ -104,23 +104,19 @@ export const MultiProvider: FC<PropsWithChildren> = ({ children }) => {
}; };
return ( return (
<>
<CssBaseline />
<NextAppDirEmotionCacheProvider options={{ key: "css" }}> <NextAppDirEmotionCacheProvider options={{ key: "css" }}>
<CssBaseline />
<SWRConfig value={{ fetcher: multiFetcher }}> <SWRConfig value={{ fetcher: multiFetcher }}>
<SessionProvider> <SessionProvider>
<CookiesProvider> <CookiesProvider>
<LocalizationProvider dateAdapter={AdapterDateFns}> <LocalizationProvider dateAdapter={AdapterDateFns}>
<I18n locale={locale} messages={messages[locale]}> <I18n locale={locale} messages={messages[locale]}>
<LeafcutterProvider> <LeafcutterProvider>{children}</LeafcutterProvider>
{children}
</LeafcutterProvider>
</I18n> </I18n>
</LocalizationProvider> </LocalizationProvider>
</CookiesProvider> </CookiesProvider>
</SessionProvider> </SessionProvider>
</SWRConfig> </SWRConfig>
</NextAppDirEmotionCacheProvider> </NextAppDirEmotionCacheProvider>
</>
); );
}; };

View file

@ -1,113 +0,0 @@
import { Roboto, Playfair_Display, Poppins } from "next/font/google";
const roboto = Roboto({
weight: ["400"],
subsets: ["latin"],
display: "swap",
});
const playfair = Playfair_Display({
weight: ["900"],
subsets: ["latin"],
display: "swap",
});
const poppins = Poppins({
weight: ["400", "700"],
subsets: ["latin"],
display: "swap",
});
export const fonts = {
roboto,
playfair,
poppins,
};
export const colors: any = {
lightGray: "#ededf0",
mediumGray: "#e3e5e5",
darkGray: "#33302f",
mediumBlue: "#4285f4",
green: "#349d7b",
lavender: "#a5a6f6",
darkLavender: "#5d5fef",
pink: "#fcddec",
cdrLinkOrange: "#ff7115",
coreYellow: "#fac942",
helpYellow: "#fff4d5",
dwcDarkBlue: "#191847",
hazyMint: "#ecf7f8",
leafcutterElectricBlue: "#4d6aff",
leafcutterLightBlue: "#fafbfd",
waterbearElectricPurple: "#332c83",
waterbearLightSmokePurple: "#eff3f8",
bumpedPurple: "#212058",
mutedPurple: "#373669",
warningPink: "#ef5da8",
lightPink: "#fff0f7",
lightGreen: "#f0fff3",
lightOrange: "#fff5f0",
beige: "#f6f2f1",
almostBlack: "#33302f",
white: "#ffffff",
};
export const typography: any = {
h1: {
fontFamily: playfair.style.fontFamily,
fontSize: 45,
fontWeight: 700,
lineHeight: 1.1,
margin: 0,
},
h2: {
fontFamily: poppins.style.fontFamily,
fontSize: 35,
fontWeight: 700,
lineHeight: 1.1,
margin: 0,
},
h3: {
fontFamily: poppins.style.fontFamily,
fontWeight: 400,
fontSize: 27,
lineHeight: 1.1,
margin: 0,
},
h4: {
fontFamily: poppins.style.fontFamily,
fontWeight: 700,
fontSize: 18,
},
h5: {
fontFamily: roboto.style.fontFamily,
fontWeight: 700,
fontSize: 16,
lineHeight: "24px",
textTransform: "uppercase",
textAlign: "center",
margin: 1,
},
h6: {
fontFamily: roboto.style.fontFamily,
fontWeight: 400,
fontSize: 14,
textAlign: "center",
},
p: {
fontFamily: roboto.style.fontFamily,
fontSize: 17,
lineHeight: "26.35px",
fontWeight: 400,
margin: 0,
},
small: {
fontFamily: roboto.style.fontFamily,
fontSize: 13,
lineHeight: "18px",
fontWeight: 400,
margin: 0,
},
};

View file

@ -20,11 +20,12 @@
"@mui/material": "^5", "@mui/material": "^5",
"@mui/x-data-grid-pro": "^7.3.2", "@mui/x-data-grid-pro": "^7.3.2",
"@mui/x-date-pickers-pro": "^7.3.2", "@mui/x-date-pickers-pro": "^7.3.2",
"bridge-common": "*",
"bridge-ui": "*",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"graphql-request": "^6.1.0", "graphql-request": "^6.1.0",
"leafcutter-ui": "*", "leafcutter-ui": "*",
"material-ui-popup-state": "^5.1.0", "material-ui-popup-state": "^5.1.0",
"bridge-ui": "*",
"mui-chips-input": "^2.1.4", "mui-chips-input": "^2.1.4",
"next": "14.2.3", "next": "14.2.3",
"next-auth": "^4.24.7", "next-auth": "^4.24.7",

View file

@ -5,7 +5,7 @@ services:
container_name: leafcutter container_name: leafcutter
restart: ${RESTART} restart: ${RESTART}
build: build:
context: ../../ context: .
dockerfile: ../../apps/leafcutter/Dockerfile dockerfile: ../../apps/leafcutter/Dockerfile
image: registry.gitlab.com/digiresilience/link/link-stack/leafcutter:${LINK_STACK_VERSION} image: registry.gitlab.com/digiresilience/link/link-stack/leafcutter:${LINK_STACK_VERSION}
expose: expose:

View file

@ -5,7 +5,7 @@ services:
container_name: link container_name: link
restart: ${RESTART} restart: ${RESTART}
build: build:
context: ../../ context: .
dockerfile: ../../apps/link/Dockerfile dockerfile: ../../apps/link/Dockerfile
image: registry.gitlab.com/digiresilience/link/link-stack/link:${LINK_STACK_VERSION} image: registry.gitlab.com/digiresilience/link/link-stack/link:${LINK_STACK_VERSION}
expose: expose:

View file

@ -1 +1 @@
FROM memcached:1.6.23-bookworm FROM memcached:1.6.27-bookworm

View file

@ -1 +1 @@
FROM nginxproxy/nginx-proxy:1.5.1 FROM nginxproxy/nginx-proxy:1.5.2

View file

@ -1 +1 @@
FROM opensearchproject/opensearch-dashboards:2.12.0 FROM opensearchproject/opensearch-dashboards:2.13.0

View file

@ -1,2 +1,2 @@
FROM opensearchproject/opensearch:2.12.0 FROM opensearchproject/opensearch:2.13.0
RUN /usr/share/opensearch/bin/opensearch-plugin install ingest-attachment -b RUN /usr/share/opensearch/bin/opensearch-plugin install ingest-attachment -b

View file

@ -1 +1 @@
FROM bbernhard/signal-cli-rest-api:0.81 FROM bbernhard/signal-cli-rest-api:0.83

View file

@ -1,4 +1,6 @@
ARG ZAMMAD_VERSION=6.2.0 # need to include changes from https://github.com/zammad/zammad/blob/506c295c1d15f8dc19fc8bb69af1fb721bf10f49/contrib/docker/setup.sh
ARG ZAMMAD_VERSION=6.3.0
FROM node:16.18.0-slim as node FROM node:16.18.0-slim as node
FROM zammad/zammad-docker-compose:${ZAMMAD_VERSION} AS builder FROM zammad/zammad-docker-compose:${ZAMMAD_VERSION} AS builder

View file

@ -8,6 +8,7 @@ import { Button, Dialog, TextField, Select, MultiValueField } from "ui";
import { generateCreateAction } from "../lib/actions"; import { generateCreateAction } from "../lib/actions";
import { FieldDescription } from "../lib/service"; import { FieldDescription } from "../lib/service";
import { serviceConfig } from "../config/config"; import { serviceConfig } from "../config/config";
import { getBasePath } from "../lib/frontendUtils";
type CreateProps = { type CreateProps = {
service: string; service: string;
@ -51,7 +52,7 @@ export const Create: FC<CreateProps> = ({ service }) => {
useEffect(() => { useEffect(() => {
if (formState.success) { if (formState.success) {
router.push(`/${entity}/${formState.values.id}`); router.push(`${getBasePath()}${entity}/${formState.values.id}`);
} }
}, [formState.success, router, entity, formState.values.id]); }, [formState.success, router, entity, formState.values.id]);
@ -60,14 +61,14 @@ export const Create: FC<CreateProps> = ({ service }) => {
open open
title={`Create ${displayName}`} title={`Create ${displayName}`}
formAction={formAction} formAction={formAction}
onClose={() => router.push(`/${entity}`)} onClose={() => router.push(`${getBasePath()}${entity}`)}
buttons={ buttons={
<Grid container justifyContent="space-between"> <Grid container justifyContent="space-between">
<Grid item> <Grid item>
<Button <Button
text="Cancel" text="Cancel"
kind="secondary" kind="secondary"
onClick={() => router.push(`/${entity}`)} onClick={() => router.push(`${getBasePath()}${entity}`)}
/> />
</Grid> </Grid>
<Grid item> <Grid item>

View file

@ -9,6 +9,7 @@ import { type Database } from "bridge-common";
import { generateDeleteAction } from "../lib/actions"; import { generateDeleteAction } from "../lib/actions";
import { serviceConfig } from "../config/config"; import { serviceConfig } from "../config/config";
import { FieldDescription } from "../lib/service"; import { FieldDescription } from "../lib/service";
import { getBasePath } from "../lib/frontendUtils";
type DetailProps = { type DetailProps = {
service: string; service: string;
@ -29,7 +30,7 @@ export const Detail: FC<DetailProps> = ({ service, row }) => {
const continueDeleteAction = async () => { const continueDeleteAction = async () => {
await deleteAction?.(id); await deleteAction?.(id);
setShowDeleteConfirmation(false); setShowDeleteConfirmation(false);
router.push(`/${entity}`); router.push(`${getBasePath()}${entity}`);
}; };
return ( return (
@ -37,7 +38,7 @@ export const Detail: FC<DetailProps> = ({ service, row }) => {
<Dialog <Dialog
open open
title={`${displayName} Detail`} title={`${displayName} Detail`}
onClose={() => router.push(`/${entity}`)} onClose={() => router.push(`${getBasePath()}${entity}`)}
buttons={ buttons={
<Grid container justifyContent="space-between"> <Grid container justifyContent="space-between">
<Grid item container xs="auto" spacing={2}> <Grid item container xs="auto" spacing={2}>
@ -52,12 +53,16 @@ export const Detail: FC<DetailProps> = ({ service, row }) => {
<Button <Button
text="Edit" text="Edit"
kind="secondary" kind="secondary"
href={`/${entity}/${id}/edit`} href={`${getBasePath()}${entity}/${id}/edit`}
/> />
</Grid> </Grid>
</Grid> </Grid>
<Grid item> <Grid item>
<Button text="Done" kind="primary" href={`/${entity}`} /> <Button
text="Done"
kind="primary"
href={`${getBasePath()}${entity}`}
/>
</Grid> </Grid>
</Grid> </Grid>
} }

View file

@ -10,6 +10,7 @@ import { type Database } from "bridge-common";
import { generateUpdateAction } from "../lib/actions"; import { generateUpdateAction } from "../lib/actions";
import { serviceConfig } from "../config/config"; import { serviceConfig } from "../config/config";
import { FieldDescription } from "../lib/service"; import { FieldDescription } from "../lib/service";
import { getBasePath } from "../lib/frontendUtils";
type EditProps = { type EditProps = {
service: string; service: string;
@ -51,7 +52,7 @@ export const Edit: FC<EditProps> = ({ service, row }) => {
useEffect(() => { useEffect(() => {
if (formState.success) { if (formState.success) {
router.push(`/${entity}`); router.push(`${getBasePath()}${entity}`);
} }
}, [formState.success, router, entity]); }, [formState.success, router, entity]);
@ -60,14 +61,14 @@ export const Edit: FC<EditProps> = ({ service, row }) => {
open open
title={`Edit ${displayName}`} title={`Edit ${displayName}`}
formAction={formAction} formAction={formAction}
onClose={() => router.push(`/${entity}`)} onClose={() => router.push(`${getBasePath()}${entity}`)}
buttons={ buttons={
<Grid container justifyContent="space-between"> <Grid container justifyContent="space-between">
<Grid item> <Grid item>
<Button <Button
text="Cancel" text="Cancel"
kind="secondary" kind="secondary"
onClick={() => router.push(`/${entity}`)} onClick={() => router.push(`${getBasePath()}${entity}`)}
/> />
</Grid> </Grid>
<Grid item> <Grid item>

View file

@ -0,0 +1,5 @@
import { FC } from "react";
export const Home: FC = () => {
return <h1>Home</h1>;
};

View file

@ -6,6 +6,7 @@ import { List as InternalList, Button } from "ui";
import { type Selectable } from "kysely"; import { type Selectable } from "kysely";
import { type Database } from "bridge-common"; import { type Database } from "bridge-common";
import { serviceConfig } from "../config/config"; import { serviceConfig } from "../config/config";
import { getBasePath } from "../lib/frontendUtils";
type ListProps = { type ListProps = {
service: string; service: string;
@ -18,7 +19,7 @@ export const List: FC<ListProps> = ({ service, rows }) => {
const router = useRouter(); const router = useRouter();
const onRowClick = (id: string) => { const onRowClick = (id: string) => {
router.push(`/${entity}/${id}`); router.push(`${getBasePath()}${entity}/${id}`);
}; };
return ( return (
@ -28,7 +29,11 @@ export const List: FC<ListProps> = ({ service, rows }) => {
columns={listColumns} columns={listColumns}
onRowClick={onRowClick} onRowClick={onRowClick}
buttons={ buttons={
<Button text="Create" kind="primary" href={`/${entity}/create`} /> <Button
text="Create"
kind="primary"
href={`${getBasePath()}${entity}/create`}
/>
} }
/> />
); );

View file

@ -149,9 +149,14 @@ export const webhooksConfig: ServiceConfig = {
flex: 1, flex: 1,
}, },
{ {
field: "description", field: "backendType",
headerName: "Description", headerName: "Type",
flex: 2, flex: 1,
},
{
field: "endpointUrl",
headerName: "Endpoint",
flex: 1,
}, },
{ {
field: "updatedAt", field: "updatedAt",

View file

@ -1,3 +1,4 @@
export { Home } from "./components/Home";
export { List } from "./components/List"; export { List } from "./components/List";
export { Create } from "./components/Create"; export { Create } from "./components/Create";
export { Edit } from "./components/Edit"; export { Edit } from "./components/Edit";

View file

@ -0,0 +1,10 @@
export const getBasePath = (): string => {
if (
typeof window !== "undefined" &&
window?.location?.pathname?.includes("/admin/bridge")
) {
return "/admin/bridge/";
}
return "/";
};

File diff suppressed because one or more lines are too long

View file

@ -1,18 +1,27 @@
"use server"; "use server";
import { performLeafcutterQuery, performZammadQuery, createUserVisualization } from "opensearch-common"; import {
performLeafcutterQuery,
performZammadQuery,
createUserVisualization,
} from "opensearch-common";
export const createUserVisualizationAction = async ({visualizationID, title, description, query}: any) => { export const createUserVisualizationAction = async ({
const email = "darren@redaranj.com"; visualizationID,
title,
description,
query,
}: any) => {
const email = "xxx@example.com";
const id = await createUserVisualization({ const id = await createUserVisualization({
email, email,
visualizationID, visualizationID,
title, title,
description, description,
query query,
}); });
return id; return id;
} };
export const searchVisualizationsAction = async ( export const searchVisualizationsAction = async (
kind: string, kind: string,

View file

@ -3,7 +3,6 @@
import { useEffect, FC } from "react"; import { useEffect, FC } from "react";
import { useRouter, usePathname } from "next/navigation"; import { useRouter, usePathname } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import ReactMarkdown from "react-markdown";
import { Grid, Button } from "@mui/material"; import { Grid, Button } from "@mui/material";
import { useTranslate } from "react-polyglot"; import { useTranslate } from "react-polyglot";
import { useCookies } from "react-cookie"; import { useCookies } from "react-cookie";
@ -11,13 +10,18 @@ import { Welcome } from "./Welcome";
import { WelcomeDialog } from "./WelcomeDialog"; import { WelcomeDialog } from "./WelcomeDialog";
import { VisualizationCard } from "./VisualizationCard"; import { VisualizationCard } from "./VisualizationCard";
import { useLeafcutterContext } from "./LeafcutterProvider"; import { useLeafcutterContext } from "./LeafcutterProvider";
import { getBasePath } from "../lib/utils";
type HomeProps = { type HomeProps = {
visualizations: any; visualizations: any;
showWelcome?: boolean; showWelcome?: boolean;
}; };
export const Home: FC<HomeProps> = ({ visualizations = [], showWelcome = true }) => { export const Home: FC<HomeProps> = ({
visualizations = [],
showWelcome = true,
}) => {
console.log("Home", visualizations);
const router = useRouter(); const router = useRouter();
const pathname = usePathname() ?? ""; const pathname = usePathname() ?? "";
const cookieName = "homeIntroComplete"; const cookieName = "homeIntroComplete";
@ -45,7 +49,7 @@ export const Home: FC<HomeProps> = ({ visualizations = [], showWelcome = true })
sx={{ pt: "22px", pb: "22px" }} sx={{ pt: "22px", pb: "22px" }}
direction="row-reverse" direction="row-reverse"
> >
<Link href={`${process.env.LEAFCUTTER_BASE_PATH ?? ""}/create`} passHref> <Link href={`${getBasePath()}/create`} passHref>
<Button <Button
sx={{ sx={{
fontSize: 14, fontSize: 14,
@ -81,7 +85,11 @@ export const Home: FC<HomeProps> = ({ visualizations = [], showWelcome = true })
justifyContent="center" justifyContent="center"
> >
<Grid item sx={{ ...h4, width: 450, textAlign: "center" }}> <Grid item sx={{ ...h4, width: 450, textAlign: "center" }}>
<ReactMarkdown>{t("noSavedVisualizations")}</ReactMarkdown> {"You dont have any saved visualizations. Go to "}
<Link href={`${getBasePath()}/create`}>Search and Create</Link>
{" or "}
<Link href={`${getBasePath()}/trends`}>Trends</Link>
{" to get started."}
</Grid> </Grid>
</Grid> </Grid>
) : null} ) : null}

View file

@ -0,0 +1,9 @@
export const getBasePath = (): string => {
const basePath = process.env.LEAFCUTTER_BASE_PATH;
if (basePath && basePath !== "") {
return `${basePath}`;
}
return "";
};

View file

@ -0,0 +1,9 @@
export const getBasePath = (): string => {
const basePath = process.env.NEXT_PUBLIC_LEAFCUTTER_BASE_PATH;
if (basePath && basePath !== "") {
return `${basePath}`;
}
return "";
};

File diff suppressed because one or more lines are too long

View file

@ -329,7 +329,7 @@ export const getUserVisualizations = async (email: string, limit: number) => {
url: getEmbedURL("private", getDocumentID(hit)), url: getEmbedURL("private", getDocumentID(hit)),
})); }));
return results; return []; //results;
}; };
/* Global */ /* Global */

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,38 @@
import { FC } from "react";
import { TextField, Autocomplete as AutocompleteInternal } from "@mui/material";
import { colors } from "../styles/theme";
type AutocompleteProps = {
name: string;
label: string;
options: any[];
formState: Record<string, any>;
disabled?: boolean;
required?: boolean;
};
export const Autocomplete: FC<AutocompleteProps> = ({
name,
label,
options,
formState,
disabled = false,
required = false,
}) => (
<AutocompleteInternal
disablePortal
options={options}
defaultValue={formState.values[name]}
fullWidth
size="small"
renderInput={(params) => (
<TextField
{...params}
label={label}
disabled={disabled}
required={required}
sx={{ backgroundColor: colors.white }}
/>
)}
/>
);

View file

@ -10,6 +10,7 @@ interface ListProps {
rows: any; rows: any;
columns: GridColDef<any>[]; columns: GridColDef<any>[];
onRowClick?: (id: string) => void; onRowClick?: (id: string) => void;
getRowID?: (row: any) => any;
buttons?: React.ReactNode; buttons?: React.ReactNode;
paginate?: boolean; paginate?: boolean;
} }
@ -19,12 +20,20 @@ export const List: FC<ListProps> = ({
rows, rows,
columns, columns,
onRowClick, onRowClick,
getRowID,
buttons, buttons,
paginate = false, paginate = false,
}) => { }) => {
const { h3 } = typography; const { h3 } = typography;
const { mediumGray, lightGray, veryLightGray, mediumBlue, white, darkGray } = const { mediumGray, lightGray, veryLightGray, mediumBlue, white, darkGray } =
colors; colors;
const getRowIDInternal = (row: any) => {
if (getRowID) {
return getRowID(row);
}
return row.id;
};
return ( return (
<Box sx={{ height: "100vh", backgroundColor: lightGray, p: 3 }}> <Box sx={{ height: "100vh", backgroundColor: lightGray, p: 3 }}>
@ -92,7 +101,7 @@ export const List: FC<ListProps> = ({
scrollbarSize={0} scrollbarSize={0}
disableVirtualization disableVirtualization
disableColumnMenu disableColumnMenu
onRowClick={(row: any) => onRowClick?.(row.id)} onRowClick={({ row }: any) => onRowClick?.(getRowIDInternal(row))}
/> />
</Box> </Box>
</Grid> </Grid>

View file

@ -28,7 +28,7 @@ import { fonts } from "../styles/theme";
// import { useSession, signOut } from "next-auth/react"; // import { useSession, signOut } from "next-auth/react";
const openWidth = 270; const openWidth = 270;
const closedWidth = 100; const closedWidth = 70;
const MenuItem = ({ const MenuItem = ({
name, name,

View file

@ -14,7 +14,7 @@ import Image from "next/image";
import { fonts } from "../styles/theme"; import { fonts } from "../styles/theme";
const openWidth = 270; const openWidth = 270;
const closedWidth = 100; const closedWidth = 70;
export const SidebarItem: FC = ({ export const SidebarItem: FC = ({
name, name,

View file

@ -5,7 +5,7 @@ import {
IconButton, IconButton,
} from "@mui/material"; } from "@mui/material";
import { Refresh as RefreshIcon } from "@mui/icons-material"; import { Refresh as RefreshIcon } from "@mui/icons-material";
import { colors } from "../styles/theme"; import { colors, fonts } from "../styles/theme";
type TextFieldProps = { type TextFieldProps = {
name: string; name: string;
@ -28,7 +28,8 @@ export const TextField: FC<TextFieldProps> = ({
lines = 1, lines = 1,
helperText, helperText,
}) => { }) => {
const { darkMediumGray } = colors; const { darkMediumGray, white } = colors;
const { roboto } = fonts;
return ( return (
<InternalTextField <InternalTextField
@ -58,7 +59,8 @@ export const TextField: FC<TextFieldProps> = ({
) : null, ) : null,
sx: { sx: {
backgroundColor: "#fff", fontFamily: roboto.style.fontFamily,
backgroundColor: white,
}, },
}} }}
/> />

View file

@ -9,6 +9,7 @@ export { Button } from "./components/Button";
export { TextField } from "./components/TextField"; export { TextField } from "./components/TextField";
export { DisplayTextField } from "./components/DisplayTextField"; export { DisplayTextField } from "./components/DisplayTextField";
export { Select } from "./components/Select"; export { Select } from "./components/Select";
export { Autocomplete } from "./components/Autocomplete";
export { MultiValueField } from "./components/MultiValueField"; export { MultiValueField } from "./components/MultiValueField";
export { Dialog } from "./components/Dialog"; export { Dialog } from "./components/Dialog";
export { fonts, typography, colors } from "./styles/theme"; export { fonts, typography, colors } from "./styles/theme";

File diff suppressed because one or more lines are too long