Ticket list updates

This commit is contained in:
Darren Clarke 2023-06-07 08:02:29 +00:00 committed by GitHub
parent 6a85c644dc
commit dce765033d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 397 additions and 133 deletions

View file

@ -1,4 +1,5 @@
import { FC, useState } from "react"; import { FC, useState } from "react";
import useSWR from "swr";
import { import {
Box, Box,
Grid, Grid,
@ -19,12 +20,14 @@ import {
Cottage as CottageIcon, Cottage as CottageIcon,
Settings as SettingsIcon, Settings as SettingsIcon,
ExpandCircleDown as ExpandCircleDownIcon, ExpandCircleDown as ExpandCircleDownIcon,
Dvr as DvrIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; 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 } from "next-auth/react"; import { useSession, signOut } from "next-auth/react";
import { getTicketOverviewCountsQuery } from "graphql/getTicketOverviewCountsQuery";
const openWidth = 270; const openWidth = 270;
const closedWidth = 100; const closedWidth = 100;
@ -38,8 +41,9 @@ const MenuItem = ({
selected = false, selected = false,
open = true, open = true,
badge, badge,
target = "_self",
}: any) => ( }: any) => (
<Link href={href}> <Link href={href} target={target}>
<ListItemButton <ListItemButton
sx={{ sx={{
p: 0, p: 0,
@ -124,7 +128,7 @@ const MenuItem = ({
} }
/> />
)} )}
{badge && ( {badge && badge > 0 ? (
<ListItemSecondaryAction> <ListItemSecondaryAction>
<Typography <Typography
color="textSecondary" color="textSecondary"
@ -142,7 +146,7 @@ const MenuItem = ({
{badge} {badge}
</Typography> </Typography>
</ListItemSecondaryAction> </ListItemSecondaryAction>
)} ) : null}
</ListItemButton> </ListItemButton>
</Link> </Link>
); );
@ -153,9 +157,29 @@ interface SidebarProps {
} }
export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => { export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
const { pathname } = useRouter(); const router = useRouter();
const { pathname } = router;
const { data: session } = useSession(); const { data: session } = useSession();
const username = session?.user?.name || "User"; const username = session?.user?.name || "User";
const { data: overviewData, error: overviewError }: any = useSWR(
{
document: getTicketOverviewCountsQuery,
},
{ refreshInterval: 10000 }
);
const findOverviewCountByID = (id: number) =>
overviewData?.ticketOverviews?.edges?.find((overview: any) =>
overview.node.id.endsWith(`/${id}`)
)?.node?.ticketCount ?? 0;
const assignedCount = findOverviewCountByID(1);
const urgentCount = findOverviewCountByID(7);
const pendingCount = findOverviewCountByID(3);
const unassignedCount = findOverviewCountByID(2);
console.log({ assignedCount, urgentCount, pendingCount, unassignedCount });
const logout = () => {
signOut({ callbackUrl: "/login" });
};
return ( return (
<Drawer <Drawer
@ -335,13 +359,12 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
/> />
<MenuItem <MenuItem
name="Tickets" name="Tickets"
href="/tickets/3" href="/tickets/assigned"
Icon={FeaturedPlayListIcon} Icon={FeaturedPlayListIcon}
selected={pathname.startsWith("/tickets")} selected={pathname.startsWith("/tickets")}
iconSize={20} iconSize={20}
open={open} open={open}
/> />
{/*
<Collapse <Collapse
in={pathname.startsWith("/tickets")} in={pathname.startsWith("/tickets")}
timeout="auto" timeout="auto"
@ -355,7 +378,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
Icon={FeaturedPlayListIcon} Icon={FeaturedPlayListIcon}
iconSize={0} iconSize={0}
selected={pathname.endsWith("/tickets/assigned")} selected={pathname.endsWith("/tickets/assigned")}
badge={3} badge={assignedCount}
open={open} open={open}
/> />
<MenuItem <MenuItem
@ -364,7 +387,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
Icon={FeaturedPlayListIcon} Icon={FeaturedPlayListIcon}
iconSize={0} iconSize={0}
selected={pathname.endsWith("/tickets/urgent")} selected={pathname.endsWith("/tickets/urgent")}
badge={1} badge={urgentCount}
open={open} open={open}
/> />
<MenuItem <MenuItem
@ -373,7 +396,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
Icon={FeaturedPlayListIcon} Icon={FeaturedPlayListIcon}
iconSize={0} iconSize={0}
selected={pathname.endsWith("/tickets/pending")} selected={pathname.endsWith("/tickets/pending")}
badge={9} badge={pendingCount}
open={open} open={open}
/> />
<MenuItem <MenuItem
@ -381,21 +404,12 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
href="/tickets/unassigned" href="/tickets/unassigned"
Icon={FeaturedPlayListIcon} Icon={FeaturedPlayListIcon}
iconSize={0} iconSize={0}
selected={pathname.endsWith("/tickets/unnassigned")} selected={pathname.endsWith("/tickets/unassigned")}
badge={27} badge={unassignedCount}
open={open}
/>
<MenuItem
name="New Ticket UI"
href="/tickets/3"
Icon={SettingsIcon}
iconSize={0}
selected={pathname.endsWith("/tickets/3")}
open={open} open={open}
/> />
</List> </List>
</Collapse> </Collapse>
*/ }
<MenuItem <MenuItem
name="Knowledge Base" name="Knowledge Base"
href="/knowledge" href="/knowledge"
@ -419,7 +433,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
onClick={undefined} onClick={undefined}
> >
<List component="div" disablePadding> <List component="div" disablePadding>
{/* {/*
<MenuItem <MenuItem
name="Dashboard" name="Dashboard"
href="/leafcutter" href="/leafcutter"
@ -510,13 +524,21 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
/> />
</List> </List>
</Collapse> </Collapse>
<MenuItem
name="Zammad Interface"
href="/proxy/zammad"
Icon={DvrIcon}
iconSize={20}
open={open}
target="_blank"
/>
<MenuItem <MenuItem
name="Logout" name="Logout"
href="/logout" href="/logout"
Icon={LogoutIcon} Icon={LogoutIcon}
iconSize={20} iconSize={20}
open={open} open={open}
onClick={logout}
/> />
</List> </List>
</Grid> </Grid>

View file

@ -0,0 +1,84 @@
import { FC } from "react";
import { Box } from "@mui/material";
import {
DataGridPro,
GridColDef,
GridColumnVisibilityModel,
GridRowSelectionModel,
} from "@mui/x-data-grid-pro";
import { useCookies } from "react-cookie";
interface StyledDataGridProps {
name: string;
columns: GridColDef[];
rows: any[];
// eslint-disable-next-line no-unused-vars
onRowClick?: (row: any) => void;
height?: string;
selectedRows?: GridRowSelectionModel;
// eslint-disable-next-line no-unused-vars
setSelectedRows?: (rows: GridRowSelectionModel) => void;
}
export const StyledDataGrid: FC<StyledDataGridProps> = ({
name,
columns,
rows,
onRowClick,
height = "calc(100vh - 20px)",
selectedRows,
setSelectedRows,
}) => {
const cookieName = `${name}DataGridColumnState`;
const [cookies, setCookie] = useCookies([cookieName]);
const handleColumnVisibilityChange = (model: GridColumnVisibilityModel) =>
setCookie(cookieName, model, { path: "/" });
return (
<Box
sx={{
backgroundColor: "#ddd",
border: 0,
width: "100%",
height,
".MuiDataGrid-row:nth-of-type(1n)": {
backgroundColor: "#f3f3f3",
},
".MuiDataGrid-row:nth-of-type(2n)": {
backgroundColor: "#fff",
},
".MuiDataGrid-columnHeaderTitle": {
color: "#333",
fontWeight: "bold",
fontSize: 11,
margin: "0 auto",
},
".MuiDataGrid-columnHeader": {
backgroundColor: "#ccc",
border: 0,
borderBottom: "3px solid #ddd",
},
}}
>
<DataGridPro
rows={rows}
columns={columns}
density="compact"
hideFooter
sx={{ height }}
rowBuffer={30}
checkboxSelection={!!setSelectedRows}
onRowSelectionModelChange={setSelectedRows}
rowSelectionModel={selectedRows}
rowHeight={46}
scrollbarSize={0}
disableVirtualization
columnVisibilityModel={
(cookies[cookieName] as GridColumnVisibilityModel) ?? undefined
}
onColumnVisibilityModelChange={handleColumnVisibilityChange}
onRowClick={onRowClick}
/>
</Box>
);
};

View file

@ -0,0 +1,71 @@
import { FC } from "react";
import { Grid, Box } from "@mui/material";
import { GridColDef } from "@mui/x-data-grid-pro";
import { StyledDataGrid } from "./StyledDataGrid";
import { typography } from "styles/theme";
import { useRouter } from "next/router";
interface TicketListProps {
title: string;
tickets: any;
}
export const TicketList: FC<TicketListProps> = ({ title, tickets }) => {
const router = useRouter();
let gridColumns: GridColDef[] = [
{
field: "number",
headerName: "Number",
flex: 1,
},
{
field: "title",
headerName: "Title",
flex: 1,
},
{
field: "customer",
headerName: "Sender",
valueGetter: (params) => params.row?.customer?.fullname,
flex: 1,
},
{
field: "group",
headerName: "Group",
valueGetter: (params) => params.row?.group?.name,
flex: 1,
},
];
console.log({ tickets });
const rowClick = ({ row }) => {
router.push(`/tickets/${row.internalId}`);
};
return (
<Box sx={{ height: "100vh", backgroundColor: "#ddd", p: 3 }}>
<Grid container direction="column">
<Grid item>
<Box
sx={{
backgroundColor: "#ddd",
px: "8px",
pb: "16px",
...typography.h4,
fontSize: 24,
}}
>
{title}
</Box>
</Grid>
<Grid item>
<StyledDataGrid
name={title}
columns={gridColumns}
rows={tickets}
onRowClick={rowClick}
/>
</Grid>
</Grid>
</Box>
);
};

View file

@ -0,0 +1,19 @@
import { gql } from 'graphql-request';
export const getTicketOverviewCountsQuery = gql`
query ticketOverviewTicketCount {
ticketOverviews {
edges {
node {
id
name
ticketCount
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
}
}`;

View file

@ -0,0 +1,60 @@
import { gql } from 'graphql-request';
export const getTicketsByOverviewQuery = gql`
query ticketsByOverview($overviewId: ID!, $orderBy: String, $orderDirection: EnumOrderDirection, $cursor: String, $showPriority: Boolean = false, $showUpdatedBy: Boolean = false, $pageSize: Int = 10) {
ticketsByOverview(
overviewId: $overviewId
orderBy: $orderBy
orderDirection: $orderDirection
after: $cursor
first: $pageSize
) {
totalCount
edges {
node {
id
internalId
number
title
createdAt
updatedAt
updatedBy @include(if: $showUpdatedBy) {
id
fullname
}
customer {
id
firstname
lastname
fullname
}
organization {
id
name
}
state {
id
name
stateType {
name
}
}
group {
id
name
}
priority @include(if: $showPriority) {
id
name
uiColor
defaultCreate
}
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
}
}`;

View file

@ -5,17 +5,18 @@ const nextConfig = {
linkURL: process.env.LINK_URL, linkURL: process.env.LINK_URL,
leafcutterURL: process.env.LEAFCUTTER_URL, leafcutterURL: process.env.LEAFCUTTER_URL,
metamigoURL: process.env.METAMIGO_URL, metamigoURL: process.env.METAMIGO_URL,
muiLicenseKey: process.env.MUI_LICENSE_KEY,
}, },
async rewrites() { async rewrites() {
return { return {
fallback: [ fallback: [
{ {
source: '/:path*', source: "/:path*",
destination: `/proxy/zammad/:path*`, destination: `/proxy/zammad/:path*`,
}, },
], ],
} };
} },
}; };
module.exports = nextConfig; module.exports = nextConfig;

View file

@ -16,8 +16,10 @@ import "styles/global.css";
import { LicenseInfo } from "@mui/x-data-grid-pro"; import { LicenseInfo } from "@mui/x-data-grid-pro";
import { SWRConfig } from "swr"; import { SWRConfig } from "swr";
import { GraphQLClient } from "graphql-request"; import { GraphQLClient } from "graphql-request";
import getConfig from "next/config";
LicenseInfo.setLicenseKey(process.env.MUI_LICENSE_KEY); const { publicRuntimeConfig } = getConfig();
LicenseInfo.setLicenseKey(publicRuntimeConfig.muiLicenseKey);
const clientSideEmotionCache: any = createEmotionCache(); const clientSideEmotionCache: any = createEmotionCache();

View file

@ -1,31 +1,32 @@
import { FC } from "react";
import Head from "next/head"; import Head from "next/head";
import { Grid } from "@mui/material"; import useSWR from "swr";
import { NextPage } from "next";
import { Layout } from "components/Layout"; import { Layout } from "components/Layout";
import { ZammadWrapper } from "components/ZammadWrapper"; import { TicketList } from "components/TicketList";
import { getTicketsByOverviewQuery } from "graphql/getTicketsByOverviewQuery";
const Assigned: FC = () => ( const Assigned: NextPage = () => {
const { data: ticketData, error: ticketError }: any = useSWR(
{
document: getTicketsByOverviewQuery,
variables: { overviewId: "gid://zammad/Overview/1" },
},
{ refreshInterval: 10000 }
);
const shouldRender = !ticketError && ticketData;
const tickets =
ticketData?.ticketsByOverview?.edges.map((edge: any) => edge.node) || [];
return (
<Layout> <Layout>
<Head> <Head>
<title>Link Shell</title> <title>Link Shell Assigned Tickets</title>
</Head> </Head>
<Grid {shouldRender && <TicketList title="Assigned" tickets={tickets} />}
container {ticketError && <div>{ticketError.toString()}</div>}
spacing={0}
sx={{ height: "100%", width: "100%" }}
direction="column"
>
<Grid
item
sx={{
height: "100%",
width: "100%",
}}
>
<ZammadWrapper path="/#ticket/view/my_assigned" />
</Grid>
</Grid>
</Layout> </Layout>
); );
};
export default Assigned; export default Assigned;

View file

@ -1,31 +1,32 @@
import { FC } from "react";
import Head from "next/head"; import Head from "next/head";
import { Grid } from "@mui/material"; import useSWR from "swr";
import { NextPage } from "next";
import { Layout } from "components/Layout"; import { Layout } from "components/Layout";
import { ZammadWrapper } from "components/ZammadWrapper"; import { TicketList } from "components/TicketList";
import { getTicketsByOverviewQuery } from "graphql/getTicketsByOverviewQuery";
const Pending: FC = () => ( const Pending: NextPage = () => {
const { data: ticketData, error: ticketError }: any = useSWR(
{
document: getTicketsByOverviewQuery,
variables: { overviewId: "gid://zammad/Overview/3" },
},
{ refreshInterval: 10000 }
);
const shouldRender = !ticketError && ticketData;
const tickets =
ticketData?.ticketsByOverview?.edges.map((edge: any) => edge.node) || [];
return (
<Layout> <Layout>
<Head> <Head>
<title>Link Shell</title> <title>Link Shell Assigned Tickets</title>
</Head> </Head>
<Grid {shouldRender && <TicketList title="Pending" tickets={tickets} />}
container {ticketError && <div>{ticketError.toString()}</div>}
spacing={0}
sx={{ height: "100%", width: "100%" }}
direction="column"
>
<Grid
item
sx={{
height: "100%",
width: "100%",
}}
>
<ZammadWrapper path="/#ticket/view/my_pending_reached" />
</Grid>
</Grid>
</Layout> </Layout>
); );
};
export default Pending; export default Pending;

View file

@ -1,31 +1,32 @@
import { FC } from "react";
import Head from "next/head"; import Head from "next/head";
import { Grid } from "@mui/material"; import useSWR from "swr";
import { NextPage } from "next";
import { Layout } from "components/Layout"; import { Layout } from "components/Layout";
import { ZammadWrapper } from "components/ZammadWrapper"; import { TicketList } from "components/TicketList";
import { getTicketsByOverviewQuery } from "graphql/getTicketsByOverviewQuery";
const Unassigned: FC = () => ( const Unassigned: NextPage = () => {
const { data: ticketData, error: ticketError }: any = useSWR(
{
document: getTicketsByOverviewQuery,
variables: { overviewId: "gid://zammad/Overview/2" },
},
{ refreshInterval: 10000 }
);
const shouldRender = !ticketError && ticketData;
const tickets =
ticketData?.ticketsByOverview?.edges.map((edge: any) => edge.node) || [];
return (
<Layout> <Layout>
<Head> <Head>
<title>Link Shell</title> <title>Link Shell Assigned Tickets</title>
</Head> </Head>
<Grid {shouldRender && <TicketList title="Unassigned" tickets={tickets} />}
container {ticketError && <div>{ticketError.toString()}</div>}
spacing={0}
sx={{ height: "100%", width: "100%" }}
direction="column"
>
<Grid
item
sx={{
height: "100%",
width: "100%",
}}
>
<ZammadWrapper path="/#ticket/view/all_unassigned" />
</Grid>
</Grid>
</Layout> </Layout>
); );
};
export default Unassigned; export default Unassigned;

View file

@ -1,31 +1,32 @@
import { FC } from "react";
import Head from "next/head"; import Head from "next/head";
import { Grid } from "@mui/material"; import useSWR from "swr";
import { NextPage } from "next";
import { Layout } from "components/Layout"; import { Layout } from "components/Layout";
import { ZammadWrapper } from "components/ZammadWrapper"; import { TicketList } from "components/TicketList";
import { getTicketsByOverviewQuery } from "graphql/getTicketsByOverviewQuery";
const Urgent: FC = () => ( const Urgent: NextPage = () => {
const { data: ticketData, error: ticketError }: any = useSWR(
{
document: getTicketsByOverviewQuery,
variables: { overviewId: "gid://zammad/Overview/7" },
},
{ refreshInterval: 10000 }
);
const shouldRender = !ticketError && ticketData;
const tickets =
ticketData?.ticketsByOverview?.edges.map((edge: any) => edge.node) || [];
return (
<Layout> <Layout>
<Head> <Head>
<title>Link Shell</title> <title>Link Shell Urgent Tickets</title>
</Head> </Head>
<Grid {shouldRender && <TicketList title="Urgent" tickets={tickets} />}
container {ticketError && <div>{ticketError.toString()}</div>}
spacing={0}
sx={{ height: "100%", width: "100%" }}
direction="column"
>
<Grid
item
sx={{
height: "100%",
width: "100%",
}}
>
<ZammadWrapper path="/#ticket/view/all_escalated" />
</Grid>
</Grid>
</Layout> </Layout>
); );
};
export default Urgent; export default Urgent;

View file

@ -214,6 +214,7 @@ services:
link: link:
container_name: link container_name: link
restart: ${RESTART}
build: build:
context: . context: .
dockerfile: ./apps/link/Dockerfile dockerfile: ./apps/link/Dockerfile
@ -223,13 +224,12 @@ services:
- "8003:3000" - "8003:3000"
environment: environment:
ZAMMAD_PROXY_URL: ${ZAMMAD_PROXY_URL} ZAMMAD_PROXY_URL: ${ZAMMAD_PROXY_URL}
ZAMMAD_URL: ${ZAMMAD_URL}
ZAMMAD_API_TOKEN: ${ZAMMAD_API_TOKEN} ZAMMAD_API_TOKEN: ${ZAMMAD_API_TOKEN}
ZAMMAD_VIRUAL_HOST: ${ZAMMAD_VIRTUAL_HOST} ZAMMAD_VIRUAL_HOST: ${ZAMMAD_VIRTUAL_HOST}
LINK_URL: ${LINK_URL} LINK_URL: ${LINK_URL}
LEAFCUTTER_URL: http://leafcutter:3000 LEAFCUTTER_URL: http://leafcutter:3000
METAMIGO_URL: http://metamigo-frontend:3000 METAMIGO_URL: http://metamigo-frontend:3000
ZAMMAD_URL: http://zammad-nginx:8080 ZAMMAD_URL: http://localhost:8001
NEXTAUTH_URL: ${LINK_URL} NEXTAUTH_URL: ${LINK_URL}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET} NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
NEXTAUTH_AUDIENCE: ${NEXTAUTH_AUDIENCE} NEXTAUTH_AUDIENCE: ${NEXTAUTH_AUDIENCE}
@ -240,6 +240,7 @@ services:
leafcutter: leafcutter:
container_name: leafcutter container_name: leafcutter
restart: ${RESTART}
build: build:
context: . context: .
dockerfile: ./apps/leafcutter/Dockerfile dockerfile: ./apps/leafcutter/Dockerfile