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
+ }
+ variant="contained"
+ onClick={() => {
+ navigate('/jobs/preheats/new');
+ }}
+ >
+ add preheat
+
+
+
+
+
+ Workflow runs
+
+
+ Status
+ {
+ setOpenStatusSelect(false);
+ }}
+ onOpen={() => {
+ setOpenStatusSelect(true);
+ }}
+ onChange={changeStatus}
+ >
+
+ Filter by status
+
+
+ {statusList.map((item) => (
+
+ {item.lable}
+
+ ))}
+
+
+
+
+ {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
+ cluster.find((item) => item.name === name))}
+ onChange={handleSelectCluster}
+ input={ }
+ renderValue={(selected) => selected.map((item: any) => item.name).join(', ')}
+ MenuProps={{
+ PaperProps: {
+ style: {
+ maxHeight: '14rem',
+ width: '18rem',
+ },
+ },
+ }}
+ >
+ {cluster.map((item: any) => (
+
+ -1}
+ />
+
+
+ ))}
+
+ {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);
+ }}
+ >
+
+
+
+ ))}
+ }
+ onClick={() => {
+ setheaders([...headers, { key: '', value: '' }]);
+ }}
+ >
+ add headers
+
+
+ ) : (
+ {
+ setheaders([...headers, { key: '', value: '' }]);
+ }}
+ >
+ add headers
+
+ )}
+
+
+
+ }
+ 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);
+};