Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,22 @@ import {ItemUsagePage} from "./pages/items/ItemUsagePage";
import {SessionsStatsPage} from "./pages/sessions/SessionsStatsPage";
import {AllCharactersPage} from "./pages/characters/AllCharactersPage";
import {AllPlayersPage} from "./pages/players/AllPlayersPage";
import {RedirectPage} from "./pages/RedirectPage";
import {ManageInstancePage} from "./pages/foundry/ManageInstancePage";

const router = createBrowserRouter([
{
path: "/",
children: [
{ index: true, element: <HomePage /> },
{ path: "auth", element: <AuthPage /> },
{ path: "inactive/:instanceUrl/:other?", element: <RedirectPage /> },
{
path: "user",
element: <AuthenticatedLayout />,
children : [
{ index: true, element: <CharactersPage /> },
{ path: ":characterId", element: <CharacterPage />}
{ path: ":characterId", element: <CharacterPage />},
]
},
{
Expand Down Expand Up @@ -63,6 +66,13 @@ const router = createBrowserRouter([
{ path: "add", element: <AddItemPage /> },
{ path: "usage", element: <ItemUsagePage /> }
]
},
{
path: "foundry",
element: <AuthenticatedLayout />,
children: [
{ path: "instances", element: <ManageInstancePage /> },
]
}
],
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/character/CharacterList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ export const CharacterList = ({
<Heading>Character history</Heading>
</Container>
<SimpleGrid columns={size?.cards ?? 3} spacing={2}>
{!!activeCharacters &&
activeCharacters.map((it) => (
{!!otherCharacters &&
otherCharacters.map((it) => (
<CharacterCard key={it.id} character={it} linkToProfile={false}/>
))}
</SimpleGrid>
Expand Down
59 changes: 59 additions & 0 deletions src/components/foundry/FoundryRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
Badge, Button,
GridItem,
Link,
Text
} from "@chakra-ui/react";
import React from "react";
import {InstanceInfo} from "../../models/foundry/InstanceInfo";
import {useStopInstanceMutation} from "../../services/foundry";

interface FoundryRowProps {
instanceInfo: InstanceInfo
}

export const FoundryRow = ({ instanceInfo }: FoundryRowProps) => {
const [stopInstance] = useStopInstanceMutation()
return (
<>
<GridItem display="flex" alignItems="center">
<Link color='teal.500' href={instanceInfo.url} isExternal>
{instanceInfo.id}
</Link>
</GridItem>
<GridItem display="flex" alignItems="center">
<Text>{instanceInfo.masterName}</Text>
</GridItem>
<GridItem display="flex" alignItems="center">
<Text>{computeTtl(instanceInfo.uptime)}</Text>
</GridItem>
<GridItem display="flex" alignItems="center">
{instanceInfo.status === "online" && <Badge colorScheme='green'>ONLINE</Badge>}
{instanceInfo.status === "stopped" && <Badge colorScheme='red'>STOPPED</Badge>}
{instanceInfo.status !== "stopped"
&& instanceInfo.status !== "online"
&& <Badge colorScheme='yellow'>{instanceInfo.status.toUpperCase()}</Badge>}
</GridItem>
<GridItem display="flex" alignItems="center">
<Text>{Math.ceil(instanceInfo.cpu * 100)}%</Text>
</GridItem>
<GridItem display="flex" alignItems="center">
<Text>{Math.ceil(instanceInfo.memory / 1024 / 1024)} Mb</Text>
</GridItem>
<GridItem display="flex" alignItems="center">
<Text>{Math.ceil(instanceInfo.diskSize / 1024 / 1024)} Mb</Text>
</GridItem>
<GridItem display="flex" alignItems="center">
<Button colorScheme="red" onClick={() => {stopInstance(instanceInfo.id)}}>Terminate {instanceInfo.id}</Button>
</GridItem>
</>
)
}

function computeTtl(timestamp: number) {
const differenceInMinutes = Math.floor((new Date().getTime() - timestamp) / 1000 / 60)
const days = Math.floor(differenceInMinutes / 60 / 24)
const hours = Math.floor((differenceInMinutes - days * 60 * 24) / 60)
const minutes = differenceInMinutes - days * 60 * 24 - hours * 60
return `${days > 0 ? `${days}D ` : ''}${hours > 0 ? `${hours}H ` : ''}${minutes}m`
}
3 changes: 3 additions & 0 deletions src/components/menu/TopMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import React from "react";
import { CharacterButton } from "./buttons/CharacterButton";
import { ItemButton } from "./buttons/ItemButton";
import {PlayerButton} from "./buttons/PlayerButton";
import {hasRole} from "../../utils/role-utils";
import {FoundryButton} from "./buttons/FoundryButton";

export const TopMenu = ({ roles }: { roles: Role[] }) => {
const { colorMode } = useColorMode()
Expand All @@ -28,6 +30,7 @@ export const TopMenu = ({ roles }: { roles: Role[] }) => {
<CharacterButton roles={roles} backgroundColor={backgroundColor} />
<PlayerButton roles={roles} backgroundColor={backgroundColor}/>
<ItemButton roles={roles} backgroundColor={backgroundColor}/>
{hasRole(roles, Role.MANAGE_SESSIONS) && <FoundryButton backgroundColor={backgroundColor}/>}
<Box position="absolute" right="2vw" paddingTop="0.25em">
<AvatarIcon user={member} />
</Box>
Expand Down
22 changes: 22 additions & 0 deletions src/components/menu/buttons/FoundryButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ChevronDownIcon } from "@chakra-ui/icons";
import { Button, Menu, MenuButton, MenuItem, MenuList } from "@chakra-ui/react";
import { Link } from "react-router-dom";

export const FoundryButton = ({ backgroundColor }: { backgroundColor: string }) => {
return (
<Menu>
<MenuButton
as={Button}
rightIcon={<ChevronDownIcon />}
background={backgroundColor}
backdropFilter="saturate(180%) blur(5px)"
borderRadius='0'
>
Foundry
</MenuButton>
<MenuList>
<Link to="/foundry/instances"><MenuItem>Manage Instances</MenuItem></Link>
</MenuList>
</Menu>
);
};
10 changes: 10 additions & 0 deletions src/models/foundry/InstanceInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface InstanceInfo {
id: string,
url: string,
status: string,
masterName: string,
cpu: number,
memory: number,
uptime: number,
diskSize: number,
}
2 changes: 1 addition & 1 deletion src/pages/CharactersPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Alert, AlertIcon, Center, AlertTitle, AlertDescription, useBreakpointValue} from "@chakra-ui/react";
import {Alert, AlertIcon, Center, AlertTitle, AlertDescription} from "@chakra-ui/react";
import {useGetAllCharactersForSelfQuery} from "../services/character";
import { CharacterList } from "../components/character/CharacterList";

Expand Down
52 changes: 52 additions & 0 deletions src/pages/RedirectPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {useNavigate, useParams} from "react-router-dom";
import {useStartInstanceQuery} from "../services/foundry";
import {Alert, AlertIcon, AlertTitle, Center, Flex, Heading, Progress} from "@chakra-ui/react";
import React, {useEffect, useState} from "react";

export const RedirectPage = () => {
const { instanceUrl } = useParams()
const [progressValue, setProgressValue] = useState<number>(0);
const navigate = useNavigate();
const { data, isSuccess, isError } = useStartInstanceQuery(instanceUrl!!, { skip: !instanceUrl})

useEffect(() => {
if(!instanceUrl) {
setTimeout(() => {
navigate("/")
}, 3000)
}
}, [instanceUrl, navigate])

useEffect(() => {
if(isSuccess) {
setInterval(() => {
setProgressValue(current => current + 1)
}, 100)
}
}, [isSuccess])

useEffect(() => {
if (!!data && progressValue === 100) {
window.location.replace(data)
}
}, [data, navigate, progressValue]);

return <Center>
{!instanceUrl && (
<Alert status="error">
<AlertIcon />
<AlertTitle>Invalid redirect, you will return to the home page shortly</AlertTitle>
</Alert>
)}
{isError && (
<Alert status="error">
<AlertIcon />
<AlertTitle>Invalid foundry instance: {instanceUrl}</AlertTitle>
</Alert>
)}
{isSuccess && <Flex direction="column" mt="5vh">
<Heading>Instance successfully activated, you will be redirected shortly</Heading>
<Progress value={progressValue} mt="2em"/>
</Flex>}
</Center>
};
104 changes: 104 additions & 0 deletions src/pages/foundry/ManageInstancePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {useStreamInstancesInfoQuery} from "../../services/foundry";
import {Alert, AlertIcon, AlertTitle, Center, Flex, Grid, GridItem, Heading, Text} from "@chakra-ui/react";
import {StackedSkeleton} from "../../components/ui/StackedSkeleton";
import React from "react";
import {FoundryRow} from "../../components/foundry/FoundryRow";

export const ManageInstancePage = () => {

const { data, isLoading, isError, isSuccess } = useStreamInstancesInfoQuery();

return (<Flex mr="4em" ml="4em" direction="column">
<Center pb="2em">
<Heading>Manage Foundry Instances</Heading>
</Center>
{isLoading && <StackedSkeleton quantity={5} height="6vh"/>}
{isError && (
<Alert status="error">
<AlertIcon />
<AlertTitle>There was an error while loading the instances</AlertTitle>
</Alert>
)}
{isSuccess && (
<Grid templateColumns='repeat(8, 1fr)' gap={4}>
<GridItem>
<Flex align="center">
<Text
as="b"
fontSize="xl"
mr="0.5em"
>Instance Name</Text>
</Flex>
</GridItem>
<GridItem>
<Flex align="center">
<Text
as="b"
fontSize="xl"
mr="0.5em"
>Master</Text>
</Flex>
</GridItem>
<GridItem>
<Flex align="center">
<Text
as="b"
fontSize="xl"
mr="0.5em"
>Uptime</Text>
</Flex>
</GridItem>
<GridItem>
<Flex align="center">
<Text
as="b"
fontSize="xl"
mr="0.5em"
>Status</Text>
</Flex>
</GridItem>
<GridItem>
<Flex align="center">
<Text
as="b"
fontSize="xl"
mr="0.5em"
>CPU</Text>
</Flex>
</GridItem>
<GridItem>
<Flex align="center">
<Text
as="b"
fontSize="xl"
mr="0.5em"
>RAM</Text>
</Flex>
</GridItem>
<GridItem>
<Flex align="center">
<Text
as="b"
fontSize="xl"
mr="0.5em"
>Disk Occupation</Text>
</Flex>
</GridItem>
<GridItem>
<Flex align="center">
<Text
as="b"
fontSize="xl"
mr="0.5em"
>Terminate</Text>
</Flex>
</GridItem>
{data != null && data["deposito-pg"] != null && <FoundryRow instanceInfo={data["deposito-pg"]} />}
{data != null && Object.values(data)
.filter(it => it.id !== "deposito-pg")
.map(it => <FoundryRow key={it.id} instanceInfo={it} />)
}
</Grid>
)}
</Flex>)
}
71 changes: 71 additions & 0 deletions src/services/foundry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/query/react";
import {AuthState} from "../store/auth/auth-slice";
import {InstanceInfo} from "../models/foundry/InstanceInfo";
import {QueryReturnValue} from "@reduxjs/toolkit/dist/query/baseQueryTypes";
import {FetchBaseQueryError} from "@reduxjs/toolkit/dist/query/react";

export const foundryApi = createApi({
reducerPath: "foundryApi",
baseQuery: fetchBaseQuery({
baseUrl: `${process.env.REACT_APP_KAIRON_API_URL}/foundry`,
}),
endpoints: (builder) => ({
startInstance: builder.query<string, string>({
query: instanceUrl => ({
url: `/inactive/${instanceUrl}`,
responseHandler: 'text'
}),
}),
stopInstance: builder.mutation<string, string>({
queryFn: async (instanceId, api, _, baseMutation) => {
const {
auth: { jwt },
} = api.getState() as { auth: AuthState }

return baseMutation({
url: `/${instanceId}`,
method: 'POST',
headers: {
Authorization: `Bearer ${jwt}`,
'Access-Control-Allow-Origin': '*',
},
responseHandler: 'text',
}) as QueryReturnValue<string, FetchBaseQueryError>
}
}),
streamInstancesInfo: builder.query<{[key: string]: InstanceInfo}, void>({
queryFn: () => ({ data: {} }),
async onCacheEntryAdded(
_,
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved, getState }
) {
await cacheDataLoaded
const {
auth: { jwt },
} = getState() as unknown as { auth: AuthState }
const eventSource = new EventSource(`${process.env.REACT_APP_KAIRON_API_URL}/foundry/info?jwt=${jwt}`)

eventSource.addEventListener("foundry-info", (event: MessageEvent) => {
const newData: {[key: string]: InstanceInfo} = JSON.parse(event.data);
updateCachedData((_) => {
return newData
})
})

eventSource.onerror = (error) => {
console.error('SSE error:', error);
eventSource.close()
};

await cacheEntryRemoved;
eventSource.close()
},
}),
}),
});

export const {
useStartInstanceQuery,
useStopInstanceMutation,
useStreamInstancesInfoQuery
} = foundryApi;
Loading