From 74e78508ba438065f12478424470d76aa5ebdb07 Mon Sep 17 00:00:00 2001 From: Paulius Dambrauskas Date: Sat, 7 Oct 2023 18:03:55 +0300 Subject: [PATCH] Add Session statements UI --- frontend/package.json | 1 + frontend/src/client/client.ts | 14 +- frontend/src/client/types.ts | 18 + frontend/src/components/Statements.tsx | 35 + .../src/components/statement/Statement.tsx | 49 ++ .../components/statement/StatementForm.tsx | 56 ++ .../components/statement/StatementOutput.tsx | 22 + frontend/src/hooks/session.ts | 26 + frontend/src/pages/Session.tsx | 33 +- frontend/yarn.lock | 817 +++++++++++++++++- server/build.gradle | 10 +- .../exacaster/lighter/backend/SparkApp.java | 2 +- .../src/main/resources/application-local.yml | 2 + 13 files changed, 1063 insertions(+), 22 deletions(-) create mode 100644 frontend/src/components/Statements.tsx create mode 100644 frontend/src/components/statement/Statement.tsx create mode 100644 frontend/src/components/statement/StatementForm.tsx create mode 100644 frontend/src/components/statement/StatementOutput.tsx create mode 100644 server/src/main/resources/application-local.yml diff --git a/frontend/package.json b/frontend/package.json index f8733ebd..bfd0e1df 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/client/client.ts b/frontend/src/client/client.ts index 0bfd5631..7fea6ced 100644 --- a/frontend/src/client/client.ts +++ b/frontend/src/client/client.ts @@ -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; @@ -51,4 +51,16 @@ export class Api { fetchConfiguration(): Promise { return this.get('/api/configuration'); } + + fetchSessionStatements(sessionId: string, size: number, from: number): Promise { + return this.get(`/api/sessions/${sessionId}/statements?size=${size}&from=${from}`); + } + + postSessionStatement(sessionId: string, statement: SessionStatementCode): Promise { + return this.client.post(`/api/sessions/${sessionId}/statements`, statement); + } + + cancelSessionStatement(sessionId: string, statementId: string): Promise { + return this.client.post(`/api/sessions/${sessionId}/statements/${statementId}/cancel`); + } } diff --git a/frontend/src/client/types.ts b/frontend/src/client/types.ts index b6f75290..e21c868c 100644 --- a/frontend/src/client/types.ts +++ b/frontend/src/client/types.ts @@ -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; + }; +}; + +export type SessionStatementPage = { + statements: SessionStatement[]; +}; diff --git a/frontend/src/components/Statements.tsx b/frontend/src/components/Statements.tsx new file mode 100644 index 00000000..f20d8ff1 --- /dev/null +++ b/frontend/src/components/Statements.tsx @@ -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 = ({session}) => { + const {data: page, isLoading} = useStatements(session.id, 5, 0); + const {colorMode} = useColorMode(); + const theme = colorMode === 'light' ? a11yLight : a11yDark; + + if (isLoading) { + return ; + } + if (!page?.statements.length) { + return
Session has no statements
; + } + + return ( + + {page.statements.toReversed().map((statement) => ( + + ))} + + + ); +}; + +export default Statements; diff --git a/frontend/src/components/statement/Statement.tsx b/frontend/src/components/statement/Statement.tsx new file mode 100644 index 00000000..ada2852f --- /dev/null +++ b/frontend/src/components/statement/Statement.tsx @@ -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 ; + case 'canceled': + return ; + case 'error': + return ; + case 'waiting': + return ; + } + }, [statement.state]); + + return ( + + + + + + + + + + + {statusIcon} + {statement.state !== 'canceled' ? ( + cancel()} isLoading={isCanceling} aria-label="Cancel" icon={} /> + ) : null} + + + + + + + ); +}; + +export default Statement; diff --git a/frontend/src/components/statement/StatementForm.tsx b/frontend/src/components/statement/StatementForm.tsx new file mode 100644 index 00000000..2f60d2fc --- /dev/null +++ b/frontend/src/components/statement/StatementForm.tsx @@ -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 = ({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 ( + + Session cannot accept new statements. + + ); + } + + return ( +
+ + + + + New Statement +