Merge branch 'shell-updates' into 'main'
Shell updates See merge request digiresilience/link/link-stack!1
This commit is contained in:
commit
8949b10671
25 changed files with 3980 additions and 4813 deletions
|
|
@ -18,6 +18,7 @@ COPY --from=builder ${APP_DIR}/out/package-lock.json ./package-lock.json
|
|||
RUN npm ci --omit=dev
|
||||
|
||||
COPY --from=builder ${APP_DIR}/out/full/ .
|
||||
ARG LINK_EMBEDDED=true
|
||||
RUN npm i -g turbo
|
||||
RUN turbo run build --filter=leafcutter
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ import { useAppContext } from "./AppProvider";
|
|||
|
||||
export const Layout: FC<PropsWithChildren> = ({ children }) => {
|
||||
const [cookies, setCookie] = useCookies(["cookieConsent"]);
|
||||
|
||||
const consentGranted = cookies.cookieConsent === "true";
|
||||
|
||||
const {
|
||||
publicRuntimeConfig: { embedded },
|
||||
} = getConfig();
|
||||
|
|
|
|||
|
|
@ -395,11 +395,21 @@
|
|||
"display": "LGBT / Gender / Sexuality",
|
||||
"description": ""
|
||||
},
|
||||
"male": {
|
||||
"category": "",
|
||||
"display": "Male",
|
||||
"description": ""
|
||||
},
|
||||
"media": {
|
||||
"category": "",
|
||||
"display": "Media",
|
||||
"description": ""
|
||||
},
|
||||
"non-binary": {
|
||||
"category": "",
|
||||
"display": "Non-binary",
|
||||
"description": ""
|
||||
},
|
||||
"policy-politics": {
|
||||
"category": "",
|
||||
"display": "Policy / Politics",
|
||||
|
|
@ -415,6 +425,11 @@
|
|||
"display": "Refugees",
|
||||
"description": ""
|
||||
},
|
||||
"transgender": {
|
||||
"category": "",
|
||||
"display": "Transgender",
|
||||
"description": ""
|
||||
},
|
||||
"womens-rights": {
|
||||
"category": "",
|
||||
"display": "Womens' Rights",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
module.exports = {
|
||||
publicRuntimeConfig: {
|
||||
embedded: Boolean(process.env.LINK_EMBEDDED),
|
||||
embedded: true
|
||||
},
|
||||
basePath: "/proxy/leafcutter",
|
||||
assetPrefix: "/proxy/leafcutter",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { FC, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
|
|
@ -19,12 +20,14 @@ import {
|
|||
Cottage as CottageIcon,
|
||||
Settings as SettingsIcon,
|
||||
ExpandCircleDown as ExpandCircleDownIcon,
|
||||
Dvr as DvrIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
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 closedWidth = 100;
|
||||
|
|
@ -38,8 +41,9 @@ const MenuItem = ({
|
|||
selected = false,
|
||||
open = true,
|
||||
badge,
|
||||
target = "_self",
|
||||
}: any) => (
|
||||
<Link href={href}>
|
||||
<Link href={href} target={target}>
|
||||
<ListItemButton
|
||||
sx={{
|
||||
p: 0,
|
||||
|
|
@ -124,7 +128,7 @@ const MenuItem = ({
|
|||
}
|
||||
/>
|
||||
)}
|
||||
{badge && (
|
||||
{badge && badge > 0 ? (
|
||||
<ListItemSecondaryAction>
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
|
|
@ -142,7 +146,7 @@ const MenuItem = ({
|
|||
{badge}
|
||||
</Typography>
|
||||
</ListItemSecondaryAction>
|
||||
)}
|
||||
) : null}
|
||||
</ListItemButton>
|
||||
</Link>
|
||||
);
|
||||
|
|
@ -153,9 +157,28 @@ interface SidebarProps {
|
|||
}
|
||||
|
||||
export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
||||
const { pathname } = useRouter();
|
||||
const router = useRouter();
|
||||
const { pathname } = router;
|
||||
const { data: session } = useSession();
|
||||
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);
|
||||
|
||||
const logout = () => {
|
||||
signOut({ callbackUrl: "/login" });
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
|
|
@ -341,7 +364,6 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
iconSize={20}
|
||||
open={open}
|
||||
/>
|
||||
|
||||
<Collapse
|
||||
in={pathname.startsWith("/tickets")}
|
||||
timeout="auto"
|
||||
|
|
@ -355,7 +377,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/tickets/assigned")}
|
||||
badge={3}
|
||||
badge={assignedCount}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
|
|
@ -364,7 +386,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/tickets/urgent")}
|
||||
badge={1}
|
||||
badge={urgentCount}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
|
|
@ -373,7 +395,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/tickets/pending")}
|
||||
badge={9}
|
||||
badge={pendingCount}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
|
|
@ -381,21 +403,12 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
href="/tickets/unassigned"
|
||||
Icon={FeaturedPlayListIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/tickets/unnassigned")}
|
||||
badge={27}
|
||||
open={open}
|
||||
/>
|
||||
<MenuItem
|
||||
name="New Ticket UI"
|
||||
href="/tickets/2"
|
||||
Icon={SettingsIcon}
|
||||
iconSize={0}
|
||||
selected={pathname.endsWith("/tickets/2")}
|
||||
selected={pathname.endsWith("/tickets/unassigned")}
|
||||
badge={unassignedCount}
|
||||
open={open}
|
||||
/>
|
||||
</List>
|
||||
</Collapse>
|
||||
|
||||
<MenuItem
|
||||
name="Knowledge Base"
|
||||
href="/knowledge"
|
||||
|
|
@ -406,7 +419,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
/>
|
||||
<MenuItem
|
||||
name="Leafcutter"
|
||||
href="/leafcutter"
|
||||
href="/leafcutter/about"
|
||||
Icon={AnalyticsIcon}
|
||||
iconSize={20}
|
||||
selected={pathname.endsWith("/leafcutter")}
|
||||
|
|
@ -419,6 +432,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
onClick={undefined}
|
||||
>
|
||||
<List component="div" disablePadding>
|
||||
{/*
|
||||
<MenuItem
|
||||
name="Dashboard"
|
||||
href="/leafcutter"
|
||||
|
|
@ -442,7 +456,7 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
selected={pathname.endsWith("/leafcutter/trends")}
|
||||
open={open}
|
||||
/>
|
||||
|
||||
*/}
|
||||
<MenuItem
|
||||
name="FAQ"
|
||||
href="/leafcutter/faq"
|
||||
|
|
@ -509,13 +523,21 @@ export const Sidebar: FC<SidebarProps> = ({ open, setOpen }) => {
|
|||
/>
|
||||
</List>
|
||||
</Collapse>
|
||||
|
||||
<MenuItem
|
||||
name="Zammad Interface"
|
||||
href="/proxy/zammad"
|
||||
Icon={DvrIcon}
|
||||
iconSize={20}
|
||||
open={open}
|
||||
target="_blank"
|
||||
/>
|
||||
<MenuItem
|
||||
name="Logout"
|
||||
href="/logout"
|
||||
Icon={LogoutIcon}
|
||||
iconSize={20}
|
||||
open={open}
|
||||
onClick={logout}
|
||||
/>
|
||||
</List>
|
||||
</Grid>
|
||||
|
|
|
|||
84
apps/link/components/StyledDataGrid.tsx
Normal file
84
apps/link/components/StyledDataGrid.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
71
apps/link/components/TicketList.tsx
Normal file
71
apps/link/components/TicketList.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
19
apps/link/graphql/getTicketOverviewCountsQuery.ts
Normal file
19
apps/link/graphql/getTicketOverviewCountsQuery.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}`;
|
||||
60
apps/link/graphql/getTicketsByOverviewQuery.ts
Normal file
60
apps/link/graphql/getTicketsByOverviewQuery.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { withAuth } from "next-auth/middleware";
|
||||
import { getToken } from "next-auth/jwt";
|
||||
import { withAuth, NextRequestWithAuth } from "next-auth/middleware";
|
||||
|
||||
const rewriteURL = (request: NextRequest, originBaseURL: string, destinationBaseURL: string, headers: any = {}) => {
|
||||
if (request.nextUrl.protocol.startsWith('ws')) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
if (request.nextUrl.pathname.includes('/_next/static/development/')) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
|
@ -11,31 +14,54 @@ const rewriteURL = (request: NextRequest, originBaseURL: string, destinationBase
|
|||
const destinationURL = request.url.replace(originBaseURL, destinationBaseURL);
|
||||
console.log(`Rewriting ${request.url} to ${destinationURL}`);
|
||||
|
||||
return NextResponse.rewrite(new URL(destinationURL), { ...request.headers, ...headers });
|
||||
const requestHeaders = new Headers(request.headers);
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
// @ts-ignore
|
||||
requestHeaders.set(key, value);
|
||||
}
|
||||
|
||||
requestHeaders.delete('connection');
|
||||
|
||||
console.log({ finalHeaders: requestHeaders });
|
||||
|
||||
return NextResponse.rewrite(new URL(destinationURL), { request: { headers: requestHeaders } });
|
||||
};
|
||||
|
||||
const checkRewrites = async (request: NextRequest) => {
|
||||
const checkRewrites = async (request: NextRequestWithAuth) => {
|
||||
console.log({ currentURL: request.nextUrl.href });
|
||||
|
||||
const linkBaseURL = process.env.LINK_URL ?? "http://localhost:3000";
|
||||
const zammadURL = process.env.ZAMMAD_URL ?? "http://zammad-nginx:8080";
|
||||
const leafcutterURL = process.env.LEAFCUTTER_URL ?? "http://leafcutter:3000";
|
||||
const metamigoURL = process.env.METAMIGO_URL ?? "http://metamigo:3000";
|
||||
|
||||
if (request.nextUrl.pathname.startsWith('/proxy/leafcutter')) {
|
||||
return rewriteURL(request, process.env.LINK_URL, process.env.LEAFCUTTER_URL);
|
||||
return rewriteURL(request, linkBaseURL, leafcutterURL);
|
||||
} else if (request.nextUrl.pathname.startsWith('/proxy/metamigo')) {
|
||||
return rewriteURL(request, process.env.LINK_URL, process.env.METAMIGO_URL);
|
||||
return rewriteURL(request, linkBaseURL, metamigoURL);
|
||||
} else if (request.nextUrl.pathname.startsWith('/proxy/zammad')) {
|
||||
const session = await getToken({
|
||||
req: request,
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
});
|
||||
console.log('proxying to zammad');
|
||||
const { token } = request.nextauth;
|
||||
|
||||
console.log({ nextauth: request.nextauth });
|
||||
|
||||
const headers = {
|
||||
'X-Forwarded-User': session.email.toLowerCase(),
|
||||
host: 'zammad.example.com'
|
||||
'X-Forwarded-User': token.email.toLowerCase(),
|
||||
host: 'link-stack-dev.digiresilience.org'
|
||||
};
|
||||
|
||||
return rewriteURL(request, `${process.env.LINK_URL}/proxy/zammad`, process.env.ZAMMAD_URL, headers);
|
||||
} else if (request.nextUrl.pathname.startsWith('/assets')) {
|
||||
console.log({ headers });
|
||||
|
||||
return rewriteURL(request, `${linkBaseURL}/proxy/zammad`, zammadURL, headers);
|
||||
} else if (request.nextUrl.pathname.startsWith('/assets') || request.nextUrl.pathname.startsWith('/api/v1')) {
|
||||
console.log('asset');
|
||||
return rewriteURL(request, `${process.env.LINK_URL}`, process.env.ZAMMAD_URL);
|
||||
} else if (request.nextUrl.pathname.startsWith('/proxy/assets') || request.nextUrl.pathname.startsWith('/proxy/api')) {
|
||||
return rewriteURL(request, linkBaseURL, zammadURL);
|
||||
} else if (request.nextUrl.pathname.startsWith('/proxy/assets')) {
|
||||
console.log('proxy asset');
|
||||
return rewriteURL(request, `${process.env.LINK_URL}/proxy`, process.env.ZAMMAD_URL);
|
||||
return rewriteURL(request, `${linkBaseURL}/proxy`, zammadURL);
|
||||
} else if (request.nextUrl.pathname.startsWith('/proxy/api')) {
|
||||
console.log('proxy api');
|
||||
return rewriteURL(request, `${linkBaseURL}/proxy`, zammadURL);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -71,4 +97,3 @@ export default withAuth(
|
|||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,17 @@ const nextConfig = {
|
|||
linkURL: process.env.LINK_URL,
|
||||
leafcutterURL: process.env.LEAFCUTTER_URL,
|
||||
metamigoURL: process.env.METAMIGO_URL,
|
||||
muiLicenseKey: process.env.MUI_LICENSE_KEY,
|
||||
},
|
||||
async rewrites() {
|
||||
return {
|
||||
fallback: [
|
||||
{
|
||||
source: "/:path*",
|
||||
destination: `/proxy/zammad/:path*`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,10 @@ import "styles/global.css";
|
|||
import { LicenseInfo } from "@mui/x-data-grid-pro";
|
||||
import { SWRConfig } from "swr";
|
||||
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();
|
||||
|
||||
|
|
|
|||
5
apps/link/pages/api/v1/users
Normal file
5
apps/link/pages/api/v1/users
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
res.redirect(307, '/proxy/zammad/api/v1' + req.url.substring('/api/v1'.length));
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ const Login: FC<LoginProps> = ({ session }) => {
|
|||
sx={buttonStyles}
|
||||
onClick={() =>
|
||||
signIn("google", {
|
||||
callbackUrl: `${origin}/auth/sso`,
|
||||
callbackUrl: `${origin}/proxy/zammad/auth/sso`,
|
||||
})
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,32 @@
|
|||
import { FC } from "react";
|
||||
import Head from "next/head";
|
||||
import { Grid } from "@mui/material";
|
||||
import useSWR from "swr";
|
||||
import { NextPage } from "next";
|
||||
import { Layout } from "components/Layout";
|
||||
import { ZammadWrapper } from "components/ZammadWrapper";
|
||||
import { TicketList } from "components/TicketList";
|
||||
import { getTicketsByOverviewQuery } from "graphql/getTicketsByOverviewQuery";
|
||||
|
||||
const Assigned: FC = () => (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell</title>
|
||||
</Head>
|
||||
<Grid
|
||||
container
|
||||
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>
|
||||
);
|
||||
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>
|
||||
<Head>
|
||||
<title>Link Shell – Assigned Tickets</title>
|
||||
</Head>
|
||||
{shouldRender && <TicketList title="Assigned" tickets={tickets} />}
|
||||
{ticketError && <div>{ticketError.toString()}</div>}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Assigned;
|
||||
|
|
|
|||
|
|
@ -1,31 +1,32 @@
|
|||
import { FC } from "react";
|
||||
import Head from "next/head";
|
||||
import { Grid } from "@mui/material";
|
||||
import useSWR from "swr";
|
||||
import { NextPage } from "next";
|
||||
import { Layout } from "components/Layout";
|
||||
import { ZammadWrapper } from "components/ZammadWrapper";
|
||||
import { TicketList } from "components/TicketList";
|
||||
import { getTicketsByOverviewQuery } from "graphql/getTicketsByOverviewQuery";
|
||||
|
||||
const Pending: FC = () => (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell</title>
|
||||
</Head>
|
||||
<Grid
|
||||
container
|
||||
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>
|
||||
);
|
||||
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>
|
||||
<Head>
|
||||
<title>Link Shell – Assigned Tickets</title>
|
||||
</Head>
|
||||
{shouldRender && <TicketList title="Pending" tickets={tickets} />}
|
||||
{ticketError && <div>{ticketError.toString()}</div>}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pending;
|
||||
|
|
|
|||
|
|
@ -1,31 +1,32 @@
|
|||
import { FC } from "react";
|
||||
import Head from "next/head";
|
||||
import { Grid } from "@mui/material";
|
||||
import useSWR from "swr";
|
||||
import { NextPage } from "next";
|
||||
import { Layout } from "components/Layout";
|
||||
import { ZammadWrapper } from "components/ZammadWrapper";
|
||||
import { TicketList } from "components/TicketList";
|
||||
import { getTicketsByOverviewQuery } from "graphql/getTicketsByOverviewQuery";
|
||||
|
||||
const Unassigned: FC = () => (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell</title>
|
||||
</Head>
|
||||
<Grid
|
||||
container
|
||||
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>
|
||||
);
|
||||
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>
|
||||
<Head>
|
||||
<title>Link Shell – Assigned Tickets</title>
|
||||
</Head>
|
||||
{shouldRender && <TicketList title="Unassigned" tickets={tickets} />}
|
||||
{ticketError && <div>{ticketError.toString()}</div>}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Unassigned;
|
||||
|
|
|
|||
|
|
@ -1,31 +1,32 @@
|
|||
import { FC } from "react";
|
||||
import Head from "next/head";
|
||||
import { Grid } from "@mui/material";
|
||||
import useSWR from "swr";
|
||||
import { NextPage } from "next";
|
||||
import { Layout } from "components/Layout";
|
||||
import { ZammadWrapper } from "components/ZammadWrapper";
|
||||
import { TicketList } from "components/TicketList";
|
||||
import { getTicketsByOverviewQuery } from "graphql/getTicketsByOverviewQuery";
|
||||
|
||||
const Urgent: FC = () => (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>Link Shell</title>
|
||||
</Head>
|
||||
<Grid
|
||||
container
|
||||
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>
|
||||
);
|
||||
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>
|
||||
<Head>
|
||||
<title>Link Shell – Urgent Tickets</title>
|
||||
</Head>
|
||||
{shouldRender && <TicketList title="Urgent" tickets={tickets} />}
|
||||
{ticketError && <div>{ticketError.toString()}</div>}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Urgent;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
"pg": "^8.11.0",
|
||||
"pg-monitor": "^2.0.0",
|
||||
"pg-promise": "^11.4.3",
|
||||
"postgraphile": "4.12.3",
|
||||
"postgraphile-plugin-connection-filter": "^2.3.0",
|
||||
"remeda": "^1.18.1",
|
||||
"twilio": "^4.11.1",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {
|
|||
TextField,
|
||||
EmailField,
|
||||
BooleanField,
|
||||
ListProps,
|
||||
} from "react-admin";
|
||||
|
||||
const UserList = () => (
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import Cognito from "next-auth/providers/cognito";
|
|||
import { loadConfig, IAppConfig } from "@digiresilience/metamigo-config";
|
||||
import { MetamigoAdapter } from "../../../lib/nextauth-adapter";
|
||||
import { CloudflareAccessProvider } from "../../../lib/cloudflare";
|
||||
import { AdapterSession, AdapterUser } from "next-auth/adapters";
|
||||
|
||||
const nextAuthOptions = (config: IAppConfig, req: NextApiRequest) => {
|
||||
const { nextAuth, cfaccess } = config;
|
||||
|
|
@ -70,8 +69,8 @@ const nextAuthOptions = (config: IAppConfig, req: NextApiRequest) => {
|
|||
providers,
|
||||
adapter,
|
||||
callbacks: {
|
||||
async session({session, token, user}) {
|
||||
session.user.id = user.id
|
||||
async session({ session, user }: any) {
|
||||
session.user.id = user.id;
|
||||
session.user.userRole = user.userRole;
|
||||
return session;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ services:
|
|||
depends_on:
|
||||
- zammad-postgresql
|
||||
environment:
|
||||
<<: [*common-zammad-variables, *common-global-variables]
|
||||
<<: [ *common-zammad-variables, *common-global-variables ]
|
||||
POSTGRESQL_USER: zammad
|
||||
POSTGRESQL_PASS: ${ZAMMAD_DATABASE_PASSWORD}
|
||||
build: ./docker/zammad
|
||||
|
|
@ -124,7 +124,7 @@ services:
|
|||
- zammad-postgresql
|
||||
- zammad-redis
|
||||
environment:
|
||||
<<: [*common-global-variables, *common-zammad-variables]
|
||||
<<: [ *common-global-variables, *common-zammad-variables ]
|
||||
build: ./docker/zammad
|
||||
image: registry.gitlab.com/digiresilience/link/link-stack/zammad
|
||||
restart: ${RESTART}
|
||||
|
|
@ -137,7 +137,7 @@ services:
|
|||
image: registry.gitlab.com/digiresilience/link/link-stack/zammad-redis
|
||||
restart: ${RESTART}
|
||||
environment:
|
||||
<<: *common-global-variables
|
||||
<<: *common-global-variables
|
||||
|
||||
zammad-scheduler:
|
||||
platform: linux/x86_64
|
||||
|
|
@ -148,7 +148,7 @@ services:
|
|||
- zammad-railsserver
|
||||
- zammad-redis
|
||||
environment:
|
||||
<<: [*common-global-variables, *common-zammad-variables]
|
||||
<<: [ *common-global-variables, *common-zammad-variables ]
|
||||
build: ./docker/zammad
|
||||
image: registry.gitlab.com/digiresilience/link/link-stack/zammad
|
||||
restart: ${RESTART}
|
||||
|
|
@ -164,13 +164,24 @@ services:
|
|||
- zammad-railsserver
|
||||
- zammad-redis
|
||||
environment:
|
||||
<<: [*common-global-variables, *common-zammad-variables]
|
||||
<<: [ *common-global-variables, *common-zammad-variables ]
|
||||
build: ./docker/zammad
|
||||
image: registry.gitlab.com/digiresilience/link/link-stack/zammad
|
||||
restart: ${RESTART}
|
||||
volumes:
|
||||
- zammad-data:/opt/zammad
|
||||
|
||||
opensearch:
|
||||
container_name: opensearch
|
||||
build: ./docker/opensearch
|
||||
restart: ${RESTART}
|
||||
volumes:
|
||||
- opensearch-data:/usr/share/opensearch/data
|
||||
|
||||
opensearch-dashboards:
|
||||
container_name: opensearch-dashboards
|
||||
build: ./docker/opensearch-dashboards
|
||||
restart: ${RESTART}
|
||||
|
||||
metamigo-postgresql:
|
||||
build: ./docker/postgresql
|
||||
|
|
@ -245,7 +256,7 @@ services:
|
|||
volumes:
|
||||
- ./signald-state:/signald
|
||||
environment:
|
||||
<<: *common-global-variables
|
||||
<<: *common-global-variables
|
||||
|
||||
# nginx-proxy:
|
||||
# container_name: nginx-proxy
|
||||
|
|
@ -256,40 +267,49 @@ services:
|
|||
# volumes:
|
||||
# - /var/run/docker.sock:/tmp/docker.sock:ro
|
||||
|
||||
# link:
|
||||
# container_name: link
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: ./apps/link/Dockerfile
|
||||
# expose:
|
||||
# - "3000"
|
||||
# ports:
|
||||
# - "8003:3000"
|
||||
# environment:
|
||||
# <<: *common-global-variables
|
||||
# ZAMMAD_PROXY_URL: ${ZAMMAD_PROXY_URL}
|
||||
# ZAMMAD_URL: ${ZAMMAD_URL}
|
||||
# ZAMMAD_API_TOKEN: ${ZAMMAD_API_TOKEN}
|
||||
# ZAMMAD_VIRUAL_HOST: ${ZAMMAD_VIRTUAL_HOST}
|
||||
# LINK_URL: ${LINK_URL}
|
||||
# NEXTAUTH_URL: ${LINK_URL}
|
||||
# NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
|
||||
# NEXTAUTH_AUDIENCE: ${NEXTAUTH_AUDIENCE}
|
||||
# NEXTAUTH_SIGNING_KEY_B64: ${NEXTAUTH_SIGNING_KEY_B64}
|
||||
# NEXTAUTH_ENCRYPTION_KEY_B64: ${NEXTAUTH_ENCRYPTION_KEY_B64}
|
||||
# GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
|
||||
# GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
|
||||
link:
|
||||
container_name: link
|
||||
restart: ${RESTART}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./apps/link/Dockerfile
|
||||
expose:
|
||||
- "3000"
|
||||
ports:
|
||||
- "8003:3000"
|
||||
environment:
|
||||
ZAMMAD_PROXY_URL: ${ZAMMAD_PROXY_URL}
|
||||
ZAMMAD_API_TOKEN: ${ZAMMAD_API_TOKEN}
|
||||
ZAMMAD_VIRUAL_HOST: ${ZAMMAD_VIRTUAL_HOST}
|
||||
LINK_URL: ${LINK_URL}
|
||||
LEAFCUTTER_URL: http://leafcutter:3000
|
||||
METAMIGO_URL: http://metamigo-frontend:3000
|
||||
ZAMMAD_URL: http://localhost:8001
|
||||
NEXTAUTH_URL: ${LINK_URL}
|
||||
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
|
||||
NEXTAUTH_AUDIENCE: ${NEXTAUTH_AUDIENCE}
|
||||
NEXTAUTH_SIGNING_KEY_B64: ${NEXTAUTH_SIGNING_KEY_B64}
|
||||
NEXTAUTH_ENCRYPTION_KEY_B64: ${NEXTAUTH_ENCRYPTION_KEY_B64}
|
||||
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
|
||||
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
|
||||
|
||||
# leafcutter:
|
||||
# container_name: leafcutter
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: ./apps/leafcutter/Dockerfile
|
||||
# ports:
|
||||
# - "8004:3000"
|
||||
# environment:
|
||||
# <<: *common-global-variables
|
||||
# LINK_EMBEDDED: "true"
|
||||
leafcutter:
|
||||
container_name: leafcutter
|
||||
restart: ${RESTART}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./apps/leafcutter/Dockerfile
|
||||
expose:
|
||||
- "3000"
|
||||
ports:
|
||||
- "8004:3000"
|
||||
environment:
|
||||
LINK_EMBEDDED: "true"
|
||||
NEXTAUTH_URL: ${LINK_URL}
|
||||
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
|
||||
NEXTAUTH_AUDIENCE: ${NEXTAUTH_AUDIENCE}
|
||||
NEXTAUTH_SIGNING_KEY_B64: ${NEXTAUTH_SIGNING_KEY_B64}
|
||||
NEXTAUTH_ENCRYPTION_KEY_B64: ${NEXTAUTH_ENCRYPTION_KEY_B64}
|
||||
|
||||
volumes:
|
||||
elasticsearch-data:
|
||||
|
|
@ -300,3 +320,5 @@ volumes:
|
|||
driver: local
|
||||
metamigo-data:
|
||||
driver: local
|
||||
opensearch-data:
|
||||
driver: local
|
||||
|
|
|
|||
1
docker/opensearch-dashboards/Dockerfile
Normal file
1
docker/opensearch-dashboards/Dockerfile
Normal file
|
|
@ -0,0 +1 @@
|
|||
FROM opensearchproject/opensearch-dashboards:2.8.0
|
||||
1
docker/opensearch/Dockerfile
Normal file
1
docker/opensearch/Dockerfile
Normal file
|
|
@ -0,0 +1 @@
|
|||
FROM opensearchproject/opensearch:2.8.0
|
||||
8067
package-lock.json
generated
8067
package-lock.json
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue