diff --git a/src/dashboard/Data/Jobs/EditScheduledJobModal.react.js b/src/dashboard/Data/Jobs/EditScheduledJobModal.react.js
new file mode 100644
index 0000000000..8ae97f2d92
--- /dev/null
+++ b/src/dashboard/Data/Jobs/EditScheduledJobModal.react.js
@@ -0,0 +1,373 @@
+import React, { useState, useEffect } from 'react';
+import B4aModal from 'components/B4aModal/B4aModal.react';
+import B4aToggle from 'components/Toggle/B4aToggle.react';
+import DateTimeInput from 'components/DateTimeInput/DateTimeInput.react';
+import Dropdown from 'components/Dropdown/Dropdown.react';
+import Field from 'components/Field/Field.react';
+import FormNote from 'components/FormNote/FormNote.react';
+import IntervalInput from 'components/IntervalInput/IntervalInput.react';
+import Label from 'components/Label/Label.react';
+import Option from 'components/Dropdown/Option.react';
+import TextInput from 'components/TextInput/TextInput.react';
+import TimeInput from 'components/TimeInput/TimeInput.react';
+import { hoursFrom } from 'lib/DateUtils';
+
+function parseInitialState(job) {
+ if (!job) {
+ return {
+ name: '',
+ cloudCodeFunction: '',
+ parameter: '',
+ startAt: hoursFrom(new Date(), 1),
+ repeat: false,
+ repeatType: 'Every day',
+ intervalCount: 15,
+ intervalUnit: 'minute',
+ repeatHour: '12',
+ repeatMinute: '00',
+ };
+ }
+
+ const startAfter = job.startAfter ? new Date(job.startAfter) : hoursFrom(new Date(), 1);
+ let repeat = false;
+ let repeatType = 'Every day';
+ let intervalCount = 15;
+ let intervalUnit = 'minute';
+ let repeatHour = '12';
+ let repeatMinute = '00';
+
+ if (job.repeatMinutes) {
+ repeat = true;
+ if (job.repeatMinutes === 1440) {
+ repeatType = 'Every day';
+ } else {
+ repeatType = 'On an interval';
+ if (job.repeatMinutes > 60) {
+ intervalCount = (job.repeatMinutes / 60) | 0;
+ intervalUnit = 'hour';
+ } else {
+ intervalCount = job.repeatMinutes;
+ intervalUnit = 'minute';
+ }
+ }
+ }
+
+ if (job.timeOfDay) {
+ const parts = job.timeOfDay.split(':');
+ repeatHour = parts[0] ? parts[0].replace(/^0/, '') || '0' : '12';
+ repeatMinute = parts[1] || '00';
+ }
+
+ return {
+ name: job.description || '',
+ cloudCodeFunction: job.jobName || '',
+ parameter: job.params || '',
+ startAt: startAfter,
+ repeat,
+ repeatType,
+ intervalCount,
+ intervalUnit,
+ repeatHour,
+ repeatMinute,
+ };
+}
+
+const FIELD_PADDING = { padding: '0 1rem', width: '100%' };
+
+const EditScheduledJobModal = ({ job, context, onCancel, onSuccess }) => {
+ const isCreate = !job;
+ const initial = parseInitialState(job);
+
+ const [name, setName] = useState(initial.name);
+ const [cloudCodeFunction, setCloudCodeFunction] = useState(initial.cloudCodeFunction);
+ const [parameter, setParameter] = useState(initial.parameter);
+ const [startAt, setStartAt] = useState(initial.startAt);
+ const [repeat, setRepeat] = useState(initial.repeat);
+ const [repeatType, setRepeatType] = useState(initial.repeatType);
+ const [intervalCount, setIntervalCount] = useState(initial.intervalCount);
+ const [intervalUnit, setIntervalUnit] = useState(initial.intervalUnit);
+ const [repeatHour, setRepeatHour] = useState(initial.repeatHour);
+ const [repeatMinute, setRepeatMinute] = useState(initial.repeatMinute);
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState(null);
+ const [paramError, setParamError] = useState(null);
+ const [availableJobs, setAvailableJobs] = useState([]);
+ const [loadingJobs, setLoadingJobs] = useState(isCreate);
+
+ useEffect(() => {
+ if (!isCreate) {
+ return;
+ }
+ context.getAvailableJobs()
+ .then(result => {
+ const jobs = (result && result.jobs) || [];
+ setAvailableJobs(jobs);
+ if (jobs.length > 0) {
+ setCloudCodeFunction(jobs[0]);
+ }
+ })
+ .catch(() => setAvailableJobs([]))
+ .finally(() => setLoadingJobs(false));
+ }, []);
+
+ function buildPayload() {
+ let parsedParam = null;
+ if (parameter && parameter.trim() !== '') {
+ parsedParam = JSON.parse(parameter);
+ }
+
+ let dailyRun = null;
+ let intervalRun = null;
+
+ if (repeat) {
+ const hour = repeatHour.length < 2 ? '0' + repeatHour : repeatHour;
+ if (repeatType === 'Every day') {
+ dailyRun = `${hour}:${repeatMinute}`;
+ } else {
+ intervalRun = intervalCount * (intervalUnit === 'hour' ? 60 : 1);
+ dailyRun = `${hour}:${repeatMinute}`;
+ }
+ }
+
+ return {
+ job: {
+ name,
+ cloudCodeFunction,
+ parameter: parsedParam,
+ schedule: {
+ start: startAt.toISOString(),
+ dailyRun,
+ intervalRun,
+ },
+ },
+ };
+ }
+
+ function validateParameter(value) {
+ if (!value || value.trim() === '') {
+ return null;
+ }
+ let parsed;
+ try {
+ parsed = JSON.parse(value);
+ } catch (e) {
+ return 'Parameters must be valid JSON.';
+ }
+ if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) {
+ return 'Parameters must be a JSON object, e.g. { "key": "value" }.';
+ }
+ return null;
+ }
+
+ function validate() {
+ if (!name.trim()) {
+ return 'A name is required.';
+ }
+ if (!cloudCodeFunction) {
+ return 'A cloud code function is required.';
+ }
+ const paramErr = validateParameter(parameter);
+ if (paramErr) {
+ return paramErr;
+ }
+ return null;
+ }
+
+ function handleConfirm() {
+ const validationError = validate();
+ if (validationError) {
+ setError(validationError);
+ return;
+ }
+ setSaving(true);
+ setError(null);
+ const payload = buildPayload();
+ const request = isCreate
+ ? context.createScheduledJob(payload)
+ : context.updateScheduledJob(job.objectId, payload);
+ request
+ .then(() => {
+ setSaving(false);
+ onSuccess();
+ })
+ .catch(err => {
+ setSaving(false);
+ setError((err && (err.error || err.message)) || `Failed to ${isCreate ? 'create' : 'update'} job. Please try again.`);
+ });
+ }
+
+ return (
+
+
+
+
+ }
+ input={
+
{ setName(val); setError(null); }}
+ />
+ }
+ />
+
+ }
+ input={
+ isCreate ? (
+ loadingJobs ? (
+
+ ) : (
+ { setCloudCodeFunction(val); setError(null); }}
+ >
+ {availableJobs.map(jobName => (
+
+ ))}
+
+ )
+ ) : (
+
+ )
+ }
+ />
+
+
+ }
+ input={
+
+
{
+ setParameter(val);
+ setParamError(validateParameter(val));
+ setError(null);
+ }}
+ />
+ {paramError && (
+
+ {paramError}
+
+ )}
+
+ }
+ />
+
+ }
+ input={
+
+
+
+ }
+ />
+
+
+ }
+ input={
+
+
+
+ }
+ />
+
+ {repeat && (
+ }
+ input={
+
+
+
+ }
+ />
+ )}
+
+ {repeat && repeatType === 'On an interval' && (
+ }
+ input={
+
+ { setIntervalCount(count); setIntervalUnit(unit); }}
+ />
+
+ }
+ />
+ )}
+
+ {repeat && (
+
+ }
+ input={
+
+ { setRepeatHour(h); setRepeatMinute(m); }}
+ />
+
+ }
+ />
+ )}
+
+
+ {error && (
+ {error}
+ )}
+
+ );
+};
+
+export default EditScheduledJobModal;
diff --git a/src/dashboard/Data/Jobs/Jobs.react.js b/src/dashboard/Data/Jobs/Jobs.react.js
index 3abdd59a0d..7288f6433b 100644
--- a/src/dashboard/Data/Jobs/Jobs.react.js
+++ b/src/dashboard/Data/Jobs/Jobs.react.js
@@ -12,8 +12,9 @@ import CategoryList from 'components/CategoryList/CategoryList.react';
import EmptyGhostState from 'components/EmptyGhostState/EmptyGhostState.react';
import ChromeDropdown from 'components/ChromeDropdown/ChromeDropdown.react';
import Icon from 'components/Icon/Icon.react';
-import JobScheduleReminder from 'dashboard/Data/Jobs/JobScheduleReminder.react';
-import Modal from 'components/Modal/Modal.react';
+import B4aModal from 'components/B4aModal/B4aModal.react';
+import EditScheduledJobModal from 'dashboard/Data/Jobs/EditScheduledJobModal.react';
+import FormNote from 'components/FormNote/FormNote.react';
import Popover from 'components/Popover/Popover.react';
import Position from 'lib/Position';
import React from 'react';
@@ -34,7 +35,8 @@ import { withRouter } from 'lib/withRouter';
const subsections = {
all: 'All Jobs',
- // scheduled: 'Scheduled Jobs',
+ scheduled: 'Scheduled Jobs',
+ 'scheduled-jobs': 'Scheduled Jobs',
status: 'Job Status',
};
@@ -44,7 +46,22 @@ const statusColors = {
running: 'blue',
};
+const MONTH_ABBREVS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+
+function formatScheduleDate(date) {
+ const month = MONTH_ABBREVS[date.getUTCMonth()];
+ const day = date.getUTCDate();
+ const hours = (date.getUTCHours() < 10 ? '0' : '') + date.getUTCHours();
+ const minutes = (date.getUTCMinutes() < 10 ? '0' : '') + date.getUTCMinutes();
+ return `${month} ${day} at ${hours}:${minutes}`;
+}
+
function scheduleString(data) {
+ const runAt = new Date(data.startAfter);
+ if (Number.isNaN(runAt.getTime())) {
+ return
-
;
+ }
+
let schedule = '';
if (data.repeatMinutes) {
if (data.repeatMinutes === 1440) {
@@ -54,22 +71,12 @@ function scheduleString(data) {
} else {
schedule += 'Each day, every ' + data.repeatMinutes + ' minutes, ';
}
- schedule += 'after ' + data.timeOfDay.substr(0, 5) + ', ';
+ if (data.timeOfDay) {
+ schedule += 'after ' + data.timeOfDay.substr(0, 5) + ', ';
+ }
schedule += 'starting ';
- } else {
- schedule = 'On ';
}
- const runAt = new Date(data.startAfter);
- schedule +=
- runAt.getUTCMonth() + '/' + runAt.getUTCDate() + '/' + String(runAt.getUTCFullYear()).substr(2);
- schedule +=
- ' at ' +
- (runAt.getUTCHours() < 10 ? '0' : '') +
- runAt.getUTCHours() +
- ':' +
- (runAt.getUTCMinutes() < 10 ? '0' : '') +
- runAt.getUTCMinutes() +
- '.';
+ schedule += formatScheduleDate(runAt);
return {schedule}
;
}
@@ -84,6 +91,10 @@ class Jobs extends TableView {
this.state = {
toDelete: null,
+ deleteInProgress: false,
+ deleteError: null,
+ toEdit: null,
+ toCreate: false,
jobStatus: undefined,
loading: true,
// Properties used to control data access
@@ -98,11 +109,29 @@ class Jobs extends TableView {
// Job Status pagination (botão "Carregar mais")
jobStatusHasMore: true,
jobStatusLoadingMore: false,
+ // Job limit enforcement
+ jobLimitReached: false,
+ maxJobAmount: undefined,
};
this.filterWrapRef = React.createRef();
this.JOB_STATUS_PAGE_SIZE = 100;
}
+ handleScheduleClick() {
+ const { maxJobAmount } = this.state;
+ if (maxJobAmount === undefined) {
+ // Plan data not yet loaded — proceed and let the backend enforce the limit
+ this.setState({ toCreate: true });
+ return;
+ }
+ const currentCount = (this.tableData() || []).length;
+ if (currentCount >= maxJobAmount) {
+ this.setState({ jobLimitReached: true });
+ } else {
+ this.setState({ toCreate: true });
+ }
+ }
+
componentWillMount() {
this.loadData();
}
@@ -111,19 +140,28 @@ class Jobs extends TableView {
if (nextProps.availableJobs) {
if (nextProps.availableJobs.length > 0) {
this.action = new SidebarAction(Schedule job, this.navigateToNew.bind(this));
- return;
}
+ } else {
+ this.action = null;
}
- // check if the changes are in currentApp serverInfo status
- // if not return without making any request
+
+ const sectionChanged = nextProps.params.section !== this.props.params.section;
+ const appChanged = nextProps.params.appId !== this.props.params.appId;
+ let appStatusChanged = false;
+
if (this.props.apps !== nextProps.apps) {
- const updatedCurrentApp = nextProps.apps.find(ap => ap.slug === this.props.params.appId);
+ const updatedCurrentApp = nextProps.apps.find(ap => ap.slug === nextProps.params.appId);
const prevCurrentApp = this.props.apps.find(ap => ap.slug === this.props.params.appId);
- const shouldUpdate = updatedCurrentApp.serverInfo.status !== prevCurrentApp.serverInfo.status;
- if (!shouldUpdate) {return;}
+ const updatedStatus = updatedCurrentApp && updatedCurrentApp.serverInfo && updatedCurrentApp.serverInfo.status;
+ const prevStatus = prevCurrentApp && prevCurrentApp.serverInfo && prevCurrentApp.serverInfo.status;
+ appStatusChanged = updatedStatus !== prevStatus;
}
- this.action = null;
- this.loadData();
+
+ if (!sectionChanged && !appChanged && !appStatusChanged) {
+ return;
+ }
+
+ this.loadData(nextProps.params.section);
}
navigateToNew() {
@@ -134,8 +172,37 @@ class Jobs extends TableView {
this.props.navigate(generatePath(this.context, `jobs/edit/${jobId}`));
}
- loadData() {
- this.props.jobs.dispatch(ActionTypes.FETCH).finally(() => {
+ getCurrentSection() {
+ return this.props.params.section || 'all';
+ }
+
+ isScheduledSection(section) {
+ const s = section || this.getCurrentSection();
+ return s === 'scheduled' || s === 'scheduled-jobs';
+ }
+
+ loadData(section) {
+ const currentSection = section || this.getCurrentSection();
+ this.setState({
+ loading: true,
+ errorMessage: '',
+ hasPermission: true,
+ jobLimitReached: false,
+ ...(this.isScheduledSection(currentSection) ? { maxJobAmount: undefined } : {}),
+ ...(currentSection === 'status' ? { jobStatus: undefined } : {}),
+ });
+
+ if (this.isScheduledSection(currentSection)) {
+ this.context.getAppPlanData()
+ .then(planData => {
+ this.setState({ maxJobAmount: (planData && planData.maxJobAmount) || null });
+ })
+ .catch(() => {
+ this.setState({ maxJobAmount: null });
+ });
+ }
+
+ this.props.jobs.dispatch(ActionTypes.FETCH, { section: currentSection }).finally(() => {
const err = this.props.jobs.data && this.props.jobs.data.get('err')
// Verify error message, used to control collaborators permissions
if (err && err.code === 403)
@@ -153,13 +220,14 @@ class Jobs extends TableView {
}
// If is a unexpected error just finish loading state
else {this.setState({ loading: false });}
- this.renderEmpty()
- });
- this.context.getJobStatus(0, this.JOB_STATUS_PAGE_SIZE).then(status => {
- this.setState({ jobStatus: status, jobStatusHasMore: status.length === this.JOB_STATUS_PAGE_SIZE });
- }).catch(() => {
- this.setState({ jobStatus: [], jobStatusHasMore: false });
});
+ if (currentSection === 'status') {
+ this.context.getJobStatus(0, this.JOB_STATUS_PAGE_SIZE).then(status => {
+ this.setState({ jobStatus: status, jobStatusHasMore: status.length === this.JOB_STATUS_PAGE_SIZE });
+ }).catch(() => {
+ this.setState({ jobStatus: [], jobStatusHasMore: false });
+ });
+ }
}
loadMoreJobStatus() {
@@ -191,7 +259,7 @@ class Jobs extends TableView {
linkPrefix={'jobs/'}
categories={[
{ name: 'All Jobs', id: 'all' },
- // { name: 'Scheduled Jobs', id: 'scheduled' },
+ { name: 'Scheduled Jobs', id: 'scheduled-jobs' },
{ name: 'Job Status', id: 'status' },
]}
/>
@@ -210,21 +278,23 @@ class Jobs extends TableView {
);
- } else if (this.props.params.section === 'scheduled') {
+ } else if (this.isScheduledSection()) {
+ const openEdit = () => this.setState({ toEdit: data });
+ const openDelete = () => this.setState({ toDelete: data, deleteError: null });
return (
- | {data.description} |
- {data.jobName} |
- {scheduleString(data)} |
-
-
- |
);
@@ -259,18 +329,18 @@ class Jobs extends TableView {
Actions
,
];
- } else if (this.props.params.section === 'scheduled') {
+ } else if (this.isScheduledSection()) {
return [
-
+
Name
,
-
+
Function
,
-
+
Schedule (UTC)
,
-
+
Actions
,
];
@@ -296,9 +366,6 @@ class Jobs extends TableView {
}
renderFooter() {
- if (this.props.params.section === 'scheduled') {
- return ;
- }
if (this.props.params.section === 'status' && this.state.jobStatus && this.state.jobStatus.length > 0) {
const { jobStatusHasMore, jobStatusLoadingMore } = this.state;
return (
@@ -334,17 +401,11 @@ class Jobs extends TableView {
description="Define Jobs on parse-server with Parse.Cloud.job()"
/>
);
- } else if (this.props.params.section === 'scheduled') {
+ } else if (this.isScheduledSection()) {
return (
- {'On this page you can create JobSchedule objects.'}
-
-
-
- }
+ description="There are no scheduled jobs to show at this time."
/>
);
} else {
@@ -358,33 +419,108 @@ class Jobs extends TableView {
}
renderExtras() {
- if (this.state.toDelete) {
+ const { toDelete, deleteInProgress, deleteError, toEdit, toCreate, jobLimitReached } = this.state;
+
+ if (jobLimitReached) {
return (
- this.setState({ toDelete: null })}
+ buttonsInCenter={false}
+ onCancel={() => this.setState({ jobLimitReached: false })}
onConfirm={() => {
- this.setState({ toDelete: null });
- this.props.jobs.dispatch(ActionTypes.DELETE, {
- jobId: this.state.toDelete,
- });
+ this.props.navigate(generatePath(this.context, 'plan-usage'));
+ }}
+ >
+
+ Upgrade your plan to schedule additional background jobs.
+
+
+ );
+ }
+
+ if (toCreate) {
+ return (
+ this.setState({ toCreate: false })}
+ onSuccess={() => {
+ this.setState({ toCreate: false });
+ this.loadData();
}}
/>
);
}
+ if (toEdit) {
+ return (
+ this.setState({ toEdit: null })}
+ onSuccess={() => {
+ this.setState({ toEdit: null });
+ this.loadData();
+ }}
+ />
+ );
+ }
+ if (!toDelete) {
+ return null;
+ }
+ return (
+ this.setState({ toDelete: null, deleteError: null })}
+ onConfirm={() => {
+ this.setState({ deleteInProgress: true, deleteError: null });
+ this.context.deleteScheduledJob(toDelete.objectId)
+ .then(() => {
+ this.setState({ toDelete: null, deleteInProgress: false, deleteError: null });
+ this.loadData();
+ })
+ .catch(err => {
+ this.setState({
+ deleteInProgress: false,
+ deleteError: (err && err.error) || 'Failed to delete job. Please try again.',
+ });
+ });
+ }}
+ >
+ {deleteError ? (
+ {deleteError}
+ ) : null}
+
+ );
}
tableData() {
// Return a empty array if user don't have permission to read scheduled jobs
if (!this.state.hasPermission) {return []}
+ const currentSection = this.getCurrentSection();
+ const jobsState = this.props.jobs.data;
+ if (jobsState) {
+ const fetchedSection = jobsState.get('section');
+ // While loading data for the new section, return undefined so TableView shows
+ // the loader instead of the empty state.
+ if (fetchedSection && fetchedSection !== currentSection) {
+ return undefined;
+ }
+ }
let data = undefined;
- if (this.props.params.section === 'scheduled' || this.props.params.section === 'all') {
- if (this.props.jobs.data) {
- const jobs = this.props.jobs.data.get('jobs');
+ if (this.isScheduledSection() || currentSection === 'all') {
+ if (jobsState) {
+ const jobs = jobsState.get('jobs');
if (jobs) {
if (Array.isArray(jobs)) {
data = jobs;
@@ -534,9 +670,29 @@ class Jobs extends TableView {
- {this.props.availableJobs && this.props.availableJobs.length > 0 ? (
-
- ) : null}
+ {this.isScheduledSection() ? (
+ Schedule a job
+ }
+ color="green"
+ width="auto"
+ additionalStyles={{ marginLeft: '1rem', padding: '0 0.5rem', fontSize: '12px', position: 'relative' }}
+ onClick={this.handleScheduleClick.bind(this)}
+ />
+ ) : (this.props.availableJobs && this.props.availableJobs.length > 0 ? (
+ Schedule a job
+ }
+ color="green"
+ width="auto"
+ additionalStyles={{ marginLeft: '1rem', padding: '0 0.5rem', fontSize: '12px', position: 'relative' }}
+ onClick={this.navigateToNew.bind(this)}
+ />
+ ) : null)}
);
}
diff --git a/src/dashboard/Data/Jobs/Jobs.scss b/src/dashboard/Data/Jobs/Jobs.scss
index 0fab97298b..7e9b65bf32 100644
--- a/src/dashboard/Data/Jobs/Jobs.scss
+++ b/src/dashboard/Data/Jobs/Jobs.scss
@@ -18,3 +18,31 @@
margin-right: 10px;
}
}
+
+.scheduleJobButton {
+ margin-left: 12px;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 0 14px;
+ height: 32px;
+ box-sizing: border-box;
+ background: rgba(39, 174, 96, 0.06);
+ border: 1px solid rgba(39, 174, 96, 0.6);
+ border-radius: 6px;
+ color: #27AE60;
+ font-size: 13px;
+ font-weight: 500;
+ font-family: inherit;
+ cursor: pointer;
+ transition: background 0.15s ease, border-color 0.15s ease;
+
+ &:hover {
+ background: rgba(39, 174, 96, 0.14);
+ border-color: #27AE60;
+ }
+
+ &:active {
+ background: rgba(39, 174, 96, 0.22);
+ }
+}
diff --git a/src/lib/ParseApp.js b/src/lib/ParseApp.js
index b0f81e432f..ced71c7e5b 100644
--- a/src/lib/ParseApp.js
+++ b/src/lib/ParseApp.js
@@ -1057,6 +1057,26 @@ export default class ParseApp {
return this.apiRequest('GET', path, {}, { useMasterKey: true });
}
+ getScheduledJobs() {
+ // eslint-disable-next-line no-undef
+ return axios.get(`${b4aSettings.BACK4APP_API_PATH}/jobs/${this.slug}`, { withCredentials: true }).then(({ data }) => data);
+ }
+
+ deleteScheduledJob(jobId) {
+ // eslint-disable-next-line no-undef
+ return axios.delete(`${b4aSettings.BACK4APP_API_PATH}/jobs/delete/${jobId}/`, { withCredentials: true }).then(({ data }) => data);
+ }
+
+ updateScheduledJob(jobId, payload) {
+ // eslint-disable-next-line no-undef
+ return axios.post(`${b4aSettings.BACK4APP_API_PATH}/jobs/job/${jobId}/`, payload, { withCredentials: true }).then(({ data }) => data);
+ }
+
+ createScheduledJob(payload) {
+ // eslint-disable-next-line no-undef
+ return axios.post(`${b4aSettings.BACK4APP_API_PATH}/jobs/${this.slug}/`, payload, { withCredentials: true }).then(({ data }) => data);
+ }
+
getJobStatus(skip = 0, limit = 100) {
const query = new Parse.Query('_JobStatus');
query.descending('createdAt');
diff --git a/src/lib/stores/JobsStore.js b/src/lib/stores/JobsStore.js
index 1bcb47a092..020b1af0d2 100644
--- a/src/lib/stores/JobsStore.js
+++ b/src/lib/stores/JobsStore.js
@@ -20,28 +20,59 @@ function JobsStore(state, action) {
let path = '';
switch (action.type) {
case ActionTypes.FETCH:
- if (state && new Date() - state.get('lastFetch') < 60000) {
+ const isScheduledJobsSection = action.section === 'scheduled-jobs';
+ if (
+ state &&
+ state.get('section') === action.section &&
+ !isScheduledJobsSection &&
+ new Date() - state.get('lastFetch') < 60000
+ ) {
return Promise.resolve(state);
}
+ if (isScheduledJobsSection) {
+ return action.app.getScheduledJobs().then(
+ results => {
+ const jobs = (results.jobs || []).map(job => ({
+ objectId: job.id || job._id,
+ description: job.name || job.description || job.jobName || job.cloudCodeFunction || '-',
+ jobName: job.cloudCodeFunction || job.jobName || '-',
+ startAfter: (
+ (job.schedule && (job.schedule.start || job.schedule.startAfter || job.schedule.startAt)) ||
+ job.startAfter ||
+ job.startAt
+ ),
+ repeatMinutes: job.schedule && (job.schedule.intervalRun || (job.schedule.dailyRun ? 1440 : null)),
+ timeOfDay: (job.schedule && (job.schedule.dailyRun || job.schedule.timeOfDay)) || null,
+ params: job.parameter ? JSON.stringify(job.parameter) : null,
+ }));
+ return Map({ lastFetch: new Date(), section: action.section, jobs: List(jobs) });
+ },
+ err => Map({ lastFetch: new Date(), section: action.section, jobs: [], err })
+ );
+ }
return Parse._request('GET', 'serverInfo', {}).then(serverInfo => {
- let serverVersionPrefix = serverInfo.parseServerVersion.substring(0,3)
+ const serverVersionPrefix = serverInfo.parseServerVersion.substring(0,3)
if (serverVersionPrefix === '2.2' || serverVersionPrefix === '2.3') {
path = 'cloud_code/jobs?per_page=50'
return Parse._request('GET', path, {}, { useMasterKey: true}).then((results) => {
- return Map({ lastFetch: new Date(), jobs: List(results) });
+ return Map({ lastFetch: new Date(), section: action.section, jobs: List(results) });
})
// In error case return a map with a empty array and the error message
// used to control collaborators permissions
- .catch(err => Map({ lastFetch: new Date(), jobs: [], err }));
+ .catch(err => Map({ lastFetch: new Date(), section: action.section, jobs: [], err }));
}
else {
path = 'cloud_code/jobs/data?per_page=50';
return Parse._request('GET', path, {}, { useMasterKey: true}).then((results) => {
- return Map({ lastFetch: new Date(), jobs: List(results.jobs.map(job => ({ 'jobName': job })))});
+ return Map({
+ lastFetch: new Date(),
+ section: action.section,
+ jobs: List(results.jobs.map(job => ({ 'jobName': job })))
+ });
})
// In error case return a map with a empty array and the error message
// used to control collaborators permissions
- .catch(err => Map({ lastFetch: new Date(), jobs: [], err }));
+ .catch(err => Map({ lastFetch: new Date(), section: action.section, jobs: [], err }));
}
})