Skip to content

Commit

Permalink
Merge pull request #694 from exacaster/session_statements_ui
Browse files Browse the repository at this point in the history
Add Session statements UI
  • Loading branch information
pdambrauskas authored Oct 10, 2023
2 parents 6646779 + 74e7850 commit 920de84
Show file tree
Hide file tree
Showing 13 changed files with 1,063 additions and 22 deletions.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"moment": "^2.29.1",
"query-string": "^8.1.0",
"react": "^18.0.0",
"react-code-blocks": "^0.1.4",
"react-dom": "^18.0.0",
"react-moment": "^1.1.1",
"react-router-dom": "^6.16.0",
Expand Down
14 changes: 13 additions & 1 deletion frontend/src/client/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {AxiosInstance} from 'axios';
import {Application, ApplicationLog, BatchPage, Configuration} from './types';
import {Application, ApplicationLog, BatchPage, Configuration, SessionStatement, SessionStatementCode, SessionStatementPage} from './types';

export class Api {
client: AxiosInstance;
Expand Down Expand Up @@ -51,4 +51,16 @@ export class Api {
fetchConfiguration(): Promise<Configuration> {
return this.get('/api/configuration');
}

fetchSessionStatements(sessionId: string, size: number, from: number): Promise<SessionStatementPage> {
return this.get(`/api/sessions/${sessionId}/statements?size=${size}&from=${from}`);
}

postSessionStatement(sessionId: string, statement: SessionStatementCode): Promise<SessionStatement> {
return this.client.post(`/api/sessions/${sessionId}/statements`, statement);
}

cancelSessionStatement(sessionId: string, statementId: string): Promise<void> {
return this.client.post(`/api/sessions/${sessionId}/statements/${statementId}/cancel`);
}
}
18 changes: 18 additions & 0 deletions frontend/src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,21 @@ export type Configuration = {
sparkHistoryServerUrl?: string;
externalLogsUrlTemplate?: string;
};

export type SessionStatementCode = {
code: string;
};

export type SessionStatement = SessionStatementCode & {
id: string;
state: 'available' | 'error' | 'waiting' | 'canceled';
output?: {
status: 'ok' | 'error';
traceback?: string;
data: Record<string, unknown>;
};
};

export type SessionStatementPage = {
statements: SessionStatement[];
};
35 changes: 35 additions & 0 deletions frontend/src/components/Statements.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {Application} from '../client/types';
import {useStatements} from '../hooks/session';
import {Spinner, useColorMode, VStack} from '@chakra-ui/react';
import {a11yLight, a11yDark} from 'react-code-blocks';
import Statement from './statement/Statement';
import React from 'react';
import StatementForm from './statement/StatementForm';

interface StatementsProps {
session: Application;
}

const Statements: React.FC<StatementsProps> = ({session}) => {
const {data: page, isLoading} = useStatements(session.id, 5, 0);
const {colorMode} = useColorMode();
const theme = colorMode === 'light' ? a11yLight : a11yDark;

if (isLoading) {
return <Spinner />;
}
if (!page?.statements.length) {
return <div>Session has no statements</div>;
}

return (
<VStack align="stretch" spacing={2}>
{page.statements.toReversed().map((statement) => (
<Statement key={statement.id} sessionId={session.id} statement={statement} theme={theme} />
))}
<StatementForm session={session} />
</VStack>
);
};

export default Statements;
49 changes: 49 additions & 0 deletions frontend/src/components/statement/Statement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, {useMemo} from 'react';
import {SessionStatement} from '../../client/types';
import {useSessionStatementCancel} from '../../hooks/session';
import {CheckIcon, CloseIcon, WarningTwoIcon} from '@chakra-ui/icons';
import {Box, Card, CardBody, Flex, IconButton, Spinner, VStack} from '@chakra-ui/react';
import {CodeBlock} from 'react-code-blocks';
import StatementOutput from './StatementOutput';

const Statement: React.FC<{sessionId: string; statement: SessionStatement; theme: any}> = ({sessionId, statement, theme}) => {
const {mutate: cancel, isLoading: isCanceling} = useSessionStatementCancel(sessionId, statement.id);

const statusIcon = useMemo(() => {
switch (statement.state) {
case 'available':
return <CheckIcon color="green.500" />;
case 'canceled':
return <CloseIcon />;
case 'error':
return <WarningTwoIcon color="red.500" />;
case 'waiting':
return <Spinner />;
}
}, [statement.state]);

return (
<Card>
<CardBody>
<VStack align="stretch" spacing={1}>
<Flex gap={2}>
<Box flex={1}>
<CodeBlock language="python" text={statement.code} theme={theme} />
<StatementOutput theme={theme} output={statement.output} />
</Box>
<Box>
<VStack>
{statusIcon}
{statement.state !== 'canceled' ? (
<IconButton onClick={() => cancel()} isLoading={isCanceling} aria-label="Cancel" icon={<CloseIcon />} />
) : null}
</VStack>
</Box>
</Flex>
</VStack>
</CardBody>
</Card>
);
};

export default Statement;
56 changes: 56 additions & 0 deletions frontend/src/components/statement/StatementForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {Button, Card, CardBody, FormControl, FormLabel, HStack, Spacer, Textarea, VStack} from '@chakra-ui/react';
import React from 'react';
import {useSessionStatementSubmit} from '../../hooks/session';
import {Application} from '../../client/types';

interface StatementFormProps {
session: Application;
}

const deadStates = ['SHUTTING_DOWN', 'ERROR', 'DEAD', 'KILLED'];

const StatementForm: React.FC<StatementFormProps> = ({session}) => {
const {mutateAsync: submit, isLoading: isSubmitting} = useSessionStatementSubmit(session.id);
const handleSubmit = (event: React.FormEvent) => {
// @ts-ignore
const code = event.target.elements.code.value;
// @ts-ignore
submit({code}).then(() => (event.target.elements.code.value = ''));
event.preventDefault();
};

const isSessionDead = deadStates.includes(session.state);

if (isSessionDead) {
return (
<Card align="center">
<CardBody>Session cannot accept new statements.</CardBody>
</Card>
);
}

return (
<form onSubmit={handleSubmit}>
<Card>
<CardBody>
<VStack align="stretch" spacing={2}>
<FormControl>
<FormLabel>New Statement</FormLabel>
<Textarea name="code" />
</FormControl>
<FormControl>
<HStack>
<Spacer />
<Button type="submit" isLoading={isSubmitting}>
Submit
</Button>
</HStack>
</FormControl>
</VStack>
</CardBody>
</Card>
</form>
);
};

export default StatementForm;
22 changes: 22 additions & 0 deletions frontend/src/components/statement/StatementOutput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import {SessionStatement} from '../../client/types';
import {CodeBlock} from 'react-code-blocks';

const StatementOutput: React.FC<{output?: SessionStatement['output']; theme: any}> = ({output, theme}) => {
if (output?.traceback) {
return <CodeBlock showLineNumbers={false} theme={theme} text={output.traceback} />;
}

if (!output?.data) {
return null;
}

const text = String(Object.values(output.data)[0]);
if (!text) {
return null;
}

return <CodeBlock showLineNumbers={false} theme={theme} text={text} />;
};

export default StatementOutput;
26 changes: 26 additions & 0 deletions frontend/src/hooks/session.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {useApi} from '../client/hooks';
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
import {SessionStatementCode} from '../client/types';

export function useSessions(size: number, from: number) {
const api = useApi();
Expand All @@ -21,3 +22,28 @@ export function useSessionLog(id: string) {
const api = useApi();
return useQuery(['logs', id], () => api.fetchSessionLog(id));
}

export function useStatements(sessionId: string, size: number, from: number) {
const api = useApi();
return useQuery(['sessions', sessionId, 'statements', size, from], () => api.fetchSessionStatements(sessionId, size, from), {
refetchInterval: (data) => (data?.statements?.some((stmt) => stmt.state === 'waiting') ? 1000 : false),
});
}

export function useSessionStatementSubmit(sessionId: string) {
const api = useApi();
const client = useQueryClient();

return useMutation((code: SessionStatementCode) => api.postSessionStatement(sessionId, code), {
onSuccess: () => client.refetchQueries(['sessions', sessionId, 'statements']),
});
}

export function useSessionStatementCancel(sessionId: string, statementId: string) {
const api = useApi();
const client = useQueryClient();

return useMutation(() => api.cancelSessionStatement(sessionId, statementId), {
onSuccess: () => client.refetchQueries(['sessions', sessionId, 'statements']),
});
}
33 changes: 24 additions & 9 deletions frontend/src/pages/Session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import React from 'react';
import {useParams} from 'react-router';
import {useSession, useSessionDelete, useSessionLog} from '../hooks/session';
import AppInfo from '../components/AppInfo';
import {Box, Spinner} from '@chakra-ui/react';
import {Box, Spinner, Tab, TabList, TabPanel, TabPanels, Tabs} from '@chakra-ui/react';
import AppLogs from '../components/AppLogs';
import AppTitle from '../components/AppTitle';
import {useNavigate} from 'react-router-dom';
import {RoutePath} from '../configuration/consts';
import Statements from '../components/Statements';

const Session: React.FC = () => {
const {id} = useParams();
Expand All @@ -31,14 +32,28 @@ const Session: React.FC = () => {
return (
<div>
<AppTitle app={session} onDelete={onDelete} />
<Box textStyle="caption" mt="5">
Logs:
</Box>
<Box mt="1">
<AppLogs logs={logs} />
</Box>

<AppInfo app={session} />
<Tabs isLazy>
<TabList>
<Tab>Info</Tab>
<Tab>Statements</Tab>
</TabList>

<TabPanels>
<TabPanel>
<Box textStyle="caption" mt="5">
Logs:
</Box>
<Box mt="1">
<AppLogs logs={logs} />
</Box>

<AppInfo app={session} />
</TabPanel>
<TabPanel>
<Statements session={session} />
</TabPanel>
</TabPanels>
</Tabs>
</div>
);
};
Expand Down
Loading

0 comments on commit 920de84

Please sign in to comment.