diff --git a/public/icons/job/preheat/created-at.svg b/public/icons/job/preheat/created-at.svg new file mode 100644 index 00000000..90a4cce0 --- /dev/null +++ b/public/icons/job/preheat/created-at.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/job/preheat/description.svg b/public/icons/job/preheat/description.svg new file mode 100644 index 00000000..c1adfe66 --- /dev/null +++ b/public/icons/job/preheat/description.svg @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/public/icons/job/preheat/error-log.svg b/public/icons/job/preheat/error-log.svg new file mode 100644 index 00000000..355c3413 --- /dev/null +++ b/public/icons/job/preheat/error-log.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/public/icons/job/preheat/failure.svg b/public/icons/job/preheat/failure.svg new file mode 100644 index 00000000..977efa8f --- /dev/null +++ b/public/icons/job/preheat/failure.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/public/icons/job/preheat/filter.svg b/public/icons/job/preheat/filter.svg new file mode 100644 index 00000000..dc377243 --- /dev/null +++ b/public/icons/job/preheat/filter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/job/preheat/headers.svg b/public/icons/job/preheat/headers.svg new file mode 100644 index 00000000..84a30109 --- /dev/null +++ b/public/icons/job/preheat/headers.svg @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/public/icons/job/preheat/id.svg b/public/icons/job/preheat/id.svg new file mode 100644 index 00000000..f2f864a0 --- /dev/null +++ b/public/icons/job/preheat/id.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/job/preheat/job.svg b/public/icons/job/preheat/job.svg new file mode 100644 index 00000000..ed3405bd --- /dev/null +++ b/public/icons/job/preheat/job.svg @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/public/icons/job/preheat/pending.svg b/public/icons/job/preheat/pending.svg new file mode 100644 index 00000000..1e68bb85 --- /dev/null +++ b/public/icons/job/preheat/pending.svg @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/public/icons/job/preheat/status-failure.svg b/public/icons/job/preheat/status-failure.svg new file mode 100644 index 00000000..b8f6a266 --- /dev/null +++ b/public/icons/job/preheat/status-failure.svg @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/public/icons/job/preheat/status-pending.svg b/public/icons/job/preheat/status-pending.svg new file mode 100644 index 00000000..c37ff19a --- /dev/null +++ b/public/icons/job/preheat/status-pending.svg @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/public/icons/job/preheat/status-success.svg b/public/icons/job/preheat/status-success.svg new file mode 100644 index 00000000..db1bd8cc --- /dev/null +++ b/public/icons/job/preheat/status-success.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/public/icons/job/preheat/status.svg b/public/icons/job/preheat/status.svg new file mode 100644 index 00000000..63d9f6aa --- /dev/null +++ b/public/icons/job/preheat/status.svg @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/public/icons/job/preheat/success.svg b/public/icons/job/preheat/success.svg new file mode 100644 index 00000000..d1ca9400 --- /dev/null +++ b/public/icons/job/preheat/success.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/public/icons/job/preheat/tag.svg b/public/icons/job/preheat/tag.svg new file mode 100644 index 00000000..5618d247 --- /dev/null +++ b/public/icons/job/preheat/tag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons/job/preheat/url.svg b/public/icons/job/preheat/url.svg new file mode 100644 index 00000000..3e03b8ac --- /dev/null +++ b/public/icons/job/preheat/url.svg @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/src/components/clusters/show.tsx b/src/components/clusters/show.tsx index 8b4b7fa1..32af45a9 100644 --- a/src/components/clusters/show.tsx +++ b/src/components/clusters/show.tsx @@ -635,7 +635,7 @@ export default function ShowCluster() { - + @@ -913,7 +913,7 @@ export default function ShowCluster() {
- + diff --git a/src/components/developer/tokens/edit.tsx b/src/components/developer/tokens/edit.tsx index cb4fc907..e4b78695 100644 --- a/src/components/developer/tokens/edit.tsx +++ b/src/components/developer/tokens/edit.tsx @@ -238,7 +238,6 @@ export default function UpdateTokens() { - + Expiration diff --git a/src/components/developer/tokens/index.tsx b/src/components/developer/tokens/index.tsx index 86d33c2b..282ed630 100644 --- a/src/components/developer/tokens/index.tsx +++ b/src/components/developer/tokens/index.tsx @@ -173,8 +173,8 @@ export default function PersonalAccessTokens() { - Developer - Personal access tokens + developer + personal access tokens Personal access tokens @@ -246,7 +246,6 @@ export default function PersonalAccessTokens() { ) : ( <> )} - {tokens.length === 0 ? ( )} - {tokensTotalPages > 1 ? ( )} - { diff --git a/src/components/developer/tokens/new.tsx b/src/components/developer/tokens/new.tsx index b5870730..270db585 100644 --- a/src/components/developer/tokens/new.tsx +++ b/src/components/developer/tokens/new.tsx @@ -55,6 +55,7 @@ export default function CreateTokens() { id: 'name', label: 'Name', name: 'name', + required: true, autoComplete: 'family-name', placeholder: 'Enter your token name', helperText: nameError ? 'Fill in the characters, the length is 1-100.' : '', @@ -236,7 +237,6 @@ export default function CreateTokens() { - + Expiration diff --git a/src/components/job/preheats/index.tsx b/src/components/job/preheats/index.tsx new file mode 100644 index 00000000..93a89ae0 --- /dev/null +++ b/src/components/job/preheats/index.tsx @@ -0,0 +1,432 @@ +import { + Button, + Paper, + Typography, + Box, + Pagination, + ThemeProvider, + createTheme, + Chip, + Link as RouterLink, + Divider, + FormControl, + MenuItem, + Select, + InputLabel, + Snackbar, + Alert, + Skeleton, + Breadcrumbs, +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import MoreTimeIcon from '@mui/icons-material/MoreTime'; +import { useNavigate, Link } from 'react-router-dom'; +import { useContext, useEffect, useState } from 'react'; +import { getJobs } from '../../../lib/api'; +import { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE } from '../../../lib/constants'; +import { getDatetime, getPaginatedList } from '../../../lib/utils'; +import { MyContext } from '../../menu/index'; + +export default function Preheats() { + const [errorMessage, setErrorMessage] = useState(false); + const [errorMessageText, setErrorMessageText] = useState(''); + const [preheatPage, setPreheatPage] = useState(1); + const [isLoading, setIsLoading] = useState(false); + const [status, setStatus] = useState('ALL'); + const [shouldPoll, setShouldPoll] = useState(false); + const [openStatusSelect, setOpenStatusSelect] = useState(false); + const [allPreheats, setAllPreheats] = useState([ + { + id: 0, + created_at: '', + updated_at: '', + is_del: 0, + task_id: '', + bio: '', + type: '', + state: '', + args: { + filter: '', + headers: {}, + tag: '', + type: '', + url: '', + }, + }, + ]); + + const user = useContext(MyContext); + const navigate = useNavigate(); + + const theme = createTheme({ + palette: { + primary: { + main: '#1C293A', + }, + }, + typography: { + fontFamily: 'mabry-light,sans-serif', + }, + }); + + useEffect(() => { + (async function () { + try { + setIsLoading(true); + + if (user.name === 'root') { + const jobs = await getJobs({ + page: 1, + per_page: MAX_PAGE_SIZE, + state: status === 'ALL' ? undefined : status, + }); + + setAllPreheats(jobs.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())); + + const states = jobs.filter((obj) => obj.state !== 'SUCCESS' && obj.state !== 'FAILURE').length; + states === 0 ? setShouldPoll(false) : setShouldPoll(true); + + setIsLoading(false); + } else if (user.name !== '') { + const jobs = await getJobs({ + page: 1, + per_page: MAX_PAGE_SIZE, + state: status === 'ALL' ? undefined : status, + user_id: String(user.id), + }); + + setAllPreheats(jobs.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())); + + const states = jobs.filter((obj) => obj.state !== 'SUCCESS' && obj.state !== 'FAILURE').length; + states === 0 ? setShouldPoll(false) : setShouldPoll(true); + + setIsLoading(false); + } + } catch (error) { + if (error instanceof Error) { + setErrorMessage(true); + setErrorMessageText(error.message); + } + } + })(); + }, [status, user]); + + useEffect(() => { + if (shouldPoll) { + const pollingInterval = setInterval(() => { + const pollPreheat = async () => { + try { + if (user.name === 'root') { + const jobs = await getJobs({ + page: 1, + per_page: MAX_PAGE_SIZE, + state: status === 'ALL' ? undefined : status, + }); + setAllPreheats(jobs.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())); + + const states = jobs.filter((obj) => obj.state !== 'SUCCESS' && obj.state !== 'FAILURE').length; + states === 0 ? setShouldPoll(false) : setShouldPoll(true); + } else if (user.name !== '') { + const jobs = await getJobs({ + page: 1, + per_page: MAX_PAGE_SIZE, + state: status === 'ALL' ? undefined : status, + user_id: String(user.id), + }); + setAllPreheats(jobs.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())); + + const states = jobs.filter((obj) => obj.state !== 'SUCCESS' && obj.state !== 'FAILURE').length; + states === 0 ? setShouldPoll(false) : setShouldPoll(true); + } + } catch (error) { + if (error instanceof Error) { + setErrorMessage(true); + setErrorMessageText(error.message); + } + } + }; + + pollPreheat(); + }, 3000); + + return () => { + clearInterval(pollingInterval); + }; + } + }, [status, shouldPoll, user]); + + const statusList = [ + { lable: 'Pending', name: 'PENDING' }, + { lable: 'All', name: 'ALL' }, + { lable: 'Success', name: 'SUCCESS' }, + { lable: 'Failure', name: 'FAILURE' }, + ]; + + const totalPage = Math.ceil(allPreheats.length / DEFAULT_PAGE_SIZE); + const currentPageData = getPaginatedList(allPreheats, preheatPage, DEFAULT_PAGE_SIZE); + + const changeStatus = (event: any) => { + setStatus(event.target.value); + setShouldPoll(true); + setPreheatPage(1); + }; + + const handleClose = (_event: any, reason?: string) => { + if (reason === 'clickaway') { + return; + } + + setErrorMessage(false); + }; + + return ( + + + + {errorMessageText} + + + + jobs + preheats + + + Preheat + + + + + + Workflow runs + + + Status + + + + + {currentPageData.length === 0 ? ( + + You don't have any preheat tasks. + + ) : ( + <> + {Array.isArray(currentPageData) && + currentPageData.map((item, index) => { + return index !== currentPageData.length - 1 ? ( + + + + {isLoading ? ( + + ) : item.state === 'SUCCESS' ? ( + + ) : item.state === 'FAILURE' ? ( + + ) : ( + + )} + + {isLoading ? ( + + ) : ( + + {item.id} + + )} + + {isLoading ? ( + + ) : ( + {item.bio || '-'} + )} + + + + {isLoading ? ( + + ) : ( + } + label={getDatetime(item.created_at) || '-'} + variant="outlined" + size="small" + /> + )} + + + {isLoading ? ( + + ) : ( + + + + )} + + + + + ) : ( + + + {isLoading ? ( + + ) : item.state === 'SUCCESS' ? ( + + ) : item.state === 'FAILURE' ? ( + + ) : ( + + )} + + {isLoading ? ( + + ) : ( + + {item.id} + + )} + + {isLoading ? ( + + ) : ( + {item.bio || '-'} + )} + + + + {isLoading ? ( + + ) : ( + } + label={getDatetime(item.created_at) || '-'} + variant="outlined" + size="small" + /> + )} + + + {isLoading ? ( + + ) : ( + + + + )} + + + ); + })} + + )} + + {totalPage > 1 ? ( + + { + setPreheatPage(newPage); + }} + boundaryCount={1} + color="primary" + size="small" + /> + + ) : ( + <> + )} + + ); +} diff --git a/src/components/job/preheats/new.module.css b/src/components/job/preheats/new.module.css new file mode 100644 index 00000000..7931ee5e --- /dev/null +++ b/src/components/job/preheats/new.module.css @@ -0,0 +1,20 @@ +.textField { + width: 20rem; +} + +.filterInput { + width: 40rem; +} + +.headersKeyInput { + width: 20rem; + margin-right: 0.6rem !important; +} + +.headersValueInput { + width: 20rem; +} + +.title { + margin-bottom: 0.5rem; +} diff --git a/src/components/job/preheats/new.tsx b/src/components/job/preheats/new.tsx new file mode 100644 index 00000000..b4e88ccc --- /dev/null +++ b/src/components/job/preheats/new.tsx @@ -0,0 +1,680 @@ +import { + Tooltip, + TextField, + Box, + Typography, + FormControl, + Button, + IconButton, + Autocomplete, + InputLabel, + Select, + OutlinedInput, + MenuItem, + Checkbox, + ListItemText, + createTheme, + ThemeProvider, + FormHelperText, + Grid, + Chip, + Divider, + Snackbar, + Alert, + Paper, +} from '@mui/material'; +import { useContext, useEffect, useState } from 'react'; +import HelpIcon from '@mui/icons-material/Help'; +import { LoadingButton } from '@mui/lab'; +import CancelIcon from '@mui/icons-material/Cancel'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import ClearIcon from '@mui/icons-material/Clear'; +import { useNavigate } from 'react-router-dom'; +import { createJob, getClusters } from '../../../lib/api'; +import { MAX_PAGE_SIZE } from '../../../lib/constants'; +import styles from './new.module.css'; +import { MyContext } from '../../menu/index'; +import AddIcon from '@mui/icons-material/Add'; + +export default function NewPreheat() { + const [successMessage, setSuccessMessage] = useState(false); + const [errorMessage, setErrorMessage] = useState(false); + const [errorMessageText, setErrorMessageText] = useState(''); + const [bioError, setBioError] = useState(false); + const [urlError, setURLError] = useState(false); + const [tagError, setTagError] = useState(false); + const [filterError, setFilterError] = useState(false); + const [loadingButton, setLoadingButton] = useState(false); + const [headers, setheaders] = useState>([]); + const [cluster, setCluster] = useState([{ id: 0, name: '' }]); + const [clusterError, setClusterError] = useState(false); + const [filter, setFilter] = useState([]); + const [clusterName, setClusterName] = useState([]); + const [clusterID, setClusterID] = useState([]); + + const navigate = useNavigate(); + const user = useContext(MyContext); + + const theme = createTheme({ + palette: { + secondary: { + main: '#1c293a', + }, + }, + }); + + useEffect(() => { + (async function () { + const cluster = await getClusters({ page: 1, per_page: MAX_PAGE_SIZE }); + setCluster(cluster.data); + })(); + }, []); + + const informationForm = [ + { + formProps: { + id: 'bio', + label: 'Description', + name: 'bio', + multiline: true, + autoComplete: 'family-name', + placeholder: 'Enter your description', + helperText: bioError ? 'Fill in the characters, the length is 0-1000.' : '', + error: bioError, + InputProps: { + endAdornment: ( + + + + ), + }, + onChange: (e: any) => { + changeValidate(e.target.value, informationForm[0]); + }, + }, + syncError: false, + setError: setBioError, + + validate: (value: string) => { + const reg = /^.{0,1000}$/; + return reg.test(value); + }, + }, + ]; + + const argsForm = [ + { + formProps: { + id: 'url', + label: 'URL', + name: 'url', + required: true, + autoComplete: 'family-name', + placeholder: 'Enter your URL', + helperText: urlError ? 'Fill in the characters, the length is 1-1000.' : '', + error: urlError, + InputProps: { + endAdornment: ( + + + + ), + }, + onChange: (e: any) => { + changeValidate(e.target.value, argsForm[0]); + }, + }, + syncError: false, + setError: setURLError, + validate: (value: string) => { + const reg = /^(?:https?|ftp):\/\/[^\s/$.?#].[^\s]*.{0,1000}$/; + return reg.test(value); + }, + }, + { + formProps: { + id: 'tag', + label: 'Tag', + name: 'tag', + autoComplete: 'family-name', + placeholder: 'Enter your tag', + helperText: tagError ? 'Fill in the characters, the length is 0-1000.' : '', + error: tagError, + InputProps: { + endAdornment: ( + + + + ), + }, + onChange: (e: any) => { + changeValidate(e.target.value, argsForm[1]); + }, + }, + syncError: false, + setError: setTagError, + + validate: (value: string) => { + const reg = /^.{0,1000}$/; + return reg.test(value); + }, + }, + { + name: 'filter', + label: 'Filter', + filterFormProps: { + value: filter, + options: [], + onChange: (_e: any, newValue: any) => { + if (!argsForm[2].formProps.error) { + setFilter(newValue); + } + }, + onInputChange: (e: any) => { + changeValidate(e.target.value, argsForm[2]); + }, + + renderTags: (value: any, getTagProps: any) => + value.map((option: any, index: any) => ( + + )), + }, + + formProps: { + id: 'filter', + label: 'Filter', + name: 'filter', + placeholder: 'Press the Enter key to confirm the entered value', + error: filterError, + helperText: filterError ? 'Fill in the characters, the length is 0-100.' : '', + + onKeyDown: (e: any) => { + if (e.keyCode === 13) { + e.preventDefault(); + } + }, + }, + + syncError: false, + setError: setFilterError, + + validate: (value: string) => { + const reg = /^(.{0,100})$/; + return reg.test(value); + }, + }, + ]; + + const handleSelectCluster = (event: any) => { + const selectedValues = event.target.value; + const selectedNames = selectedValues.map((item: any) => item.name); + const selectedIds = selectedValues.map((item: any) => item.id); + setClusterName(selectedNames); + setClusterID(selectedIds); + setClusterError(false); + }; + + const headersKeyValidate = (key: any) => { + const regex = /^.{1,50}$/; + return regex.test(key); + }; + + const headersValueValidate = (value: any) => { + const regex = /^.{1,1000}$/; + return regex.test(value); + }; + + const changeValidate = (value: string, data: any) => { + const { setError, validate } = data; + setError(!validate(value)); + }; + + const handleSubmit = async (event: any) => { + setLoadingButton(true); + + event.preventDefault(); + const bio = event.currentTarget.elements.bio; + const url = event.currentTarget.elements.url; + const tag = event.currentTarget.elements.tag; + const filters = filter.join('&'); + + const data = new FormData(event.currentTarget); + + informationForm.forEach((item) => { + const value = data.get(item.formProps.name); + item.setError(!item.validate(value as string)); + item.syncError = !item.validate(value as string); + }); + + argsForm.forEach((item) => { + const value = data.get(item.formProps.name); + item.setError(!item.validate(value as string)); + item.syncError = !item.validate(value as string); + }); + + const headerValidate = headers.every((item) => { + const isValidKey = headersKeyValidate(item.key); + const isValidValue = headersValueValidate(item.value); + + return isValidKey && isValidValue; + }); + + const headerList: { [key: string]: string } = headers.reduce( + (accumulator, currentValue) => ({ ...accumulator, [currentValue.key]: currentValue.value }), + {}, + ); + + let clusterIDValidate = true; + + if (clusterName.length === 0) { + setClusterError(true); + clusterIDValidate = false; + } else { + clusterIDValidate = true; + } + + const canSubmit = Boolean( + !informationForm.filter((item) => item.syncError).length && + !argsForm.filter((item) => item.syncError).length && + clusterIDValidate && + headerValidate, + ); + + const formDate = { + bio: bio.value, + type: 'preheat', + args: { + type: 'file', + url: url.value, + tag: tag.value, + filter: filters, + headers: headerList, + }, + cdn_cluster_ids: clusterID, + user_id: user.id, + }; + + if (canSubmit) { + try { + await createJob({ ...formDate }); + setLoadingButton(false); + navigate('/jobs/preheats'); + } catch (error) { + if (error instanceof Error) { + setLoadingButton(false); + setErrorMessage(true); + setErrorMessageText(error.message); + } + } + } else { + setLoadingButton(false); + } + }; + + const handleClose = (_event: any, reason?: string) => { + if (reason === 'clickaway') { + return; + } + + setErrorMessage(false); + setSuccessMessage(false); + }; + + return ( + + + + Submission successful! + + + + + {errorMessageText} + + + + Create Preheat + + + + + + + + Information + + + + + + {informationForm.map((item) => ( + + ))} + + + + + Clusters + + + + + + + Clusters + + {clusterError && Select at least one option.} + + + + + + Args + + + + + + {argsForm.map((item) => { + return ( + + {item.label === 'Filter' ? ( + ( + + {params.InputProps.endAdornment} + + + + + ), + }} + color="success" + {...item.formProps} + /> + )} + /> + ) : ( + + )} + + ); + })} + {headers.length > 0 ? ( + + + + Headers + + + + + + {headers.map((item, index) => ( + + { + const newheaders = [...headers]; + newheaders[index].key = event.target.value; + setheaders(newheaders); + }} + /> + { + const newheaders = [...headers]; + newheaders[index].value = event.target.value; + setheaders(newheaders); + }} + /> + { + const newheaders = [...headers]; + newheaders.splice(index, 1); + setheaders(newheaders); + }} + > + + + + ))} + + + ) : ( + + )} + + + + } + size="small" + variant="outlined" + loadingPosition="end" + sx={{ + '&.MuiLoadingButton-root': { + color: 'var(--calcel-size-color)', + borderRadius: 0, + borderColor: 'var(--calcel-color)', + }, + ':hover': { + backgroundColor: 'var( --calcel-hover-corlor)', + borderColor: 'var( --calcel-hover-corlor)', + }, + '&.MuiLoadingButton-loading': { + backgroundColor: 'var(--button-loading-color)', + color: 'var(--button-loading-size-color)', + borderColor: 'var(--button-loading-color)', + }, + mr: '1rem', + width: '8rem', + }} + onClick={() => { + navigate('/jobs/preheats'); + }} + > + Cancel + + } + size="small" + variant="outlined" + type="submit" + loadingPosition="end" + sx={{ + '&.MuiLoadingButton-root': { + backgroundColor: 'var(--save-color)', + borderRadius: 0, + color: 'var(--save-size-color)', + borderColor: 'var(--save-color)', + }, + ':hover': { + backgroundColor: 'var(--save-hover-corlor)', + borderColor: 'var(--save-hover-corlor)', + }, + '&.MuiLoadingButton-loading': { + backgroundColor: 'var(--button-loading-color)', + color: 'var(--button-loading-size-color)', + borderColor: 'var(--button-loading-color)', + }, + width: '8rem', + }} + > + Save + + + + + + ); +} diff --git a/src/components/job/preheats/show.module.css b/src/components/job/preheats/show.module.css new file mode 100644 index 00000000..c8aa437b --- /dev/null +++ b/src/components/job/preheats/show.module.css @@ -0,0 +1,50 @@ +.statusIcon { + width: 1.3rem !important; + height: 1.3rem !important; + margin-right: 0.4rem; +} + +.informationContainer { + display: flex; + align-items: flex-start; + padding: 1rem 0rem; +} + +.informationTitle { + width: 30%; + display: flex; + align-items: center; +} + +.informationTitleIcon { + width: 1.4rem !important; + height: 1.4rem !important; +} + +.informationTitleText { + margin-left: 1rem !important; + color: #515151; +} + +.informationContent { + width: 70%; + overflow-wrap: break-word; + font-family: 'mabry-bold' !important; +} + +.statusContent { + display: inline-flex; + align-items: center; +} + +.headersContent { + width: 70%; + overflow-wrap: break-word; + padding-bottom: 1rem; + display: inline; +} + +.headersText { + display: flex; + padding: 1rem 1rem 0 1rem; +} diff --git a/src/components/job/preheats/show.tsx b/src/components/job/preheats/show.tsx new file mode 100644 index 00000000..98078698 --- /dev/null +++ b/src/components/job/preheats/show.tsx @@ -0,0 +1,526 @@ +import { + Box, + Chip, + Divider, + Paper, + Typography, + Breadcrumbs, + Link as RouterLink, + Tooltip, + Snackbar, + Alert, + Skeleton, + IconButton, + Drawer, + AccordionDetails, + AccordionSummary, + Accordion, + ThemeProvider, + createTheme, +} from '@mui/material'; +import { useEffect, useState } from 'react'; +import { getJob } from '../../../lib/api'; +import { useParams, Link } from 'react-router-dom'; +import MoreTimeIcon from '@mui/icons-material/MoreTime'; +import { getDatetime } from '../../../lib/utils'; +import styles from './show.module.css'; +import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp'; + +export default function ShowPreheat() { + const [errorMessage, setErrorMessage] = useState(false); + const [errorMessageText, setErrorMessageText] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [shouldPoll, setShouldPoll] = useState(false); + const [errorLog, setErrorLog] = useState(false); + const [preheat, setPreheat] = useState({ + id: 0, + created_at: '', + updated_at: '', + is_del: 0, + task_id: '', + bio: '', + type: '', + state: '', + args: { + filter: '', + headers: '', + tag: '', + type: '', + url: '', + }, + result: { + CreatedAt: '', + GroupUUID: '', + JobStates: [ + { + CreatedAt: '', + Error: '', + Results: [''], + State: '', + TTL: 0, + TaskName: '', + TaskUUID: '', + }, + ], + State: '', + }, + scheduler_clusters: [{ id: 0 }], + }); + + const params = useParams(); + + const theme = createTheme({ + palette: { + primary: { + main: '#1C293A', + }, + }, + typography: { + fontFamily: 'mabry-light,sans-serif', + }, + }); + + useEffect(() => { + setIsLoading(true); + (async function () { + try { + if (typeof params.id === 'string') { + const job = await getJob(params.id); + setPreheat(job); + setIsLoading(false); + + if (job.result.State !== 'SUCCESS' && job.result.State !== 'FAILURE') { + setShouldPoll(true); + } + } + } catch (error) { + if (error instanceof Error) { + setErrorMessage(true); + setErrorMessageText(error.message); + setIsLoading(false); + } + } + })(); + }, [params.id]); + + useEffect(() => { + if (shouldPoll) { + const pollingInterval = setInterval(() => { + const pollPreheat = async () => { + try { + if (typeof params.id === 'string') { + const job = await getJob(params.id); + setPreheat(job); + + if (job.result.State && job.result.State === 'SUCCESS') { + setShouldPoll(false); + } + + if (job.result.State && job.result.State === 'FAILURE') { + setShouldPoll(false); + } + } + } catch (error) { + if (error instanceof Error) { + setErrorMessage(true); + setErrorMessageText(error.message); + setIsLoading(false); + } + } + }; + + pollPreheat(); + }, 3000); + + return () => { + clearInterval(pollingInterval); + }; + } + }, [shouldPoll, params.id]); + + const handleClose = (_event: any, reason?: string) => { + if (reason === 'clickaway') { + return; + } + + setErrorMessage(false); + setErrorLog(false); + }; + + return ( + + + + {errorMessageText} + + + + jobs + + preheats + + + {preheat.id} + + + + Preheat + + + + + + + + ID + + + + {isLoading ? : preheat.id || ''} + + + + + + + Description + + + + {isLoading ? : preheat.bio || '-'} + + + + + + + Status + + + + {isLoading ? ( + + ) : ( + + + {preheat.result.State === 'SUCCESS' ? ( + <> + ) : preheat.result.State === 'FAILURE' ? ( + <> + ) : ( + + )} + + {preheat.result.State || ''} + + {preheat.result.State === 'FAILURE' ? ( + <> + + + { + setErrorLog(true); + }} + > + + + + + ) : ( + <> + )} + + + )} + + + + + + + URL + + + + {isLoading ? : preheat.args.url || '-'} + + + + + + + Filter + + + + {isLoading ? ( + + ) : ( + preheat.args.filter.split('&').map((item) => + item ? ( + + ) : ( + + - + + ), + ) + )} + + + + + + + Tag + + + + {isLoading ? ( + + ) : preheat.args.tag ? ( + + ) : ( + + - + + )} + + + + + + + Headers + + + {isLoading ? ( + + ) : Object.keys(preheat.args.headers).length > 0 ? ( + + {Object.entries(preheat.args.headers).map(([key, value], index) => ( + + + {key} + + {value} + + ))} + + ) : ( + + - + + )} + + + + + + Scheduler clusters ID + + + + {isLoading ? : preheat.scheduler_clusters[0].id || '-'} + + + + + + + Created At + + + {isLoading ? ( + + ) : ( + ( + } + label={getDatetime(preheat.created_at)} + variant="outlined" + size="small" + /> + ) || '-' + )} + + + + + {preheat.result.JobStates.map((item) => ( + + + Error log + + + + + } + sx={{ + backgroundColor: '#32383f', + flexDirection: 'row-reverse', + '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { + transform: 'rotate(90deg)', + }, + '& .MuiAccordionSummary-content': { + marginLeft: '1rem', + }, + height: '2rem', + }} + aria-controls="panel1d-content" + id="panel1d-header" + > + + + + Preheat + + + + + {item.Error} + + + + + ))} + + + + + ); +} diff --git a/src/components/menu/index.tsx b/src/components/menu/index.tsx index ea20bd8f..5f0db4da 100644 --- a/src/components/menu/index.tsx +++ b/src/components/menu/index.tsx @@ -59,6 +59,7 @@ export default function Layout(props: any) { const [anchorElement, setAnchorElement] = useState(null); const [firstLogin, setFirstLogin] = useState(false); const [expandDeveloper, setExpandDeveloper] = useState(false); + const [expandJob, setExpandJob] = useState(false); const openProfile = Boolean(anchorElement); const location = useLocation(); @@ -110,6 +111,17 @@ export default function Layout(props: any) { text: 'Tokens', }, }, + { + label: 'Job', + href: '/jobs', + text: 'Job', + icon: , + menuProps: { + label: 'preheats', + href: '/jobs/preheats', + text: 'Preheat', + }, + }, { label: 'users', href: '/users', @@ -136,6 +148,17 @@ export default function Layout(props: any) { text: 'Tokens', }, }, + { + label: 'Job', + href: '/jobs', + text: 'Job', + icon: , + menuProps: { + label: 'preheats', + href: '/jobs/preheats', + text: 'Preheat', + }, + }, ]; const handleLogout = async () => { @@ -262,6 +285,56 @@ export default function Layout(props: any) { + ) : items.text === 'Job' ? ( + + { + setExpandJob(!expandJob); + }} + sx={{ + '&.Mui-selected': { backgroundColor: '#DFFF55' }, + '&.Mui-selected:hover': { + backgroundColor: '#DDFF55', + color: '#121726', + }, + height: '2rem', + mb: '0.4rem', + }} + > + {items.icon} + + {items.text} + + {expandJob ? : } + + + + + + {items?.menuProps?.text} + + + + + ) : ( + ) : items.text === 'Job' ? ( + + { + setExpandJob(!expandJob); + }} + sx={{ + '&.Mui-selected': { backgroundColor: '#DFFF55' }, + '&.Mui-selected:hover': { + backgroundColor: '#DDFF55', + color: '#121726', + }, + height: '2rem', + mb: '0.4rem', + }} + > + {items.icon} + + {items.text} + + {expandJob ? : } + + + + + + {items?.menuProps?.text} + + + + + ) : (
- + diff --git a/src/index.css b/src/index.css index 22c4ffa7..bf08058b 100644 --- a/src/index.css +++ b/src/index.css @@ -20,6 +20,8 @@ --button-loading-color: #dedede; --button-loading-size-color: #000000; --scopes-icon-color: #edeff2; + --table-title-color: #f6f6f6; + --title-light-color: #7a7a7a; } body { diff --git a/src/layouts/main.tsx b/src/layouts/main.tsx index 9b78726f..d9470fe4 100644 --- a/src/layouts/main.tsx +++ b/src/layouts/main.tsx @@ -14,6 +14,9 @@ import Users from '../components/users'; import Tokens from '../components/developer/tokens'; import NewTokens from '../components/developer/tokens/new'; import EditTokens from '../components/developer/tokens/edit'; +import Preheats from '../components/job/preheats'; +import NewPreheat from '../components/job/preheats/new'; +import ShowPreheat from '../components/job/preheats/show'; import { useState, useEffect } from 'react'; import { getJwtPayload } from '../lib/utils'; import { getUserRoles } from '../lib/api'; @@ -51,6 +54,9 @@ function Main() { } /> } /> } /> + } /> + } /> + } /> : } /> diff --git a/src/lib/api.ts b/src/lib/api.ts index cb4e8ec0..338b9488 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -170,7 +170,7 @@ interface getClustersParams { interface clustersResponse { id: number; name: string; - bio:string; + bio: string; scopes: { idc: string; location: string; @@ -334,7 +334,7 @@ interface updateClusterResponse { export async function updateCluster(id: string, request: updateClusterRequset): Promise { const url = new URL(`/api/v1/clusters/${id}`, API_URL); const response = await patch(url, request); - return response.json(); + return await response.json(); } export async function deleteCluster(id: string) { @@ -560,7 +560,7 @@ interface updateUserResponse { export async function updateUser(id: string, request: updateUserRequset): Promise { const url = new URL(`/api/v1/users/${id}`, API_URL); const response = await patch(url, request); - return response.json(); + return await response.json(); } interface updatePasswordRequset { @@ -698,5 +698,123 @@ interface updateTokensResponse { export async function updateTokens(id: string, request: updateTokensRequset): Promise { const url = new URL(`/api/v1/personal-access-tokens/${id}`, API_URL); const response = await patch(url, request); - return response.json(); + return await response.json(); +} + +interface getJobsParams { + page?: number; + per_page?: number; + user_id?: string; + state?: string; +} + +interface getJobsResponse { + id: number; + created_at: string; + updated_at: string; + is_del: number; + task_id: string; + bio: string; + type: string; + state: string; + args: { + filter: string; + headers: { [key: string]: string }; + tag: string; + type: string; + url: string; + }; +} + +export async function getJobs(params?: getJobsParams): Promise { + const url = params + ? new URL(`/api/v1/jobs?${queryString.stringify(params)}`, API_URL) + : new URL('/api/v1/jobs', API_URL); + + const response = await get(url); + return await response.json(); +} + +interface getJobResponse { + id: number; + created_at: string; + updated_at: string; + is_del: number; + task_id: string; + bio: string; + type: string; + state: string; + args: { + filter: string; + headers: string; + tag: string; + type: string; + url: string; + }; + result: { + CreatedAt: string; + GroupUUID: string; + JobStates: [ + { + CreatedAt: string; + Error: string; + Results: Array; + State: string; + TTL: number; + TaskName: string; + TaskUUID: string; + }, + ]; + State: string; + }; + user_id: string; + scheduler_clusters: [ + { + id: number; + }, + ]; +} + +export async function getJob(id: string): Promise { + const url = new URL(`/api/v1/jobs/${id}`, API_URL); + const response = await get(url); + return await response.json(); +} + +interface createJobRequest { + bio: string; + type: string; + args: { + type: string; + url: string; + tag: string; + filter: string; + headers?: { [key: string]: string }; + }; + cdn_cluster_ids: Array; +} + +interface cerateJobResponse { + id: number; + created_at: string; + updated_at: string; + is_del: number; + task_id: string; + bio: string; + type: string; + state: string; + args: { + filter: string; + headers: { [key: string]: string }; + tag: string; + type: string; + url: string; + }; + result: string; +} + +export async function createJob(request: createJobRequest): Promise { + const url = new URL(`/api/v1/jobs`, API_URL); + const response = await post(url, request); + return await response.json(); } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index b0df0259..15636c0b 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -40,3 +40,8 @@ export const formatDate = (time: string) => { const hour = date.getHours(); return `${day}, ${month} ${date.getDate()} ${year}`; }; + +export const getPaginatedList = (list: string | any[], currentPage: number, pageSize: number) => { + const startIndex = (currentPage - 1) * pageSize; + return list.slice(startIndex, startIndex + pageSize); +};