Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/.form-horizontal .control-label{width:90px}.form-horizontal .controls{margin-left:98px}.inline-block{display:inline-block}.datepicker{z-index:1300}.coordinator .card-home{padding-bottom:10px}.dataset-input{width:380px}
*/.form-horizontal .control-label{width:90px}.form-horizontal .controls{margin-left:98px}.inline-block{display:inline-block}.datepicker{z-index:1300}.coordinator .card-home{padding-bottom:10px}.dataset-input{width:380px}.cron-expression-explainer{margin-top:10px;margin-bottom:0}.cron-expression-explainer__status{margin-bottom:6px}.cron-expression-explainer__status i{margin-right:6px}.cron-expression-explainer__status--valid{color:#1f7a1f}.cron-expression-explainer__status--invalid{color:#c0392b}.cron-expression-explainer__next-runs{margin-top:4px}.cron-expression-explainer__next-runs-list{margin:4px 0 0 18px;padding:0}
31 changes: 31 additions & 0 deletions apps/oozie/src/oozie/static/oozie/js/coordinator-editor.ko.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,37 @@ var CoordinatorEditorViewModel = (function () {
self.coordinator.refreshParameters();
}

self.cronExpressionExplanation = ko.observable({
isValid: false,
description: '',
error: '',
nextRuns: []
});

var cronExplanationTimeout = null;
self.updateCronExpressionExplanation = function () {
if (cronExplanationTimeout) {
window.clearTimeout(cronExplanationTimeout);
}

cronExplanationTimeout = window.setTimeout(function () {
if (typeof window.cronExpressionUtils === 'undefined') {
return;
}

self.cronExpressionExplanation(
window.cronExpressionUtils.explain(
self.coordinator.properties.cron_frequency(),
self.coordinator.properties.timezone()
)
);
}, 300);
};

self.coordinator.properties.cron_frequency.subscribe(self.updateCronExpressionExplanation);
self.coordinator.properties.timezone.subscribe(self.updateCronExpressionExplanation);
self.updateCronExpressionExplanation();

self.workflowModalFilter = ko.observable("");
self.filteredModalWorkflows = ko.computed(function () {
var _filter = self.workflowModalFilter().toLowerCase();
Expand Down
30 changes: 30 additions & 0 deletions apps/oozie/src/oozie/static/oozie/less/coordinator-editor.less
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,34 @@

.dataset-input {
width: 380px;
}

.cron-expression-explainer {
margin-top: 10px;
margin-bottom: 0;
}

.cron-expression-explainer__status {
margin-bottom: 6px;
}

.cron-expression-explainer__status i {
margin-right: 6px;
}

.cron-expression-explainer__status--valid {
color: #1f7a1f;
}

.cron-expression-explainer__status--invalid {
color: #c0392b;
}

.cron-expression-explainer__next-runs {
margin-top: 4px;
}

.cron-expression-explainer__next-runs-list {
margin: 4px 0 0 18px;
padding: 0;
}
33 changes: 32 additions & 1 deletion apps/oozie/src/oozie/templates/editor2/common_scheduler.inc.mako
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ else:
<div class="controls">
<input id="coord-frequency" type="text" data-bind="textInput: coordinator.properties.cron_frequency, enable: $root.isEditing, attachViewModelToElementData, tagsNotAllowed" name="cron_frequency"/>
<span class="help-inline">
<a data-bind="visible: coordinator.properties.cron_advanced" href="http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/tutorial-lesson-06.html" target="_blank">
<a data-bind="visible: coordinator.properties.cron_advanced" href="https://en.wikipedia.org/wiki/Cron" target="_blank">
<i class="fa fa-question-circle" title="${ _('Check syntax ?') }"></i></a>
</span>
</div>
Expand All @@ -102,6 +102,37 @@ else:
</a>
</div>
</div>
<div class="control-group cron-expression-explainer" data-bind="visible: coordinator.properties.cron_frequency().length > 0">
<label class="control-label"></label>
<div class="controls">
<!-- ko if: $root.cronExpressionExplanation().isValid -->
<div class="cron-expression-explainer__status cron-expression-explainer__status--valid">
<i class="fa fa-check-circle"></i>
<span data-bind="text: $root.cronExpressionExplanation().description"></span>
</div>
<!-- /ko -->
<!-- ko if: ! $root.cronExpressionExplanation().isValid && $root.cronExpressionExplanation().error -->
<div class="cron-expression-explainer__status cron-expression-explainer__status--invalid">
<i class="fa fa-exclamation-circle"></i>
<span data-bind="text: $root.cronExpressionExplanation().error"></span>
</div>
<!-- /ko -->
<!-- ko if: $root.cronExpressionExplanation().isValid && $root.cronExpressionExplanation().nextRuns.length > 0 -->
<div class="cron-expression-explainer__next-runs">
<span class="muted">
${ _('Next run') }
<!-- ko if: coordinator.properties.timezone() -->
(<span data-bind="text: coordinator.properties.timezone"></span>):
<!-- /ko -->
<!-- ko ifnot: coordinator.properties.timezone() -->
:
<!-- /ko -->
<span data-bind="text: $root.cronExpressionExplanation().nextRuns[0]"></span>
</span>
</div>
<!-- /ko -->
</div>
</div>
</form>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<div class="controls">
<div class="row-fluid">
<div class="span9">
<a data-bind="visible: isAdvancedCron" href="http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/tutorial-lesson-06.html" class="pull-right" target="_blank">&nbsp;<i class="fa fa-question-circle" title="${ _('Check syntax ?') }"></i></a>
<a data-bind="visible: isAdvancedCron" href="https://en.wikipedia.org/wiki/Cron" class="pull-right" target="_blank">&nbsp;<i class="fa fa-question-circle" title="${ _('Check syntax ?') }"></i></a>
<input data-bind="visible: isAdvancedCron" id="coord-frequency" name="cron_frequency" class="pull-right"/>
<span data-bind="visible: isAdvancedCron" class="pull-right" style="padding-right:20px">
${ _('Crontab') }
Expand Down
2 changes: 2 additions & 0 deletions desktop/core/src/desktop/js/hue.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import hueDrop from 'utils/hueDrop';
import HueGeo from 'utils/hueGeo';
import huePubSub from 'utils/huePubSub';
import hueUtils from 'utils/hueUtils';
import cronExpressionUtils from 'utils/cron/cronExpressionUtils';
import I18n from 'utils/i18n';
import MultiLineEllipsisHandler from 'utils/multiLineEllipsisHandler';
import sqlUtils from 'sql/sqlUtils';
Expand Down Expand Up @@ -103,6 +104,7 @@ window.HueFileEntry = HueFileEntry;
window.HueGeo = HueGeo;
window.huePubSub = huePubSub;
window.hueUtils = hueUtils;
window.cronExpressionUtils = cronExpressionUtils;
window.I18n = I18n;
window.localforage = localforage;
window.MultiLineEllipsisHandler = MultiLineEllipsisHandler;
Expand Down
60 changes: 60 additions & 0 deletions desktop/core/src/desktop/js/utils/cron/cronExpressionUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Licensed to Cloudera, Inc. under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. Cloudera, Inc. licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { explainCronExpression } from './cronExpressionUtils';

describe('cronExpressionUtils', () => {
it('returns an empty result for blank expressions', () => {
const result = explainCronExpression(' ');

expect(result.isValid).toBe(false);
expect(result.description).toBe('');
expect(result.error).toBe('');
expect(result.nextRuns).toEqual([]);
});

it('describes a valid daily cron expression', () => {
const result = explainCronExpression('0 0 * * *');

expect(result.isValid).toBe(true);
expect(result.description.length).toBeGreaterThan(0);
expect(result.nextRuns).toHaveLength(1);
});

it('describes weekday schedules and returns next runs', () => {
const result = explainCronExpression('0 0/2 * * 1-5', 'UTC', { nextRunsCount: 3 });

expect(result.isValid).toBe(true);
expect(result.description.length).toBeGreaterThan(0);
expect(result.nextRuns).toHaveLength(3);
});

it('rejects expressions that do not use the Oozie 5-field format', () => {
const result = explainCronExpression('0 0 0 * * *');

expect(result.isValid).toBe(false);
expect(result.error).toContain('Expected 5 cron fields');
expect(result.nextRuns).toEqual([]);
});

it('returns parser errors for invalid cron syntax', () => {
const result = explainCronExpression('not a cron');

expect(result.isValid).toBe(false);
expect(result.error.length).toBeGreaterThan(0);
expect(result.nextRuns).toEqual([]);
});
});
105 changes: 105 additions & 0 deletions desktop/core/src/desktop/js/utils/cron/cronExpressionUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Licensed to Cloudera, Inc. under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. Cloudera, Inc. licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import cronstrue from 'cronstrue';
import CronExpressionParser from 'cron-parser';
import { DateTime } from 'luxon';

export const OOZIE_CRON_FIELD_COUNT = 5;
export const DEFAULT_NEXT_RUNS_COUNT = 1;

export interface CronExplanation {
isValid: boolean;
description: string;
error: string;
nextRuns: string[];
}

export interface ExplainCronExpressionOptions {
nextRunsCount?: number;
}

const formatNextRun = (date: Date, timezone?: string): string => {
const formatted = timezone
? DateTime.fromJSDate(date, { zone: timezone })
: DateTime.fromJSDate(date);

return formatted.toFormat('ccc, LLL d, yyyy h:mm a');
};

export const explainCronExpression = (
expression: string,
timezone?: string,
options?: ExplainCronExpressionOptions
): CronExplanation => {
const trimmed = (expression || '').trim();
const emptyResult: CronExplanation = {
isValid: false,
description: '',
error: '',
nextRuns: []
};

if (!trimmed) {
return emptyResult;
}

const fields = trimmed.split(/\s+/);
if (fields.length !== OOZIE_CRON_FIELD_COUNT) {
return {
...emptyResult,
error: `Expected ${OOZIE_CRON_FIELD_COUNT} cron fields (minute hour day month weekday), found ${fields.length}`
};
}

try {
const description = cronstrue.toString(trimmed, { use24HourTimeFormat: false });
const parseOptions: { tz?: string } = {};

if (timezone) {
parseOptions.tz = timezone;
}

const interval = CronExpressionParser.parse(trimmed, parseOptions);
const nextRuns: string[] = [];
const nextRunsCount = options?.nextRunsCount ?? DEFAULT_NEXT_RUNS_COUNT;

for (let i = 0; i < nextRunsCount; i++) {
nextRuns.push(formatNextRun(interval.next().toDate(), timezone));
}

return {
isValid: true,
description,
error: '',
nextRuns
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Invalid cron expression';

return {
...emptyResult,
error: errorMessage
};
}
};

export default {
DEFAULT_NEXT_RUNS_COUNT,
OOZIE_CRON_FIELD_COUNT,
explain: explainCronExpression,
explainCronExpression
};

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,36 @@
background-color: @cui-gray-050;
}

#schedulerEditor .cron-expression-explainer {
margin-top: 10px;
margin-bottom: 0;
}

#schedulerEditor .cron-expression-explainer__status {
margin-bottom: 6px;
}

#schedulerEditor .cron-expression-explainer__status i {
margin-right: 6px;
}

#schedulerEditor .cron-expression-explainer__status--valid {
color: #1f7a1f;
}

#schedulerEditor .cron-expression-explainer__status--invalid {
color: #c0392b;
}

#schedulerEditor .cron-expression-explainer__next-runs {
margin-top: 4px;
}

#schedulerEditor .cron-expression-explainer__next-runs-list {
margin: 4px 0 0 18px;
padding: 0;
}

.warning {
color: @state-warning-text;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,36 @@
background-color: @cui-gray-050;
}

#schedulerEditor .cron-expression-explainer {
margin-top: 10px;
margin-bottom: 0;
}

#schedulerEditor .cron-expression-explainer__status {
margin-bottom: 6px;
}

#schedulerEditor .cron-expression-explainer__status i {
margin-right: 6px;
}

#schedulerEditor .cron-expression-explainer__status--valid {
color: #1f7a1f;
}

#schedulerEditor .cron-expression-explainer__status--invalid {
color: #c0392b;
}

#schedulerEditor .cron-expression-explainer__next-runs {
margin-top: 4px;
}

#schedulerEditor .cron-expression-explainer__next-runs-list {
margin: 4px 0 0 18px;
padding: 0;
}

.warning {
color: @state-warning-text;
}
Expand Down
Loading