Use server actions instead of client-side API calls

This commit is contained in:
Darren Clarke 2024-08-05 23:31:15 +02:00
parent 5a3127dcb0
commit aa453954ed
30 changed files with 703 additions and 462 deletions

View file

@ -20,11 +20,12 @@ import {
WhatsApp as WhatsAppIcon, WhatsApp as WhatsAppIcon,
Facebook as FacebookIcon, Facebook as FacebookIcon,
AirlineStops as AirlineStopsIcon, AirlineStops as AirlineStopsIcon,
Logout as LogoutIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { typography, fonts } from "@link-stack/ui"; import { typography, fonts, Button } from "@link-stack/ui";
import LinkLogo from "@/app/_images/link-logo-small.png"; import LinkLogo from "@/app/_images/link-logo-small.png";
import { useSession, signOut } from "next-auth/react"; import { useSession, signOut } from "next-auth/react";
@ -161,9 +162,9 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
const { data: session } = useSession(); const { data: session } = useSession();
const user = session?.user; const user = session?.user;
// const logout = () => { const logout = () => {
// signOut({ callbackUrl: "/login" }); signOut({ callbackUrl: "/login" });
// }; };
return ( return (
<Drawer <Drawer
@ -377,18 +378,20 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
</Box> </Box>
</Grid> </Grid>
)} )}
{open && (
<Grid item> <Grid item>
<Box <Box
sx={{ sx={{
...bodyLarge, ...bodyLarge,
color: "white", color: "white",
}} }}
> >
{user?.email} {user?.email}
</Box> </Box>
</Grid> </Grid>
)} <Grid item>
<Button text="Logout" kind="secondary" onClick={logout} />
</Grid>
</Grid> </Grid>
</Grid> </Grid>
</Drawer> </Drawer>

View file

@ -0,0 +1,9 @@
import { Kysely } from "kysely";
export async function up(db: Kysely<any>): Promise<void> {
await db.schema.alterTable("User").addColumn("role", "text").execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.alterTable("User").dropColumn("role").execute();
}

View file

