diff --git a/src/components/Routes/Routes.test.tsx b/src/components/Routes/Routes.test.tsx index c10c7e81..7f29bc25 100644 --- a/src/components/Routes/Routes.test.tsx +++ b/src/components/Routes/Routes.test.tsx @@ -16,14 +16,9 @@ jest.mock('react-router-dom', () => ({ useParams: jest.fn(), })) -jest.mock('pages/JobIndex/jobIndexHelpers', () => ({ - getFormattedTable: jest.fn(), -})) - describe('Routes', () => { afterAll(() => { jest.unmock('react-router-dom') - jest.unmock('pages/JobIndex/jobIndexHelpers') jest.clearAllMocks() }) diff --git a/src/components/SyncUserModal.test.tsx b/src/components/SyncUserModal.test.tsx index 6b8b7640..4c2bdf34 100644 --- a/src/components/SyncUserModal.test.tsx +++ b/src/components/SyncUserModal.test.tsx @@ -53,7 +53,7 @@ describe('SyncUser Modal', () => { ) await waitFor(() => { expect( - screen.getByText('ERROR: Error: Request failed with status code 404'), + screen.getByText('ERROR: Failure to return gardens'), ).toBeInTheDocument() }) mockAxios.onGet('/api/v1/gardens').reply(200, [TGarden]) @@ -71,7 +71,7 @@ describe('SyncUser Modal', () => { fireEvent.click(screen.getByRole('button', { name: 'Submit' })) await waitFor(() => { expect( - screen.getByText('ERROR: Error: Request failed with status code 404'), + screen.getByText('ERROR: Failure to sync users'), ).toBeInTheDocument() }) }) diff --git a/src/components/SyncUserModal.tsx b/src/components/SyncUserModal.tsx index 3b90ef5d..4ce535bc 100644 --- a/src/components/SyncUserModal.tsx +++ b/src/components/SyncUserModal.tsx @@ -115,7 +115,7 @@ const SyncUserModal = ({ open, setOpen }: ModalProps) => { .catch((e) => { setAlert({ severity: 'error', - message: e, + message: e.response.data.message || e, doNotAutoDismiss: true, }) }) @@ -146,7 +146,7 @@ const SyncUserModal = ({ open, setOpen }: ModalProps) => { .catch((e) => { setAlert({ severity: 'error', - message: e, + message: e.response.data.message || e, doNotAutoDismiss: true, }) }) diff --git a/src/components/table.tsx b/src/components/table.tsx deleted file mode 100644 index 25e32eda..00000000 --- a/src/components/table.tsx +++ /dev/null @@ -1,393 +0,0 @@ -import { - FirstPage as FirstPageIcon, - KeyboardArrowLeft, - KeyboardArrowRight, - LastPage as LastPageIcon, -} from '@mui/icons-material' -import { - Box, - CircularProgress, - IconButton, - Table, - TableBody, - TableCell, - TableContainer, - TableFooter, - TableHead, - TablePagination, - TableRow, - TextField, - Typography, -} from '@mui/material' -import { useTheme } from '@mui/material/styles' -import { AxiosResponse } from 'axios' -import PropTypes from 'prop-types' -import { BaseSyntheticEvent, useState } from 'react' -import CacheService from 'services/cache_service' -import { TableInterface } from 'types/custom-types' - -const tableCellStyle = { - borderWidth: 0.5, - borderColor: 'lightgrey', - borderStyle: 'solid', -} - -function TablePaginationActions(props: { - count: number - page: number - rowsPerPage: number - onPageChange(event: BaseSyntheticEvent | null, value: number): void -}) { - const theme = useTheme() - const { count, page, rowsPerPage, onPageChange } = props - - const handleFirstPageButtonClick = (event: BaseSyntheticEvent) => { - onPageChange(event, 0) - } - - const handleBackButtonClick = (event: BaseSyntheticEvent) => { - onPageChange(event, page - 1) - } - - const handleNextButtonClick = (event: BaseSyntheticEvent) => { - onPageChange(event, page + 1) - } - - const handleLastPageButtonClick = (event: BaseSyntheticEvent) => { - onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1)) - } - - return ( -
- - {theme.direction === 'rtl' ? : } - - - {theme.direction === 'rtl' ? ( - - ) : ( - - )} - - = Math.ceil(count / rowsPerPage) - 1} - aria-label="next page" - > - {theme.direction === 'rtl' ? ( - - ) : ( - - )} - - = Math.ceil(count / rowsPerPage) - 1} - aria-label="last page" - > - {theme.direction === 'rtl' ? : } - -
- ) -} - -TablePaginationActions.propTypes = { - count: PropTypes.number.isRequired, - onPageChange: PropTypes.func.isRequired, - page: PropTypes.number.isRequired, - rowsPerPage: PropTypes.number.isRequired, -} - -const MyTable = ({ parentState }: TableInterface) => { - let cachedState: { rowsPerPage: number } = { rowsPerPage: 5 } - - if (parentState.cacheKey) { - const newCachedState = CacheService.getIndexLastState(parentState.cacheKey) - - if ( - 'rowsPerPage' in newCachedState && - typeof newCachedState['rowsPerPage'] === 'string' - ) { - newCachedState['rowsPerPage'] = parseInt(newCachedState['rowsPerPage']) - } - cachedState = newCachedState - } - - const [data, setData] = useState< - (string | JSX.Element | number | null | undefined)[][] - >([]) - const [completeDataSet, setCompleteDataSet] = useState( - parentState.completeDataSet, - ) - const [isLoading, setLoading] = useState(true) - const [page, setPage] = useState(0) - const [rowsPerPage, setRowsPerPage] = useState( - cachedState.rowsPerPage, - ) - const [totalItemsFiltered, setTotalItemsFiltered] = useState('0') - const [totalItems, setTotalItems] = useState('0') - const [includeChildren, setIncludeChildren] = useState( - parentState.includeChildren, - ) - - function handleChangePage(event: BaseSyntheticEvent | null, newPage: number) { - if (parentState.setSearchApi) { - parentState.setSearchApi('' + newPage * rowsPerPage, 'start') - } - setPage(newPage) - setLoading(true) - } - - const onChange = (event: BaseSyntheticEvent) => { - if (parentState.setSearchApi) { - parentState.setSearchApi(event.target.value, event.target.id) - } - setPage(0) - setLoading(true) - } - - const onChangeEnd = (event: BaseSyntheticEvent) => { - if (parentState.setSearchApi) { - parentState.setSearchApi(event.target.value, event.target.id, true) - } - setPage(0) - setLoading(true) - } - - const handleChangeRowsPerPage = (event: BaseSyntheticEvent) => { - setRowsPerPage(parseInt(event.target.value, 10)) - setPage(0) - if (parentState.setSearchApi) { - parentState.setSearchApi('0', 'start') - } - setLoading(true) - if (parentState.cacheKey) { - CacheService.setItemInCache( - { - rowsPerPage: event.target.value, - }, - parentState.cacheKey, - ) - } - } - - function formatTextField(index: number) { - if (!parentState.disableSearch) { - if (parentState.tableHeads[index] === '') { - return - } else if (['Created'].includes(parentState.tableHeads[index])) { - return ( - - - - - - - - - ) - } else { - return ( - - ) - } - } - } - - function pageNav() { - if (parentState.includePageNav) { - let count: string = totalItemsFiltered - if (totalItemsFiltered === '0' || !totalItemsFiltered) { - count = totalItems - } - // Can't use getRowPageOptions here as can't call useEffect - return ( - - - - - - -
- ) - } - } - - function getTableHeader() { - if (parentState.tableHeads) { - return ( - - - {parentState.tableHeads.map((tableHead: string, index: number) => ( - - {tableHead} -
- {formatTextField(index)} -
- ))} -
-
- ) - } - } - - function updateData() { - if (!isLoading) { - setLoading(true) - } - if (parentState.apiDataCall) { - if (parentState.setSearchApi) { - parentState.setSearchApi('' + rowsPerPage, 'length') - } - - parentState.apiDataCall(page, rowsPerPage, successCallback) - setLoading(false) - } else { - if (parentState.completeDataSet && parentState.formatData) { - setLoading(false) - setData( - parentState.formatData( - parentState.completeDataSet.slice( - page * rowsPerPage, - page * rowsPerPage + rowsPerPage, - ), - ), - ) - setTotalItems('' + parentState.completeDataSet.length) - } else if (parentState.formatData) { - setLoading(false) - setData(parentState.formatData()) - } - } - } - - function successCallback(response: AxiosResponse) { - setLoading(false) - if (parentState.formatData) { - setData(parentState.formatData(response.data)) - } - setTotalItems(response.headers.recordstotal) - setTotalItemsFiltered(response.headers.recordsfiltered) - } - - if ( - isLoading || - parentState.includeChildren !== includeChildren || - parentState.completeDataSet !== completeDataSet - ) { - if (parentState.includeChildren !== includeChildren) { - setIncludeChildren(parentState.includeChildren) - setPage(0) - } - if (parentState.completeDataSet) { - setCompleteDataSet(parentState.completeDataSet) - } - updateData() - } - - function getCircularProgress() { - if (isLoading) { - return - } - } - - return ( - - - - {getTableHeader()} - - {data.map((items, index: number) => ( - - {items.map((item, itemIndex: number) => ( - - {typeof item === 'string' || typeof item === 'number' ? ( - {item} - ) : ( - item - )} - - ))} - - ))} - -
-
- - {getCircularProgress()} - {pageNav()} - -
- ) -} -export default MyTable diff --git a/src/hooks/useJobs.tsx b/src/hooks/useJobs.tsx index 3ba0e807..cd71dbc5 100644 --- a/src/hooks/useJobs.tsx +++ b/src/hooks/useJobs.tsx @@ -1,4 +1,4 @@ -import { AxiosRequestConfig } from 'axios' +import { AxiosPromise, AxiosRequestConfig } from 'axios' import useAxios from 'axios-hooks' import { ServerConfigContainer } from 'containers/ConfigContainer' import { useMyAxios } from 'hooks/useMyAxios' @@ -11,8 +11,8 @@ const useJobs = () => { const { axiosManualOptions } = useMyAxios() const [, execute] = useAxios({}, axiosManualOptions) - const getJobs = () => { - const config: AxiosRequestConfig = { + const getJobs = (): AxiosPromise => { + const config: AxiosRequestConfig = { url: JOBS_URL, method: 'get', withCredentials: authEnabled, @@ -20,8 +20,8 @@ const useJobs = () => { return execute(config) } - const getJob = (id: string) => { - const config: AxiosRequestConfig = { + const getJob = (id: string): AxiosPromise => { + const config: AxiosRequestConfig = { url: `${JOBS_URL}/${id}`, method: 'get', withCredentials: authEnabled, diff --git a/src/pages/JobIndex/JobIndex.test.tsx b/src/pages/JobIndex/JobIndex.test.tsx index eeaca683..c84759c4 100644 --- a/src/pages/JobIndex/JobIndex.test.tsx +++ b/src/pages/JobIndex/JobIndex.test.tsx @@ -1,19 +1,60 @@ import { render, screen, waitFor } from '@testing-library/react' import { mockAxios, regexUsers } from 'test/axios-mock' -import { TServerAuthConfig } from 'test/test-values' -import { LoggedInProviders } from 'test/testMocks' +import { TJob, TServerAuthConfig } from 'test/test-values' +import { AllProviders, LoggedInProviders } from 'test/testMocks' import { TAdmin, TUser } from 'test/user-test-values' import { JobIndex } from './JobIndex' -jest.mock('pages/JobIndex/jobIndexHelpers', () => ({ - getFormattedTable: jest.fn(), -})) - describe('JobIndex', () => { - afterAll(() => { - jest.unmock('pages/JobIndex/jobIndexHelpers') - jest.clearAllMocks() + test('displays table with job data', async () => { + render( + + + , + ) + await waitFor(() => { + expect( + screen.getByRole('heading', { name: 'Request Scheduler' }), + ).toBeInTheDocument() + }) + expect(screen.getByText(TJob.name)).toBeInTheDocument() + expect(screen.getByText(TJob.request_template.system)).toBeInTheDocument() + expect(screen.getByText(TJob.request_template.command)).toBeInTheDocument() + expect(screen.getByText(TJob.success_count as number)).toBeInTheDocument() + }) + + test('name and system are links', async () => { + render( + + + , + ) + await waitFor(() => { + expect(screen.getByText(TJob.name)).toBeInTheDocument() + }) + const links: HTMLAnchorElement[] = screen.getAllByRole('link') + expect(links[0].textContent).toEqual(TJob.name) + expect(links[0].href).toContain(`http://localhost/#/jobs/${TJob.id}`) + expect(links[1].textContent).toEqual(TJob.request_template.system) + expect(links[1].href).toContain( + `http://localhost/#/jobs/${TJob.request_template.namespace}/${TJob.request_template.system}`, + ) + }) + + test('alerts on failure to get jobs', async () => { + mockAxios + .onGet('/api/v1/jobs') + .reply(404, { message: 'Failure to get jobs' }) + render( + + + , + ) + await waitFor(() => { + expect(screen.getByText('ERROR: Failure to get jobs')).toBeInTheDocument() + }) + expect(screen.queryByText(TJob.name)).not.toBeInTheDocument() }) test('create button if permission', async () => { diff --git a/src/pages/JobIndex/JobIndex.tsx b/src/pages/JobIndex/JobIndex.tsx index d7656375..e1dd5a5a 100644 --- a/src/pages/JobIndex/JobIndex.tsx +++ b/src/pages/JobIndex/JobIndex.tsx @@ -1,45 +1,83 @@ -import { Box, Button } from '@mui/material' -import useAxios from 'axios-hooks' +import { Button } from '@mui/material' import { Divider } from 'components/Divider' import { PageHeader } from 'components/PageHeader' -import { ServerConfigContainer } from 'containers/ConfigContainer' +import { Snackbar } from 'components/Snackbar' +import { Table } from 'components/Table' import { PermissionsContainer } from 'containers/PermissionsContainer' -import { getFormattedTable } from 'pages/JobIndex/jobIndexHelpers' -import { useEffect, useState } from 'react' -import { useNavigate } from 'react-router-dom' +import { useJobs } from 'hooks/useJobs' +import { JobTableData, useJobColumns } from 'pages/JobIndex' +import { useEffect, useMemo, useState } from 'react' +import { Link, useNavigate } from 'react-router-dom' import { Job } from 'types/backend-types' +import { SnackbarState } from 'types/custom-types' +import { dateFormatted } from 'utils/date-formatter' const JobIndex = () => { - const { authEnabled } = ServerConfigContainer.useContainer() const { hasPermission } = PermissionsContainer.useContainer() const [jobs, setJobs] = useState([]) - const [{ data, error }] = useAxios({ - url: '/api/v1/jobs', - method: 'get', - withCredentials: authEnabled, - }) + const [alert, setAlert] = useState(undefined) + const { getJobs } = useJobs() const navigate = useNavigate() useEffect(() => { - if (data && !error) { - setJobs(data) - } - }, [data, error]) + getJobs() + .then((response) => { + setJobs(response.data) + }) + .catch((e) => { + setAlert({ + severity: 'error', + message: e.response.data.message || e, + doNotAutoDismiss: true, + }) + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) // temporary for demo const createRequestOnClick = () => { navigate('/jobs/create') } + const jobData = useMemo((): JobTableData[] => { + return jobs.map((job: Job): JobTableData => { + return { + name: ( + + {job.name} + + ), + status: job.status || '', + system: ( + + {job.request_template.system} + + ), + instance: job.request_template.instance_name, + command: job.request_template.command, + nextRun: job.next_run_time + ? dateFormatted(new Date(job.next_run_time)) + : '', + success: job.success_count || 0, + error: job.error_count || 0, + } + }) + }, [jobs]) + return ( - + <> - {hasPermission('job:create') && ( - - )} - {getFormattedTable(jobs)} - + + {hasPermission('job:create') && ( + + )} +
+ {alert && } + ) } diff --git a/src/pages/JobIndex/index.ts b/src/pages/JobIndex/index.ts index bedc98d3..557fb520 100644 --- a/src/pages/JobIndex/index.ts +++ b/src/pages/JobIndex/index.ts @@ -1 +1,2 @@ export { JobIndex as default } from './JobIndex' +export * from './jobIndexHelpers' diff --git a/src/pages/JobIndex/jobIndexHelpers.tsx b/src/pages/JobIndex/jobIndexHelpers.tsx index 0323a9a4..79089e9f 100644 --- a/src/pages/JobIndex/jobIndexHelpers.tsx +++ b/src/pages/JobIndex/jobIndexHelpers.tsx @@ -1,68 +1,85 @@ -import RequestTable from 'components/table' -import { Link as RouterLink } from 'react-router-dom' -import { Job } from 'types/backend-types' +import { DefaultCellRenderer } from 'components/Table/defaults' +import { useMemo } from 'react' +import { Column } from 'react-table' +import { ObjectWithStringKeys } from 'types/custom-types' -const tableHeads = [ - 'Job Name', - 'Status', - 'System', - 'Instance', - 'Command', - 'Next Run Time', - 'Success Count', - 'Error Count', -] -const formatJobs = (jobs: Job[]) => { - const formattedJobs: (string | JSX.Element | number | null | undefined)[][] = - [] - - for (const job of jobs) { - const { - name, - id, - status, - request_template: { - system, - namespace, - instance_name: instanceName, - command, - }, - next_run_time: nextRunTime, - success_count: successes, - error_count: errors, - } = job - - const formattedJob = [ - - {name} - , - status as string, - , - instanceName, - command, - new Date(nextRunTime as number).toString(), - successes || 0, - errors || 0, - ] - - formattedJobs.push(formattedJob) - } - return formattedJobs +export interface JobTableData extends ObjectWithStringKeys { + name: JSX.Element + status: string + system: JSX.Element + instance: string + command: string + nextRun: string + success: number + error: number } -const getFormattedTable = (jobs: Job[]) => { - return ( - +export const useJobColumns = () => { + return useMemo[]>( + () => [ + { + Header: 'Name', + Cell: DefaultCellRenderer, + accessor: 'name', + filter: 'fuzzyText', + minWidth: 120, + maxWidth: 180, + width: 130, + }, + { + Header: 'Status', + accessor: 'status', + minWidth: 120, + maxWidth: 180, + width: 130, + }, + { + Header: 'System', + Cell: DefaultCellRenderer, + accessor: 'system', + filter: 'fuzzyText', + minWidth: 120, + maxWidth: 180, + width: 130, + }, + { + Header: 'Instance', + accessor: 'instance', + filter: 'fuzzyText', + minWidth: 120, + maxWidth: 180, + width: 130, + }, + { + Header: 'Command', + accessor: 'command', + filter: 'fuzzyText', + minWidth: 130, + maxWidth: 180, + width: 140, + }, + { + Header: 'Next Run Time', + accessor: 'nextRun', + minWidth: 200, + maxWidth: 300, + width: 250, + }, + { + Header: 'Success Count', + accessor: 'success', + minWidth: 120, + maxWidth: 180, + width: 145, + }, + { + Header: 'Error Count', + accessor: 'error', + minWidth: 120, + maxWidth: 180, + width: 130, + }, + ], + [], ) } - -export { formatJobs, getFormattedTable } diff --git a/src/pages/JobView/JobView.tsx b/src/pages/JobView/JobView.tsx index 7fed5999..8dc3cdff 100644 --- a/src/pages/JobView/JobView.tsx +++ b/src/pages/JobView/JobView.tsx @@ -6,7 +6,7 @@ import { Stack, Typography, } from '@mui/material' -import { AxiosResponse } from 'axios' +import { AxiosError, AxiosResponse } from 'axios' import { Divider } from 'components/Divider' import { useJobRequestCreation } from 'components/JobRequestCreation' import { JsonCard } from 'components/JsonCard' @@ -77,10 +77,10 @@ const JobView = () => { ) } - const errorHandler = (e: string) => { + const errorHandler = (e: AxiosError) => { setAlert({ severity: 'error', - message: e, + message: e.response?.data.message, doNotAutoDismiss: true, }) } diff --git a/src/pages/RequestView/RequestViewTable.tsx b/src/pages/RequestView/RequestViewTable.tsx index 578aaf57..249e9699 100644 --- a/src/pages/RequestView/RequestViewTable.tsx +++ b/src/pages/RequestView/RequestViewTable.tsx @@ -13,8 +13,7 @@ import { } from 'pages/RequestsIndex/data' import { useState } from 'react' import { Request } from 'types/backend-types' - -import {dateFormatted} from './requestViewHelpers' +import { dateFormatted } from 'utils/date-formatter' interface RequestViewTableProps { request: Request diff --git a/src/pages/RequestView/requestViewHelpers.tsx b/src/pages/RequestView/requestViewHelpers.tsx index 2d386ee6..fe96d97d 100644 --- a/src/pages/RequestView/requestViewHelpers.tsx +++ b/src/pages/RequestView/requestViewHelpers.tsx @@ -1,24 +1,10 @@ import NavigateNextIcon from '@mui/icons-material/NavigateNext' import { Breadcrumbs, CircularProgress } from '@mui/material' import { SupportedColorScheme } from '@mui/material/styles' -import { DateTimeFormatOptions } from 'luxon' import ReactJson from 'react-json-view' import { Link as RouterLink } from 'react-router-dom' import { Request } from 'types/backend-types' -const dateFormatted = (date: Date) => { - const options: DateTimeFormatOptions = { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: 'numeric', - second: 'numeric', - minute: 'numeric', - hour12: false - } - return date.toLocaleString(undefined, options) -} - const outputFormatted = ( request: Request, theme: SupportedColorScheme, @@ -80,4 +66,4 @@ const getParentLinks = (request: Request): JSX.Element => { ) } -export { dateFormatted,getParentLinks, outputFormatted } +export { getParentLinks, outputFormatted } diff --git a/src/services/cache_service.ts b/src/services/cache_service.ts deleted file mode 100644 index 0e459242..00000000 --- a/src/services/cache_service.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Request } from 'types/backend-types' - -type CachedStates = { - rowsPerPage?: number - request?: Request - requestQueue?: Request[] - namespacesSelected?: string[] -} - -function getItem(key: string): CachedStates { - const lastKnownState: string | null = window.localStorage.getItem(key) - let parsedState: CachedStates = {} - if (lastKnownState) { - parsedState = JSON.parse(lastKnownState) - } - return parsedState -} - -const CacheService = { - getIndexLastState(key: string): { rowsPerPage: number } { - const item = getItem(key) - return { rowsPerPage: item.rowsPerPage || 5 } - }, - - getNamespacesSelected( - key: string, - defaultValue: string[] = [], - ): { namespacesSelected: string[] } { - const item = getItem(key) - return { - namespacesSelected: item.namespacesSelected || defaultValue, - } - }, - - popQueue(key: string): Request | undefined | void { - const requestQueue = getItem(key).requestQueue - if (requestQueue) { - const request = requestQueue.pop() - this.setItemInCache({ requestQueue: requestQueue }, key) - return request - } - }, - - pushQueue(item: Request, key: string): void { - const requestQueue: Request[] = getItem(key).requestQueue || [] - if (requestQueue) { - requestQueue.push(item) - this.setItemInCache({ requestQueue: requestQueue }, key) - } - }, - - setItemInCache(item: unknown, key: string): void { - window.localStorage.setItem(key, JSON.stringify(item)) - }, -} - -export default CacheService diff --git a/src/test/test-values.ts b/src/test/test-values.ts index c3e9d716..ccfdb82a 100644 --- a/src/test/test-values.ts +++ b/src/test/test-values.ts @@ -112,6 +112,8 @@ export const TJob: Job = { request_template: { namespace: TSystem.namespace, system: TSystem.name, + command: TCommand.name, + instance_name: TInstance.name, system_version: TSystem.version, } as RequestTemplate, status: 'RUNNING', diff --git a/src/test/testMocks.tsx b/src/test/testMocks.tsx index 4d23d270..a69d7de4 100644 --- a/src/test/testMocks.tsx +++ b/src/test/testMocks.tsx @@ -49,17 +49,19 @@ export const AllProviders = ({ children }: ProviderMocks) => { export const MemoryProvider = ({ children, startLocation }: ProviderMocks) => { return ( - - - - - - LOADING...}>{children} - - - - - + + + + + + + LOADING...}>{children} + + + + + + ) } @@ -80,19 +82,21 @@ export const LoggedInMemory = ({ }: ProviderMocks) => { return ( - - - - - - - LOADING...}>{children} - - - - - - + + + + + + + + LOADING...}>{children} + + + + + + + ) } diff --git a/src/types/custom-types.ts b/src/types/custom-types.ts index 887855d3..9542070e 100644 --- a/src/types/custom-types.ts +++ b/src/types/custom-types.ts @@ -94,10 +94,6 @@ export interface CommandParams { version: string } -export interface TableInterface { - parentState: TableState -} - export interface AugmentedCommand extends Command { namespace: string systemName: string diff --git a/src/utils/date-formatter.ts b/src/utils/date-formatter.ts new file mode 100644 index 00000000..df9ecd8c --- /dev/null +++ b/src/utils/date-formatter.ts @@ -0,0 +1,14 @@ +import { DateTimeFormatOptions } from 'luxon' + +export const dateFormatted = (date: Date) => { + const options: DateTimeFormatOptions = { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: 'numeric', + second: 'numeric', + minute: 'numeric', + hour12: false, + } + return date.toLocaleString(undefined, options) +}