@ -15,7 +15,6 @@ const fetchSignalMessagesTask = async (): Promise<void> => {
const messages = await messagesClient.v1ReceiveNumberGet({ number }); const messages = await messagesClient.v1ReceiveNumberGet({ number });
for (const msg of messages) { for (const msg of messages) {
console.log(msg);
const { envelope } = msg as any; const { envelope } = msg as any;
const { source, sourceUuid, dataMessage } = envelope; const { source, sourceUuid, dataMessage } = envelope;
const message = dataMessage?.message; const message = dataMessage?.message;

View file

@ -4,6 +4,8 @@ import { FC } from "react";
import { OpenSearchWrapper } from "@link-stack/leafcutter-ui"; import { OpenSearchWrapper } from "@link-stack/leafcutter-ui";
export const Home: FC = () => ( export const Home: FC = () => (
<OpenSearchWrapper url="/app/visualize#/edit/237b8f00-e6a0-11ee-94b3-d7b7409294e7?embed=true" marginTop="0" <OpenSearchWrapper
url="/app/visualize#/edit/237b8f00-e6a0-11ee-94b3-d7b7409294e7?embed=true"
marginTop="0"
/> />
); );

View file

@ -1,21 +1,32 @@
"use client"; "use client";
import { FC, PropsWithChildren, useState } from "react"; import { FC, PropsWithChildren, useState } from "react";
import { Grid } from "@mui/material"; import { Grid, Box } from "@mui/material";
import { Sidebar } from "./Sidebar"; import { Sidebar } from "./Sidebar";
import { SetupModeWarning } from "./SetupModeWarning";
export const InternalLayout: FC<PropsWithChildren> = ({ children }) => { interface InternalLayoutProps extends PropsWithChildren {
setupModeActive: boolean;
}
export const InternalLayout: FC<InternalLayoutProps> = ({
children,
setupModeActive,
}) => {
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
return ( return (
<Grid container direction="row"> <Box sx={{ position: "relative" }}>
<Sidebar open={open} setOpen={setOpen} /> <SetupModeWarning setupModeActive={setupModeActive} />
<Grid <Grid container direction="row">
item <Sidebar open={open} setOpen={setOpen} />
sx={{ ml: open ? "270px" : "70px", width: "100%", height: "100vh" }} <Grid
> item
{children as any} sx={{ ml: open ? "270px" : "70px", width: "100%", height: "100vh" }}
>
{children as any}
</Grid>
</Grid> </Grid>
</Grid> </Box>
); );
}; };

View file

@ -1,8 +1,7 @@
import { FC, useState, useEffect } from "react"; import { FC, useState, useEffect } from "react";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
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 { searchAllAction } from "@/app/_actions/search";
import { colors } from "@link-stack/ui"; import { colors } from "@link-stack/ui";
type SearchResultProps = { type SearchResultProps = {
@ -42,8 +41,6 @@ const SearchInput = (params: any) => (
); );
const SearchResult: FC<SearchResultProps> = ({ props, option }) => { const SearchResult: FC<SearchResultProps> = ({ props, option }) => {
console.log({ option });
const { lightGrey, mediumGray, black, white } = colors; const { lightGrey, mediumGray, black, white } = colors;
return ( return (
@ -95,22 +92,20 @@ const SearchResult: FC<SearchResultProps> = ({ props, option }) => {
export const SearchBox: FC = () => { export const SearchBox: FC = () => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [searchResults, setSearchResults] = useState([]);
const [selectedValue, setSelectedValue] = useState(null); const [selectedValue, setSelectedValue] = useState(null);
const [searchTerms, setSearchTerms] = useState(""); const [searchTerms, setSearchTerms] = useState("");
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter(); const router = useRouter();
const { data, error }: any = useSWR({
document: searchQuery,
variables: {
search: searchTerms ?? "",
limit: 50,
},
refreshInterval: 10000,
});
useEffect(() => { useEffect(() => {
setOpen(false); const fetchSearchResults = async () => {
}, [pathname]); const results = await searchAllAction(searchTerms ?? "", 50);
setSearchResults(results);
};
fetchSearchResults();
}, [searchTerms]);
return ( return (
<Autocomplete <Autocomplete
@ -132,7 +127,7 @@ export const SearchBox: FC = () => {
open={open} open={open}
onOpen={() => setOpen(true)} onOpen={() => setOpen(true)}
noOptionsText="No results" noOptionsText="No results"
options={data?.search ?? []} options={searchResults ?? []}
getOptionLabel={(option: any) => { getOptionLabel={(option: any) => {
if (option) { if (option) {
return option.title; return option.title;

View file

@ -0,0 +1,29 @@
"use client";
import { FC } from "react";
import { Box } from "@mui/material";
interface SetupModeWarningProps {
setupModeActive: boolean;
}
export const SetupModeWarning: FC<SetupModeWarningProps> = ({
setupModeActive,
}) =>
setupModeActive ? (
<Box
sx={{
backgroundColor: "red",
textAlign: "center",
zIndex: 10000,
position: "absolute",
width: "100vw",
top: 0,
left: 0,
}}
>
<Box component="h2" sx={{ color: "white", m: 0, p: 0 }}>
Setup Mode Active
</Box>
</Box>
) : null;

View file

@ -1,7 +1,6 @@
"use client"; "use client";
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import useSWR from "swr";
import { import {
Box, Box,
Grid, Grid,
@ -33,7 +32,7 @@ import Link from "next/link";
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 { useSession, signOut } from "next-auth/react"; import { useSession, signOut } from "next-auth/react";
import { getTicketOverviewCountsQuery } from "app/_graphql/getTicketOverviewCountsQuery"; import { getOverviewTicketCountsAction } from "app/_actions/overviews";
import { SearchBox } from "./SearchBox"; import { SearchBox } from "./SearchBox";
import { fonts } from "@link-stack/ui"; import { fonts } from "@link-stack/ui";
@ -185,33 +184,32 @@ interface SidebarProps {
export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => { export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
const pathname = usePathname(); const pathname = usePathname();
const { data: session } = useSession(); const { data: session } = useSession();
const [overviewCounts, setOverviewCounts] = useState<any>(null);
const { poppins } = fonts; const { poppins } = fonts;
const username = session?.user?.name || "User"; const username = session?.user?.name || "User";
// @ts-ignore // @ts-ignore
const roles = session?.user?.roles || []; const roles = session?.user?.roles || [];
const { data: overviewData, error: overviewError }: any = useSWR( const leafcutterEnabled = false;
{
document: getTicketOverviewCountsQuery, useEffect(() => {
}, const fetchCounts = async () => {
{ refreshInterval: 10000 }, const counts = await getOverviewTicketCountsAction();
); console.log({ counts });
const findOverviewByName = (name: string) => setOverviewCounts(counts);
overviewData?.ticketOverviews?.edges?.find( };
(overview: any) => overview.node.name === name,
)?.node?.id; fetchCounts();
const findOverviewCountByID = (id: string) =>
overviewData?.ticketOverviews?.edges?.find( const interval = setInterval(fetchCounts, 10000);
(overview: any) => overview.node.id === id,
)?.node?.ticketCount ?? 0; return () => clearInterval(interval);
}, []);
const recentCount = 0; const recentCount = 0;
const assignedID = findOverviewByName("My Assigned Tickets"); const assignedCount = overviewCounts?.["My Assigned Tickets"] ?? 0;
const assignedCount = findOverviewCountByID(assignedID); const openCount = overviewCounts?.["Open Tickets"] ?? 0;
const openID = findOverviewByName("Open Tickets"); const urgentCount = overviewCounts?.["Escalated Tickets"] ?? 0;
const openCount = findOverviewCountByID(openID); const unassignedCount = overviewCounts?.["Unassigned & Open Tickets"] ?? 0;
const urgentID = findOverviewByName("Escalated Tickets");
const urgentCount = findOverviewCountByID(urgentID);
const unassignedID = findOverviewByName("Unassigned & Open Tickets");
const unassignedCount = findOverviewCountByID(unassignedID);
const logout = () => { const logout = () => {
signOut({ callbackUrl: "/login" }); signOut({ callbackUrl: "/login" });
@ -404,14 +402,16 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
}, },
}} }}
> >
<MenuItem {leafcutterEnabled && (
name="Home" <MenuItem
href="/" name="Home"
Icon={CottageIcon} href="/"
iconSize={20} Icon={CottageIcon}
selected={pathname.endsWith("/")} iconSize={20}
open={open} selected={pathname.endsWith("/")}
/> open={open}
/>
)}
<MenuItem <MenuItem
name="Tickets" name="Tickets"
href="/overview/recent" href="/overview/recent"
@ -504,14 +504,16 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
selected={pathname.endsWith("/reporting")} selected={pathname.endsWith("/reporting")}
open={open} open={open}
/> />
<MenuItem {leafcutterEnabled && (
name="Leafcutter" <MenuItem
href="/leafcutter" name="Leafcutter"
Icon={InsightsIcon} href="/leafcutter"
iconSize={20} Icon={InsightsIcon}
selected={false} iconSize={20}
open={open} selected={false}
/> open={open}
/>
)}
<Collapse <Collapse
in={pathname.startsWith("/leafcutter")} in={pathname.startsWith("/leafcutter")}
timeout="auto" timeout="auto"

View file

@ -11,5 +11,11 @@ type LayoutProps = {
}; };
export default function Layout({ children }: LayoutProps) { export default function Layout({ children }: LayoutProps) {
return <InternalLayout>{children}</InternalLayout>; const setupModeActive = process.env.SETUP_MODE === "true";
return (
<InternalLayout setupModeActive={setupModeActive}>
{children}
</InternalLayout>
);
} }

View file

@ -1,11 +1,19 @@
"use client"; "use client";
import { FC } from "react"; import { FC, useState, useEffect } from "react";
import { useFormState } from "react-dom"; import { useFormState } from "react-dom";
import { useRouter } from "next/navigation";
import { Grid } from "@mui/material"; import { Grid } from "@mui/material";
import { Dialog, Button, TextField, Autocomplete } from "@link-stack/ui"; import {
Dialog,
Button,
TextField,
Autocomplete,
Select,
} from "@link-stack/ui";
import { createTicketAction } from "app/_actions/tickets"; import { createTicketAction } from "app/_actions/tickets";
import useSWR from "swr"; import { getCustomersAction } from "app/_actions/users";
import { getGroupsAction } from "app/_actions/groups";
interface TicketCreateDialogProps { interface TicketCreateDialogProps {
open: boolean; open: boolean;
@ -16,6 +24,8 @@ export const TicketCreateDialog: FC<TicketCreateDialogProps> = ({
open, open,
closeDialog, closeDialog,
}) => { }) => {
const [customers, setCustomers] = useState([]);
const [groups, setGroups] = useState([]);
const initialState = { const initialState = {
messages: [], messages: [],
errors: [], errors: [],
@ -38,16 +48,41 @@ export const TicketCreateDialog: FC<TicketCreateDialogProps> = ({
createTicketAction, createTicketAction,
initialState, initialState,
); );
const { data: users, error: usersError }: any = useSWR({ const [liveFormState, setLiveFormState] = useState(formState);
url: "/api/v1/users", const updateFormState = (field: string, value: any) => {
method: "GET", console.log({ value });
}); const newState = { ...liveFormState };
const customers = newState.values[field] = value;
users?.filter((user: any) => user.role_ids.includes(3)) ?? []; setLiveFormState(newState);
const formattedCustomers = customers.map((customer: any) => ({ };
label: customer.login,
id: `${customer.id}`, useEffect(() => {
})); const fetchUsers = async () => {
const result = await getCustomersAction();
console.log({ result });
setCustomers(result);
};
fetchUsers();
}, []);
useEffect(() => {
const fetchGroups = async () => {
const result = await getGroupsAction();
console.log({ result });
setGroups(result);
};
fetchGroups();
}, []);
const router = useRouter();
useEffect(() => {
if (formState.success) {
closeDialog();
}
}, [formState.success, router]);
return ( return (
<Dialog <Dialog
@ -74,11 +109,21 @@ export const TicketCreateDialog: FC<TicketCreateDialogProps> = ({
> >
<Grid container direction="column" spacing={3}> <Grid container direction="column" spacing={3}>
<Grid item> <Grid item>
<Autocomplete <Select
name="groupId"
label="Group"
getOptions={() => groups as any}
formState={liveFormState}
updateFormState={updateFormState}
/>
</Grid>
<Grid item>
<Select
name="customerId" name="customerId"
label="Customer" label="Customer"
options={formattedCustomers} getOptions={() => customers as any}
formState={formState} formState={liveFormState}
updateFormState={updateFormState}
/> />
</Grid> </Grid>
<Grid item> <Grid item>

View file

@ -1,113 +1,29 @@
"use client"; "use client";
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import useSWR from "swr"; import { getOverviewTicketsAction } from "app/_actions/overviews";
import { TicketList } from "./TicketList"; import { TicketList } from "./TicketList";
import { getTicketOverviewCountsQuery } from "app/_graphql/getTicketOverviewCountsQuery";
import { getTicketsByOverviewQuery } from "app/_graphql/getTicketsByOverviewQuery";
type ZammadOverviewProps = { type ZammadOverviewProps = {
name: string; name: string;
}; };
export const ZammadOverview: FC<ZammadOverviewProps> = ({ name }) => { export const ZammadOverview: FC<ZammadOverviewProps> = ({ name }) => {
const [overviewID, setOverviewID] = useState(null);
const [tickets, setTickets] = useState([]); const [tickets, setTickets] = useState([]);
const [error, setError] = useState(null);
const { data: overviewData, error: overviewError }: any = useSWR(
{
document: getTicketOverviewCountsQuery,
},
{ refreshInterval: 10000 },
);
const { data: ticketData, error: ticketError }: any = useSWR(
{
document: getTicketsByOverviewQuery,
variables: { overviewId: overviewID, pageSize: 250 },
},
{ refreshInterval: 10000 },
);
const overviewLookup = {
Assigned: "My Assigned Tickets",
Open: "Open Tickets",
Urgent: "Escalated Tickets",
Unassigned: "Unassigned & Open Tickets",
};
const findOverviewByName = (name: string) => {
const fullName = overviewLookup[name];
return overviewData?.ticketOverviews?.edges?.find(
(overview: any) => overview.node.name === fullName,
)?.node?.id;
};
useEffect(() => {
if (overviewData) {
setOverviewID(findOverviewByName(name));
}
}, [overviewData, name]);
console.log({
name,
overviewID,
overviewData,
overviewError,
ticketData,
ticketError,
});
const restFetcher = (url: string) => fetch(url).then((r) => r.json());
const { data: recent } = useSWR("/api/v1/recent_view", restFetcher);
const sortTickets = (tickets: any) => {
return tickets.sort((a: any, b: any) => {
if (a.internalId < b.internalId) {
return 1;
}
if (a.internalId > b.internalId) {
return -1;
}
return 0;
});
};
useEffect(() => { useEffect(() => {
if (name != "Recent") { const fetchTickets = async () => {
const edges = ticketData?.ticketsByOverview?.edges; const { tickets } = await getOverviewTicketsAction(name);
if (edges) { setTickets(tickets);
const nodes = edges.map((edge: any) => edge.node);
console.log({ nodes });
setError(null);
setTickets(sortTickets(nodes));
}
if (ticketError) {
setError(ticketError);
}
}
}, [ticketData, ticketError]);
useEffect(() => {
const fetchRecentTickets = async () => {
if (name === "Recent" && recent) {
let allTickets = [];
for (const rec of recent) {
const res = await fetch(`/api/v1/tickets/${rec.o_id}`);
const tkt = await res.json();
allTickets.push({
...tkt,
internalId: tkt.id,
createdAt: tkt.created_at,
updatedAt: tkt.updated_at,
});
}
setTickets(sortTickets(allTickets));
console.log({ allTickets });
}
}; };
fetchRecentTickets();
fetchTickets();
const interval = setInterval(fetchTickets, 20000);
return () => clearInterval(interval);
}, [name]); }, [name]);
const shouldRender = tickets && !error; return <TicketList title={name} tickets={tickets} />;
return shouldRender && <TicketList title={name} tickets={tickets} />;
}; };

View file

@ -17,7 +17,7 @@ export async function generateMetadata({
const section = getSection(overview); const section = getSection(overview);
return { return {
title: `Link - ${section} Tickets`, title: `CDR Link - ${section} Tickets`,
}; };
} }

View file

@ -1,4 +1,5 @@
import { Metadata } from "next"; import { Metadata } from "next";
import { redirect } from "next/navigation";
import { getServerSession } from "app/_lib/authentication"; import { getServerSession } from "app/_lib/authentication";
import { Home } from "@link-stack/leafcutter-ui"; import { Home } from "@link-stack/leafcutter-ui";
import { getUserVisualizations } from "@link-stack/opensearch-common"; import { getUserVisualizations } from "@link-stack/opensearch-common";
@ -13,11 +14,14 @@ export default async function Page() {
const { const {
user: { email }, user: { email },
}: any = session; }: any = session;
const visualizations = await getUserVisualizations(email ?? "none", 20); // const visualizations = await getUserVisualizations(email ?? "none", 20);
redirect("/overview/recent");
/*
return ( return (
<LeafcutterWrapper> <LeafcutterWrapper>
<Home visualizations={visualizations} showWelcome={false} /> <Home visualizations={visualizations} showWelcome={false} />
</LeafcutterWrapper> </LeafcutterWrapper>
); );
*/
} }

View file

@ -9,8 +9,7 @@ import {
DialogContent, DialogContent,
TextField, TextField,
} from "@mui/material"; } from "@mui/material";
import { useSWRConfig } from "swr"; import { createTicketArticleAction } from "app/_actions/tickets";
import { updateTicketMutation } from "app/_graphql/updateTicketMutation";
interface ArticleCreateDialogProps { interface ArticleCreateDialogProps {
ticketID: string; ticketID: string;
@ -30,7 +29,6 @@ export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
const [body, setBody] = useState(""); const [body, setBody] = useState("");
const backgroundColor = kind === "note" ? "#FFB620" : "#1982FC"; const backgroundColor = kind === "note" ? "#FFB620" : "#1982FC";
const color = kind === "note" ? "black" : "white"; const color = kind === "note" ? "black" : "white";
const { fetcher } = useSWRConfig();
const article = { const article = {
body, body,
type: kind, type: kind,
@ -42,15 +40,7 @@ export const ArticleCreateDialog: FC<ArticleCreateDialogProps> = ({
} }
const createArticle = async () => { const createArticle = async () => {
await fetcher({ await createTicketArticleAction(ticketID, article);
document: updateTicketMutation,
variables: {
ticketId: `gid://zammad/Ticket/${ticketID}`,
input: {
article,
},
},
});
closeDialog(); closeDialog();
setBody(""); setBody("");
}; };

View file

@ -1,12 +1,9 @@
"use client"; "use client";
import { FC, useState } from "react"; import { FC, useState, useEffect } from "react";
import useSWR from "swr"; import { getTicketAction, getTicketArticlesAction } from "app/_actions/tickets";
import { getTicketQuery } from "app/_graphql/getTicketQuery";
import { getTicketArticlesQuery } from "app/_graphql/getTicketArticlesQuery";
import { Grid, Box, Typography } from "@mui/material"; import { Grid, Box, Typography } from "@mui/material";
import { Button, fonts, colors } from "@link-stack/ui"; import { Button, fonts, colors } from "@link-stack/ui";
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,
@ -22,41 +19,46 @@ interface TicketDetailProps {
} }
export const TicketDetail: FC<TicketDetailProps> = ({ id }) => { export const TicketDetail: FC<TicketDetailProps> = ({ id }) => {
const [ticket, setTicket] = useState<any>(null);
const [ticketArticles, setTicketArticles] = useState<any>(null);
const { poppins, roboto } = fonts; const { poppins, roboto } = fonts;
const { veryLightGray, lightGray } = colors; 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(
{
document: getTicketQuery,
variables: { ticketId: `gid://zammad/Ticket/${id}` },
},
{ refreshInterval: 10000 },
);
const { data: ticketArticlesData, error: ticketArticlesError }: any = useSWR(
{
document: getTicketArticlesQuery,
variables: { ticketId: `gid://zammad/Ticket/${id}` },
},
{ refreshInterval: 2000 },
);
const { data: recentViewData, error: recentViewError }: any = useSWR({ useEffect(() => {
url: "/api/v1/recent_view", const fetchTicket = async () => {
method: "POST", const result = await getTicketAction(id);
body: JSON.stringify({ object: "Ticket", o_id: id }), setTicket(result);
}); };
fetchTicket();
const interval = setInterval(fetchTicket, 20000);
return () => clearInterval(interval);
}, [id]);
useEffect(() => {
const fetchTicketArticles = async () => {
const result = await getTicketArticlesAction(id);
setTicketArticles(result);
};
fetchTicketArticles();
const interval = setInterval(fetchTicketArticles, 2000);
return () => clearInterval(interval);
}, [id]);
const closeDialog = () => setDialogOpen(false); const closeDialog = () => setDialogOpen(false);
const ticket = ticketData?.ticket;
const ticketArticles = ticketArticlesData?.ticketArticles;
const firstArticle = ticketArticles?.edges[0]?.node; const firstArticle = ticketArticles?.edges[0]?.node;
const firstArticleKind = firstArticle?.type?.name ?? "phone"; const firstArticleKind = firstArticle?.type?.name ?? "phone";
const firstEmailSender = firstArticle?.from?.parsed?.[0]?.emailAddress ?? ""; const firstEmailSender = firstArticle?.from?.parsed?.[0]?.emailAddress ?? "";
const recipient = firstEmailSender; const recipient = firstEmailSender;
const shouldRender = const shouldRender = !!ticket && !!ticketArticles;
ticketData && !ticketError && ticketArticlesData && !ticketArticlesError;
return ( return (
shouldRender && ( shouldRender && (

View file

@ -5,57 +5,87 @@ import { Grid, Box, MenuItem } from "@mui/material";
import { useFormState } from "react-dom"; import { useFormState } from "react-dom";
import { Select, Button } from "@link-stack/ui"; import { Select, Button } from "@link-stack/ui";
import { MuiChipsInput } from "mui-chips-input"; import { MuiChipsInput } from "mui-chips-input";
import useSWR, { useSWRConfig } from "swr";
import { getTicketQuery } from "app/_graphql/getTicketQuery"; import { getTicketQuery } from "app/_graphql/getTicketQuery";
import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { updateTicketAction } from "app/_actions/tickets"; import {
updateTicketAction,
getTicketAction,
getTicketStatesAction,
getTicketPrioritiesAction,
getTicketTagsAction,
} from "app/_actions/tickets";
import { getAgentsAction } from "app/_actions/users";
import { getGroupsAction } from "app/_actions/groups";
interface TicketEditProps { interface TicketEditProps {
id: string; id: string;
} }
export const TicketEdit: FC<TicketEditProps> = ({ id }) => { export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
const [ticket, setTicket] = useState<any>();
const [ticketStates, setTicketStates] = useState<any>();
const [ticketPriorities, setTicketPriorities] = useState<any>();
const [groups, setGroups] = useState<any>();
const [tags, setTags] = useState<any>();
const [agents, setAgents] = useState<any>();
const selectedTags = []; const selectedTags = [];
const pendingVisible = false; const pendingVisible = false;
const pendingDate = new Date(); const pendingDate = new Date();
const handleDelete = () => { const handleDelete = () => {
console.info("You clicked the delete icon."); console.info("You clicked the delete icon.");
}; };
const restFetcher = (url: string) => fetch(url).then((r) => r.json());
const { data: groups } = useSWR("/api/v1/groups", restFetcher); const filteredStates = ticketStates?.filter(
const { data: users } = useSWR("/api/v1/users", restFetcher);
const { data: states } = useSWR("/api/v1/ticket_states", restFetcher);
const { data: priorities } = useSWR("/api/v1/ticket_priorities", restFetcher);
const { data: recent } = useSWR("/api/v1/recent_view", restFetcher);
// const { data: tags } = useSWR("/api/v1/tags", restFetcher);
const filteredStates = states?.filter(
(state: any) => !["new", "merged", "removed"].includes(state.name), (state: any) => !["new", "merged", "removed"].includes(state.name),
); );
const agents = users?.filter((user: any) => user.role_ids.includes(2)) ?? [];
const { fetcher } = useSWRConfig();
const { data: ticketData, error: ticketError }: any = useSWR(
{
document: getTicketQuery,
variables: { ticketId: `gid://zammad/Ticket/${id}` },
},
{ refreshInterval: 10000 },
);
useEffect(() => { useEffect(() => {
const ticket = ticketData?.ticket; const fetchAgents = async () => {
if (ticket) { const result = await getAgentsAction();
const groupID = ticket.group.id?.split("/").pop(); console.log({ result });
// setSelectedGroup(groupID); setAgents(result);
const ownerID = ticket.owner.id?.split("/").pop(); };
// setSelectedOwner(ownerID);
const priorityID = ticket.priority.id?.split("/").pop(); const fetchGroups = async () => {
// setSelectedPriority(priorityID); const result = await getGroupsAction();
const stateID = ticket.state.id?.split("/").pop(); console.log({ result });
// setSelectedState(stateID); setGroups(result);
// setSelectedTags(ticket.tags); };
}
}, [ticketData, ticketError]); const fetchTicketStates = async () => {
const result = await getTicketStatesAction();
console.log({ result });
setTicketStates(result);
};
const fetchTicketPriorities = async () => {
const result = await getTicketPrioritiesAction();
console.log({ result });
setTicketPriorities(result);
};
const fetchTicketTags = async () => {
const result = await getTicketTagsAction();
console.log({ result });
setTags(result);
};
fetchTicketStates();
fetchTicketPriorities();
fetchTicketTags();
fetchAgents();
fetchGroups();
}, []);
useEffect(() => {
const fetchTicket = async () => {
const result = await getTicketAction(id);
console.log({ result });
setTicket(result);
};
fetchTicket();
}, []);
/* /*
useEffect(() => { useEffect(() => {
@ -97,10 +127,10 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
errors: [], errors: [],
values: { values: {
customer: "", customer: "",
group: "", group: ticket.group.id?.split("/").pop(),
owner: "", owner: ticket.owner.id?.split("/").pop(),
priority: "", priority: ticket.priority.id?.split("/").pop(),
state: "", state: ticket.state.id?.split("/").pop(),
tags: [], tags: [],
title: "", title: "",
article: { article: {
@ -114,7 +144,7 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
updateTicketAction, updateTicketAction,
initialState, initialState,
); );
const shouldRender = ticketData && !ticketError; const shouldRender = !!ticket;
return ( return (
shouldRender && ( shouldRender && (
@ -220,7 +250,7 @@ export const TicketEdit: FC<TicketEditProps> = ({ id }) => {
label="Priority" label="Priority"
formState={formState} formState={formState}
getOptions={() => getOptions={() =>
priorities?.map((priority: any) => ({ ticketPriorities?.map((priority: any) => ({
value: priority.id, value: priority.id,
label: priority.name, label: priority.name,
})) ?? [] })) ?? []

View file

@ -1,62 +0,0 @@
type PageProps = {
params: {
id: string;
};
};
export default function Page({ params: { id } }: PageProps) {
return <div>Page</div>;
}
/*
import { GetServerSideProps, GetServerSidePropsContext } from "next";
import Head from "next/head";
import useSWR from "swr";
import { NextPage } from "next";
import { Grid } from "@mui/material";
import { TicketDetail } from "../_components/TicketDetail";
import { TicketEdit } from "../_components/TicketEdit";
import { getTicketQuery } from "../_graphql/getTicketQuery";
type TicketProps = {
id: string;
};
const Ticket: NextPage<TicketProps> = ({ id }) => {
const { data: ticketData, error: ticketError }: any = useSWR(
{
document: getTicketQuery,
variables: { ticketId: `gid://zammad/Ticket/${id}` },
},
{ refreshInterval: 100000 }
);
const shouldRender = !ticketError && ticketData;
return (
<>
{shouldRender && (
<Grid container spacing={0} sx={{ height: "100vh" }} direction="row">
<Grid item sx={{ height: "100vh" }} xs={9}>
<TicketDetail ticket={ticketData.ticket} />
</Grid>
<Grid item xs={3} sx={{ height: "100vh" }}>
<TicketEdit ticket={ticketData.ticket} />
</Grid>
</Grid>
)}
{ticketError && <div>{ticketError.toString()}</div>}
</>
);
};
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
) => {
const { id } = context.query;
return { props: { id } };
};
export default Ticket;
*/

View file

@ -0,0 +1,16 @@
"use server";
import { executeREST } from "app/_lib/zammad";
export const getGroupsAction = async () => {
const groups = await executeREST({
path: "/api/v1/groups",
});
const allGroups = groups ?? [];
const formattedGroups = allGroups.map((group: any) => ({
label: group.name,
value: `gid://zammad/Group/${group.id}`,
}));
return formattedGroups;
};

View file

@ -0,0 +1,82 @@
"use server";
import { executeGraphQL } from "app/_lib/zammad";
import { getTicketOverviewCountsQuery } from "app/_graphql/getTicketOverviewCountsQuery";
import { getTicketsByOverviewQuery } from "app/_graphql/getTicketsByOverviewQuery";
export const getOverviewTicketCountsAction = async () => {
const countResult = await executeGraphQL({
query: getTicketOverviewCountsQuery,
});
const overviews = countResult?.ticketOverviews?.edges;
const counts = overviews?.reduce((acc: any, overview: any) => {
acc[overview.node.name] = overview.node.ticketCount;
return acc;
});
return counts;
};
export const getOverviewTicketsAction = async (name: string) => {
let tickets = [];
let error = null;
if (name === "Recent") {
const response = await fetch(
`${process.env.ZAMMAD_URL}/api/v1/recent_view`,
);
const recent = await response.json();
console.log({ recent });
for (const rec of recent) {
const res = await fetch(
`${process.env.ZAMMAD_URL}/api/v1/tickets/${rec.o_id}`,
);
const tkt = await res.json();
tickets.push({
...tkt,
internalId: tkt.id,
createdAt: tkt.created_at,
updatedAt: tkt.updated_at,
});
}
} else {
const overviewLookup = {
Assigned: "My Assigned Tickets",
Open: "Open Tickets",
Urgent: "Escalated Tickets",
Unassigned: "Unassigned & Open Tickets",
};
const fullName = overviewLookup[name];
const countResult = await executeGraphQL({
query: getTicketOverviewCountsQuery,
});
const overviewID = countResult?.ticketOverviews?.edges?.find(
(overview: any) => overview.node.name === fullName,
)?.node?.id;
console.log({ overviewID });
const ticketsResult = await executeGraphQL({
query: getTicketsByOverviewQuery,
variables: { overviewId: overviewID, pageSize: 250 },
});
const edges = ticketsResult?.ticketsByOverview?.edges;
if (edges) {
tickets = edges.map((edge: any) => edge.node);
}
}
const sortedTickets = tickets.sort((a: any, b: any) => {
if (a.internalId < b.internalId) {
return 1;
}
if (a.internalId > b.internalId) {
return -1;
}
return 0;
});
return { tickets: sortedTickets, error };
};

View file

@ -0,0 +1,12 @@
"use server";
import { executeGraphQL } from "app/_lib/zammad";
import { searchQuery } from "@/app/_graphql/searchQuery";
export const searchAllAction = async (query: string, limit: number) => {
const result = await executeGraphQL({
query: searchQuery,
variables: { search: query, limit },
});
return result?.search;
};

View file

@ -1,56 +1,78 @@
"use server"; "use server";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { getTicketQuery } from "app/_graphql/getTicketQuery";
import { getTicketArticlesQuery } from "app/_graphql/getTicketArticlesQuery";
import { createTicketMutation } from "app/_graphql/createTicketMutation"; import { createTicketMutation } from "app/_graphql/createTicketMutation";
import { updateTicketMutation } from "app/_graphql/updateTicketMutation"; import { updateTicketMutation } from "app/_graphql/updateTicketMutation";
import { updateTagsMutation } from "app/_graphql/updateTagsMutation"; import { updateTagsMutation } from "app/_graphql/updateTagsMutation";
// import { executeMutation } from "app/_lib/graphql"; import { executeGraphQL, executeREST } from "app/_lib/zammad";
// import { AddAssetMutation } from "../_graphql/AddAssetMutation";
export const createTicketAction = async ( export const createTicketAction = async (
currentState: any, currentState: any,
formData: FormData, formData: FormData,
) => { ) => {
/*
const createTicket = async () => {
await fetcher({
document: createTicketMutation,
variables: {
input: {
ticket,
},
},
});
closeDialog();
setBody("");
};
try { try {
const ticket = { const ticket = {
groupId: formData.get("groupId"),
customerId: `gid://zammad/User/3`, // { email: formData.get("customerId") },
title: formData.get("title"), title: formData.get("title"),
article: {
internal: true,
body: formData.get("details"),
},
}; };
const result = await executeMutation({ console.log({ ticket });
project,
mutation: AddAssetMutation, const result = await executeGraphQL({
query: createTicketMutation,
variables: { variables: {
input: asset, input: ticket,
}, },
}); });
revalidatePath(`/${project}/assets`); console.log({ result });
return { return {
...currentState, ...currentState,
values: { ...asset, ...result.addAsset, project }, values: ticket,
success: true, success: true,
}; };
} catch (e: any) { } catch (e: any) {
return { success: false, message: e?.message ?? "Unknown error" }; console.log({ e });
return {
success: false,
values: {},
message: e?.message ?? "Unknown error",
};
}
};
export const createTicketArticleAction = async (
ticketID: string,
article: Record<string, any>,
) => {
try {
const result = await executeGraphQL({
query: updateTicketMutation,
variables: {
ticketId: `gid://zammad/Ticket/${ticketID}`,
input: { article },
},
});
console.log({ result });
return {
result,
success: true,
};
} catch (e: any) {
console.log({ e });
return {
success: false,
message: e?.message ?? "Unknown error",
};
} }
*/
}; };
export const updateTicketAction = async ( export const updateTicketAction = async (
@ -85,6 +107,24 @@ export const updateTicketAction = async (
*/ */
}; };
export const getTicketAction = async (id: string) => {
const ticketData = await executeGraphQL({
query: getTicketQuery,
variables: { ticketId: `gid://zammad/Ticket/${id}` },
});
return ticketData?.ticket;
};
export const getTicketArticlesAction = async (id: string) => {
const ticketData = await executeGraphQL({
query: getTicketArticlesQuery,
variables: { ticketId: `gid://zammad/Ticket/${id}` },
});
return ticketData?.ticketArticles;
};
export const updateTicketTagsAction = async ( export const updateTicketTagsAction = async (
currentState: any, currentState: any,
formData: FormData, formData: FormData,
@ -116,3 +156,27 @@ export const updateTicketTagsAction = async (
} }
*/ */
}; };
export const getTicketStatesAction = async () => {
const states = await executeREST({
path: "/api/v1/ticket_states",
});
return states;
};
export const getTicketTagsAction = async () => {
const states = await executeREST({
path: "/api/v1/tags",
});
return states;
};
export const getTicketPrioritiesAction = async () => {
const priorities = await executeREST({
path: "/api/v1/ticket_priorities",
});
return priorities;
};

View file

@ -1,3 +1,50 @@
"use server"; "use server";
const fetchUsersAction = async () => {}; import { executeREST } from "app/_lib/zammad";
export const getAgentsAction = async () => {
const users = await executeREST({
path: "/api/v1/users",
});
const agents = users?.filter((user: any) => user.role_ids.includes(2)) ?? [];
const formattedAgents = agents
.map((agent: any) => ({
label: agent.login,
value: `gid://zammad/User/${agent.id}`,
}))
.sort((a: any, b: any) => a.label.localeCompare(b.label));
return formattedAgents;
};
export const getCustomersAction = async () => {
const users = await executeREST({
path: "/api/v1/users",
});
console.log({ users });
const customers =
users?.filter((user: any) => user.role_ids.includes(3)) ?? [];
const formattedCustomers = customers
.map((customer: any) => ({
label: customer.login,
value: `gid://zammad/User/${customer.id}`,
}))
.sort((a: any, b: any) => a.label.localeCompare(b.label));
return formattedCustomers;
};
export const getUsersAction = async () => {
const users = await executeREST({
path: "/api/v1/users",
});
console.log({ users });
const formattedUsers = users
.map((customer: any) => ({
label: customer.login,
value: `gid://zammad/User/${customer.id}`,
}))
.sort((a: any, b: any) => a.label.localeCompare(b.label));
return formattedUsers;
};

View file

@ -0,0 +1,23 @@
"use client";
import { FC, PropsWithChildren, useEffect } from "react";
import { useSession } from "next-auth/react";
export const CSRFProvider: FC<PropsWithChildren> = ({ children }) => {
const { data: session, status, update } = useSession();
useEffect(() => {
const interval = setInterval(async () => {
if (status === "authenticated") {
const response = await fetch("/api/v1/users/me");
const token = response.headers.get("CSRF-Token");
update({ csrfToken: token });
console.log({ token });
}
}, 30000);
return () => clearInterval(interval);
}, [session, status, update]);
return children;
};

View file

@ -5,109 +5,30 @@ import { CssBaseline } from "@mui/material";
import { CookiesProvider } from "react-cookie"; import { CookiesProvider } from "react-cookie";
import { SessionProvider } from "next-auth/react"; import { SessionProvider } from "next-auth/react";
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir"; import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
import { SWRConfig } from "swr";
import { GraphQLClient } from "graphql-request";
import { I18n } from "react-polyglot"; import { I18n } from "react-polyglot";
import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFnsV3"; import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFnsV3";
import { LocalizationProvider } from "@mui/x-date-pickers-pro"; import { LocalizationProvider } from "@mui/x-date-pickers-pro";
import { LicenseInfo } from "@mui/x-license"; import { LicenseInfo } from "@mui/x-license";
import { locales, LeafcutterProvider } from "@link-stack/leafcutter-ui"; import { locales, LeafcutterProvider } from "@link-stack/leafcutter-ui";
import { CSRFProvider } from "./CSRFProvider";
LicenseInfo.setLicenseKey( LicenseInfo.setLicenseKey(
"c787ac6613c5f2aa0494c4285fe3e9f2Tz04OTY1NyxFPTE3NDYzNDE0ODkwMDAsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI=", "c787ac6613c5f2aa0494c4285fe3e9f2Tz04OTY1NyxFPTE3NDYzNDE0ODkwMDAsUz1wcm8sTE09c3Vic2NyaXB0aW9uLEtWPTI=",
); );
export const MultiProvider: FC<PropsWithChildren> = ({ children }) => { export const MultiProvider: FC<PropsWithChildren> = ({ children }) => {
const [csrfToken, setCsrfToken] = useState("");
const origin = const origin =
typeof window !== "undefined" && window.location.origin typeof window !== "undefined" && window.location.origin
? window.location.origin ? window.location.origin
: null; : null;
const client = new GraphQLClient(`${origin}/zammad/graphql`);
const messages: any = { en: locales.en, fr: locales.fr }; const messages: any = { en: locales.en, fr: locales.fr };
const locale = "en"; const locale = "en";
const fetchAndCheckAuth = async ({
document,
variables,
url,
method,
body,
}: any) => {
const requestHeaders = {
"Content-Type": "application/json",
Accept: "application/json",
"X-CSRF-Token": csrfToken,
};
let responseData = null;
let responseHeaders = new Headers();
let responseStatus = null;
if (document) {
const { data, headers, status } = await client.rawRequest(
document,
variables,
requestHeaders,
);
responseData = data;
responseHeaders = headers;
responseStatus = status;
} else {
const res = await fetch(url, {
method,
headers: requestHeaders,
body,
});
responseData = await res.json();
responseHeaders = res.headers;
responseStatus = res.status;
}
if (responseStatus !== 200) {
const res = await fetch("/zammad/auth/sso", {
method: "GET",
redirect: "manual",
});
console.log({ checkAuth: res });
return null;
}
const token = responseHeaders.get("CSRF-Token");
setCsrfToken(token);
return responseData;
};
const multiFetcher = async ({
document,
variables,
url,
method,
body,
}: any) => {
let checks = 0;
let data = null;
while (!data && checks < 2) {
data = await fetchAndCheckAuth({
document,
variables,
url,
method,
body,
});
checks++;
}
return data;
};
return ( return (
<NextAppDirEmotionCacheProvider options={{ key: "css" }}> <NextAppDirEmotionCacheProvider options={{ key: "css" }}>
<CssBaseline /> <CssBaseline />
<SWRConfig value={{ fetcher: multiFetcher }}> <SessionProvider>
<SessionProvider> <CSRFProvider>
<CookiesProvider> <CookiesProvider>
<LocalizationProvider dateAdapter={AdapterDateFns}> <LocalizationProvider dateAdapter={AdapterDateFns}>
<I18n locale={locale} messages={messages[locale]}> <I18n locale={locale} messages={messages[locale]}>
@ -115,8 +36,8 @@ export const MultiProvider: FC<PropsWithChildren> = ({ children }) => {
</I18n> </I18n>
</LocalizationProvider> </LocalizationProvider>
</CookiesProvider> </CookiesProvider>
</SessionProvider> </CSRFProvider>
</SWRConfig> </SessionProvider>
</NextAppDirEmotionCacheProvider> </NextAppDirEmotionCacheProvider>
); );
}; };

View file

@ -1,7 +1,4 @@
import { gql } from "graphql-request"; export const createTicketMutation = `mutation CreateTicket($input: TicketCreateInput!) {
export const createTicketMutation = gql`
mutation CreateTicket($ticketId: ID!, $input: TicketCreateInput!) {
ticketCreate(input: $input) { ticketCreate(input: $input) {
ticket { ticket {
id id

View file

@ -121,16 +121,23 @@ export const authOptions: NextAuthOptions = {
session.user.roles = token.roles ?? []; session.user.roles = token.roles ?? [];
// @ts-ignore // @ts-ignore
session.user.leafcutter = token.leafcutter; // remove session.user.leafcutter = token.leafcutter; // remove
// @ts-ignore
session.user.zammadCsrfToken = token.zammadCsrfToken;
return session; return session;
}, },
jwt: async ({ token, user, account, profile, trigger }) => { jwt: async ({ token, user, account, profile, trigger, session }) => {
if (user) { if (user) {
token.roles = (await getUserRoles(user.email)) ?? []; token.roles = (await getUserRoles(user.email)) ?? [];
} }
if (session && trigger === "update") {
token.zammadCsrfToken = session.csrfToken;
}
return token; return token;
}, },
}, },
} };
export const getServerSession = ( export const getServerSession = (
...args: ...args:

View file

@ -0,0 +1,68 @@
import { getServerSession } from "app/_lib/authentication";
import { cookies } from "next/headers";
const getHeaders = async () => {
const allCookies = cookies().getAll();
const session = await getServerSession();
const headers = {
"Content-Type": "application/json",
Accept: "application/json",
// @ts-ignore
"X-CSRF-Token": session.user.zammadCsrfToken,
Cookie: allCookies
.map((cookie: any) => `${cookie.name}=${cookie.value}`)
.join("; "),
};
return headers;
};
interface ExecuteGraphQLOptions {
query: string;
variables?: Record<string, any>;
}
export const executeGraphQL = async ({
query,
variables,
}: ExecuteGraphQLOptions): Promise<any> => {
const headers = await getHeaders();
const endpoint = `${process.env.ZAMMAD_URL}/graphql`;
const body = JSON.stringify({ query, ...(variables && { variables }) });
const result = await fetch(endpoint, {
headers,
body,
method: "POST",
next: { revalidate: 0 },
});
const { data, errors } = await result.json();
if (result.status !== 200) {
throw new Error(`Error: ${result.status}`);
} else if (errors) {
throw new Error(`Error: ${JSON.stringify(errors)}`);
}
return data;
};
interface ExecuteRESTOptions {
path: string;
}
export const executeREST = async ({
path,
}: ExecuteRESTOptions): Promise<any> => {
const headers = await getHeaders();
const result = await fetch(`${process.env.ZAMMAD_URL}${path}`, {
method: "GET",
headers,
});
if (result.status !== 200) {
throw new Error(`Error: ${result.status}`);
}
return await result.json();
};

View file

@ -10,11 +10,12 @@ const rewriteURL = (
const destinationURL = request.url.replace(originBaseURL, destinationBaseURL); const destinationURL = request.url.replace(originBaseURL, destinationBaseURL);
console.log(`Rewriting ${request.url} to ${destinationURL}`); console.log(`Rewriting ${request.url} to ${destinationURL}`);
const requestHeaders = new Headers(request.headers); const requestHeaders = new Headers(request.headers);
// console.log({ beforeHeaders: requestHeaders });
for (const [key, value] of Object.entries(headers)) { for (const [key, value] of Object.entries(headers)) {
requestHeaders.set(key, value as string); requestHeaders.set(key, value as string);
} }
requestHeaders.delete("connection"); requestHeaders.delete("connection");
// console.log({ afterHeaders: requestHeaders });
return NextResponse.rewrite(new URL(destinationURL), { return NextResponse.rewrite(new URL(destinationURL), {
request: { headers: requestHeaders }, request: { headers: requestHeaders },
}); });
@ -35,7 +36,7 @@ const checkRewrites = async (request: NextRequestWithAuth) => {
const leafcutterRole = roles.includes("admin") const leafcutterRole = roles.includes("admin")
? "leafcutter_admin" ? "leafcutter_admin"
: "leafcutter_user"; : "leafcutter_user";
headers["x-forwarded-roles"] = "admin"; // leafcutterRole; headers["x-forwarded-roles"] = leafcutterRole;
// headers["secruitytenant"] = "global"; // headers["secruitytenant"] = "global";
// headers["x-forwarded-for"] = 'link'; // headers["x-forwarded-for"] = 'link';

View file

@ -1,5 +1,11 @@
import { ServiceConfig } from "../lib/service"; import { ServiceConfig } from "../lib/service";
const getRoles = async () => [
{ value: "admin", label: "Admin" },
{ value: "user", label: "User" },
{ value: "none", label: "None" },
];
export const usersConfig: ServiceConfig = { export const usersConfig: ServiceConfig = {
entity: "users", entity: "users",
table: "User", table: "User",
@ -17,6 +23,13 @@ export const usersConfig: ServiceConfig = {
required: true, required: true,
size: 12, size: 12,
}, },
{
name: "role",
label: "Role",
required: true,
getOptions: getRoles,
size: 12,
},
], ],
updateFields: [ updateFields: [
{ name: "name", label: "Name", required: true, size: 12 }, { name: "name", label: "Name", required: true, size: 12 },
@ -47,8 +60,13 @@ export const usersConfig: ServiceConfig = {
flex: 2, flex: 2,
}, },
{ {
field: "updatedAt", field: "role",
headerName: "Updated At", headerName: "Role",
flex: 1,
},
{
field: "createdAt",
headerName: "Created At",
valueGetter: (value: any) => new Date(value).toLocaleString(), valueGetter: (value: any) => new Date(value).toLocaleString(),
flex: 1, flex: 1,
}, },

View file

@ -7,6 +7,7 @@ type AutocompleteProps = {
label: string; label: string;
options: any[]; options: any[];
formState: Record<string, any>; formState: Record<string, any>;
updateFormState: (name: string, value: any) => void;
disabled?: boolean; disabled?: boolean;
required?: boolean; required?: boolean;
}; };
@ -16,18 +17,21 @@ export const Autocomplete: FC<AutocompleteProps> = ({
label, label,
options, options,
formState, formState,
updateFormState,
disabled = false, disabled = false,
required = false, required = false,
}) => ( }) => (
<AutocompleteInternal <AutocompleteInternal
disablePortal disablePortal
options={options} options={options}
defaultValue={formState.values[name]} value={formState.values[name] || ""}
onChange={(e: any) => updateFormState?.(name, e.target.id)}
fullWidth fullWidth
size="small" size="small"
renderInput={(params) => ( renderInput={(params) => (
<TextField <TextField
{...params} {...params}
name={name}
label={label} label={label}
disabled={disabled} disabled={disabled}
required={required} required={required}