Compare commits

..

15 Commits

Author SHA1 Message Date
Alex Corey
26a947ed31 Adds functionality to add multiple rrules to a schedule and save the form 2023-01-09 09:27:47 -05:00
Shane McDonald
b99a434dee Merge pull request #13395 from shanemcd/pin-rsyslog
Pin rsyslog to avoid crash
2023-01-04 21:54:34 +08:00
Shane McDonald
6cee99a9f9 Pin rsyslog to prevent crash
With the latest version of rsyslog we had a test failing with:

AssertionError: Response data: {'error': "b'rsyslog internal message (3,-2455): could not transfer  the  specified  internal posix  capabilities settings to the kernel, capng_apply=-5\\n [v8.2102.0-107.el9 try https://www.rsyslog.com/e/2455 ]\\n'"}

Downgrading fixes it
2023-01-04 08:19:20 -05:00
Seth Foster
ee509aea56 Merge pull request #12961 from fosterseth/fix_results_traceback
Result_traceback should not include job stdout
2023-01-03 13:34:23 -05:00
Sarah Akus
b5452a48f8 Merge pull request #13196 from keithjgrant/13189-job-traceback
Fix job error traceback in job output
2023-01-03 11:59:58 -05:00
Seth Foster
0c980fa7d5 Merge pull request #13366 from fosterseth/bump_receptorctl_1.3.0
bump receptorctl version to 1.3.0
2022-12-21 16:27:25 -05:00
Shane McDonald
e34ce8c795 Merge pull request #13365 from dsavineau/downgrade_hiredis
Pin hiredis to 2.0.0
2022-12-21 15:23:15 -05:00
Seth Foster
3543644e0e bump receptorctl version to 1.3.0 2022-12-21 13:36:11 -05:00
Seth Foster
36c0d07b30 Result_traceback should not include job stdout
If a job fails, we do receptor work results and put that output
into result_traceback.

We should only do this if
1. Receptor unit has failed
2. Runner callback processed 0 events

Otherwise we risk putting too much data into this field.
2022-12-21 13:05:44 -05:00
Dimitri Savineau
239827a9cf Pin hiredis to 2.0.0
The hiredis 2.1.0 release doesn't provide source distribution on PyPi so
users can't build that python package from sources.

Signed-off-by: Dimitri Savineau <dsavinea@redhat.com>
2022-12-21 11:57:41 -05:00
Alan Rominger
ac9871b36f Merge pull request #13361 from relrod/sanity
[collection] Run sanity tests outside of our container
2022-12-21 11:00:21 -05:00
Alan Rominger
f739908ccf Add comment about Ansible-core being installed by default
Co-authored-by: John R Barker <john@johnrbarker.com>
2022-12-21 09:57:00 -05:00
Alan Rominger
cf1ec07eab Changes to run sanity tests locally
Use a Makefile arg for the ansible-test sanity CLI args
  defaults to --docker
  in the future we probably need to customize python versions

Copy the rule exception for Ansible 2.15
  this helps people who are running from Ansible devel
2022-12-21 09:53:22 -05:00
Rick Elrod
d968b648de Run sanity tests outside of our container
Also just ignore one sanity test for the export module, instead of
ignoring all of them.

Also use latest ansible-test, and make it work on GHA (by using podman
instead of docker).

Signed-off-by: Rick Elrod <rick@elrod.me>
2022-12-20 21:40:41 -06:00
Keith J. Grant
d34f6af830 fix traceback offset/counter # in UI 2022-11-15 13:35:14 -08:00
29 changed files with 1111 additions and 1296 deletions

View File

@@ -28,9 +28,6 @@ jobs:
- name: awx-collection
command: /start_tests.sh test_collection_all
label: Run Collection Tests
- name: awx-collection-sanity
command: /start_tests.sh test_collection_sanity
label: Run Ansible core Collection Sanity tests
- name: api-schema
label: Check API Schema
command: /start_tests.sh detect-schema-change SCHEMA_DIFF_BASE_BRANCH=${{ github.event.pull_request.base.ref }}
@@ -148,3 +145,22 @@ jobs:
env:
AWX_TEST_IMAGE: awx
AWX_TEST_VERSION: ci
collection-sanity:
name: awx_collection sanity
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v2
# The containers that GitHub Actions use have Ansible installed, so upgrade to make sure we have the latest version.
- name: Upgrade ansible-core
run: python3 -m pip install --upgrade ansible-core
- name: Run sanity tests
run: make test_collection_sanity
env:
# needed due to cgroupsv2. This is fixed, but a stable release
# with the fix has not been made yet.
ANSIBLE_TEST_PREFER_PODMAN: 1

View File

@@ -6,7 +6,7 @@ env:
on:
pull_request_target:
types: [labeled]
jobs:
jobs:
e2e-test:
if: contains(github.event.pull_request.labels.*.name, 'qe:e2e')
runs-on: ubuntu-latest
@@ -107,5 +107,3 @@ jobs:
with:
name: AWX-logs-${{ matrix.job }}
path: make-docker-compose-output.log

View File

@@ -9,6 +9,17 @@ VERSION := $(shell $(PYTHON) tools/scripts/scm_version.py)
# ansible-test requires semver compatable version, so we allow overrides to hack it
COLLECTION_VERSION ?= $(shell $(PYTHON) tools/scripts/scm_version.py | cut -d . -f 1-3)
# args for the ansible-test sanity command
COLLECTION_SANITY_ARGS ?= --docker
# collection unit testing directories
COLLECTION_TEST_DIRS ?= awx_collection/test/awx
# collection integration test directories (defaults to all)
COLLECTION_TEST_TARGET ?=
# args for collection install
COLLECTION_PACKAGE ?= awx
COLLECTION_NAMESPACE ?= awx
COLLECTION_INSTALL = ~/.ansible/collections/ansible_collections/$(COLLECTION_NAMESPACE)/$(COLLECTION_PACKAGE)
COLLECTION_TEMPLATE_VERSION ?= false
# NOTE: This defaults the container image version to the branch that's active
COMPOSE_TAG ?= $(GIT_BRANCH)
@@ -290,13 +301,6 @@ test:
cd awxkit && $(VENV_BASE)/awx/bin/tox -re py3
awx-manage check_migrations --dry-run --check -n 'missing_migration_file'
COLLECTION_TEST_DIRS ?= awx_collection/test/awx
COLLECTION_TEST_TARGET ?=
COLLECTION_PACKAGE ?= awx
COLLECTION_NAMESPACE ?= awx
COLLECTION_INSTALL = ~/.ansible/collections/ansible_collections/$(COLLECTION_NAMESPACE)/$(COLLECTION_PACKAGE)
COLLECTION_TEMPLATE_VERSION ?= false
test_collection:
rm -f $(shell ls -d $(VENV_BASE)/awx/lib/python* | head -n 1)/no-global-site-packages.txt
if [ "$(VENV_BASE)" ]; then \
@@ -339,7 +343,7 @@ test_collection_sanity:
if ! [ -x "$(shell command -v ansible-test)" ]; then pip install ansible-core; fi
ansible --version
COLLECTION_VERSION=1.0.0 make install_collection
cd $(COLLECTION_INSTALL) && ansible-test sanity --exclude=plugins/modules/export.py
cd $(COLLECTION_INSTALL) && ansible-test sanity $(COLLECTION_SANITY_ARGS)
test_collection_integration: install_collection
cd $(COLLECTION_INSTALL) && ansible-test integration $(COLLECTION_TEST_TARGET)

View File

@@ -411,9 +411,11 @@ class AWXReceptorJob:
unit_status = receptor_ctl.simple_command(f'work status {self.unit_id}')
detail = unit_status.get('Detail', None)
state_name = unit_status.get('StateName', None)
stdout_size = unit_status.get('StdoutSize', 0)
except Exception:
detail = ''
state_name = ''
stdout_size = 0
logger.exception(f'An error was encountered while getting status for work unit {self.unit_id}')
if 'exceeded quota' in detail:
@@ -424,9 +426,16 @@ class AWXReceptorJob:
return
try:
resultsock = receptor_ctl.get_work_results(self.unit_id, return_sockfile=True)
lines = resultsock.readlines()
receptor_output = b"".join(lines).decode()
receptor_output = ''
if state_name == 'Failed' and self.task.runner_callback.event_ct == 0:
# if receptor work unit failed and no events were emitted, work results may
# contain useful information about why the job failed. In case stdout is
# massive, only ask for last 1000 bytes
startpos = max(stdout_size - 1000, 0)
resultsock, resultfile = receptor_ctl.get_work_results(self.unit_id, startpos=startpos, return_socket=True, return_sockfile=True)
resultsock.setblocking(False) # this makes resultfile reads non blocking
lines = resultfile.readlines()
receptor_output = b"".join(lines).decode()
if receptor_output:
self.task.runner_callback.delay_update(result_traceback=receptor_output)
elif detail:

View File

@@ -55,7 +55,6 @@ function DateTimePicker({ dateFieldName, timeFieldName, label }) {
onChange={onDateChange}
/>
<TimePicker
placeholder="hh:mm AM/PM"
stepMinutes={15}
aria-label={
timeFieldName.startsWith('start') ? t`Start time` : t`End time`

View File

@@ -0,0 +1,93 @@
import React, { useState } from 'react';
import { t } from '@lingui/macro';
import {
Button,
Switch,
Toolbar,
ToolbarContent,
ToolbarItem,
} from '@patternfly/react-core';
import { PencilAltIcon } from '@patternfly/react-icons';
import {
TableComposable,
Tbody,
Thead,
Th,
Tr,
Td,
} from '@patternfly/react-table';
import { useField } from 'formik';
import ContentEmpty from 'components/ContentEmpty';
function FrequenciesList({ openWizard }) {
const [isShowingRules, setIsShowingRules] = useState(true);
const [frequencies] = useField('frequencies');
const list = (freq) => (
<Tr key={freq.rrule}>
<Td>{freq.frequency}</Td>
<Td>{freq.rrule}</Td>
<Td>{t`End`}</Td>
<Td>
<Button
variant="plain"
aria-label={t`Click to toggle default value`}
ouiaId={freq ? `${freq}-button` : 'new-freq-button'}
onClick={() => {
openWizard(true);
}}
>
<PencilAltIcon />
</Button>
</Td>
</Tr>
);
return (
<>
<Toolbar>
<ToolbarContent>
<ToolbarItem>
<Button
onClick={() => {
openWizard(true);
}}
variant="secondary"
>
{isShowingRules ? t`Add RRules` : t`Add Exception`}
</Button>
</ToolbarItem>
<ToolbarItem>
<Switch
label={t`Occurances`}
labelOff={t`Exceptions`}
isChecked={isShowingRules}
onChange={(isChecked) => {
setIsShowingRules(isChecked);
}}
/>
</ToolbarItem>
</ToolbarContent>
</Toolbar>
<div css="overflow: auto">
{frequencies.value[0].frequency === '' &&
frequencies.value.length < 2 ? (
<ContentEmpty title={t`RRules`} message={t`Add RRules`} />
) : (
<TableComposable aria-label={t`RRules`} ouiaId="rrules-list">
<Thead>
<Tr>
<Th>{t`Frequency`}</Th>
<Th>{t`RRule`}</Th>
<Th>{t`Ending`}</Th>
<Th>{t`Actions`}</Th>
</Tr>
</Thead>
<Tbody>{frequencies.value.map((freq, i) => list(freq, i))}</Tbody>
</TableComposable>
)}
</div>
</>
);
}
export default FrequenciesList;

View File

@@ -1,568 +0,0 @@
import 'styled-components/macro';
import React from 'react';
import styled from 'styled-components';
import { useField } from 'formik';
import { t, Trans, Plural } from '@lingui/macro';
import { RRule } from 'rrule';
import {
Checkbox as _Checkbox,
FormGroup,
Radio,
TextInput,
} from '@patternfly/react-core';
import { required, requiredPositiveInteger } from 'util/validators';
import AnsibleSelect from '../../AnsibleSelect';
import FormField from '../../FormField';
import DateTimePicker from './DateTimePicker';
const RunOnRadio = styled(Radio)`
display: flex;
align-items: center;
label {
display: block;
width: 100%;
}
:not(:last-of-type) {
margin-bottom: 10px;
}
select:not(:first-of-type) {
margin-left: 10px;
}
`;
const RunEveryLabel = styled.p`
display: flex;
align-items: center;
`;
const Checkbox = styled(_Checkbox)`
:not(:last-of-type) {
margin-right: 10px;
}
`;
const FrequencyDetailSubform = ({ frequency, prefix, isException }) => {
const id = prefix.replace('.', '-');
const [runOnDayMonth] = useField({
name: `${prefix}.runOnDayMonth`,
});
const [runOnDayNumber] = useField({
name: `${prefix}.runOnDayNumber`,
});
const [runOnTheOccurrence] = useField({
name: `${prefix}.runOnTheOccurrence`,
});
const [runOnTheDay] = useField({
name: `${prefix}.runOnTheDay`,
});
const [runOnTheMonth] = useField({
name: `${prefix}.runOnTheMonth`,
});
const [startDate] = useField(`${prefix}.startDate`);
const [daysOfWeek, daysOfWeekMeta, daysOfWeekHelpers] = useField({
name: `${prefix}.daysOfWeek`,
validate: (val) => {
if (frequency === 'week') {
return required(t`Select a value for this field`)(val?.length > 0);
}
return undefined;
},
});
const [end, endMeta] = useField({
name: `${prefix}.end`,
validate: required(t`Select a value for this field`),
});
const [interval, intervalMeta] = useField({
name: `${prefix}.interval`,
validate: requiredPositiveInteger(),
});
const [runOn, runOnMeta] = useField({
name: `${prefix}.runOn`,
validate: (val) => {
if (frequency === 'month' || frequency === 'year') {
return required(t`Select a value for this field`)(val);
}
return undefined;
},
});
const monthOptions = [
{
key: 'january',
value: 1,
label: t`January`,
},
{
key: 'february',
value: 2,
label: t`February`,
},
{
key: 'march',
value: 3,
label: t`March`,
},
{
key: 'april',
value: 4,
label: t`April`,
},
{
key: 'may',
value: 5,
label: t`May`,
},
{
key: 'june',
value: 6,
label: t`June`,
},
{
key: 'july',
value: 7,
label: t`July`,
},
{
key: 'august',
value: 8,
label: t`August`,
},
{
key: 'september',
value: 9,
label: t`September`,
},
{
key: 'october',
value: 10,
label: t`October`,
},
{
key: 'november',
value: 11,
label: t`November`,
},
{
key: 'december',
value: 12,
label: t`December`,
},
];
const updateDaysOfWeek = (day, checked) => {
const newDaysOfWeek = daysOfWeek.value ? [...daysOfWeek.value] : [];
daysOfWeekHelpers.setTouched(true);
if (checked) {
newDaysOfWeek.push(day);
daysOfWeekHelpers.setValue(newDaysOfWeek);
} else {
daysOfWeekHelpers.setValue(
newDaysOfWeek.filter((selectedDay) => selectedDay !== day)
);
}
};
const getPeriodLabel = () => {
switch (frequency) {
case 'minute':
return t`Minute`;
case 'hour':
return t`Hour`;
case 'day':
return t`Day`;
case 'week':
return t`Week`;
case 'month':
return t`Month`;
case 'year':
return t`Year`;
default:
throw new Error(t`Frequency did not match an expected value`);
}
};
const getRunEveryLabel = () => {
const intervalValue = interval.value;
switch (frequency) {
case 'minute':
return <Plural value={intervalValue} one="minute" other="minutes" />;
case 'hour':
return <Plural value={intervalValue} one="hour" other="hours" />;
case 'day':
return <Plural value={intervalValue} one="day" other="days" />;
case 'week':
return <Plural value={intervalValue} one="week" other="weeks" />;
case 'month':
return <Plural value={intervalValue} one="month" other="months" />;
case 'year':
return <Plural value={intervalValue} one="year" other="years" />;
default:
throw new Error(t`Frequency did not match an expected value`);
}
};
return (
<>
<p css="grid-column: 1/-1">
<b>{getPeriodLabel()}</b>
</p>
<FormGroup
name={`${prefix}.interval`}
fieldId={`schedule-run-every-${id}`}
helperTextInvalid={intervalMeta.error}
isRequired
validated={
!intervalMeta.touched || !intervalMeta.error ? 'default' : 'error'
}
label={isException ? t`Skip every` : t`Run every`}
>
<div css="display: flex">
<TextInput
css="margin-right: 10px;"
id={`schedule-run-every-${id}`}
type="number"
min="1"
step="1"
{...interval}
onChange={(value, event) => {
interval.onChange(event);
}}
/>
<RunEveryLabel>{getRunEveryLabel()}</RunEveryLabel>
</div>
</FormGroup>
{frequency === 'week' && (
<FormGroup
name={`${prefix}.daysOfWeek`}
fieldId={`schedule-days-of-week-${id}`}
helperTextInvalid={daysOfWeekMeta.error}
isRequired
validated={
!daysOfWeekMeta.touched || !daysOfWeekMeta.error
? 'default'
: 'error'
}
label={t`On days`}
>
<div css="display: flex">
<Checkbox
label={t`Sun`}
isChecked={daysOfWeek.value?.includes(RRule.SU)}
onChange={(checked) => {
updateDaysOfWeek(RRule.SU, checked);
}}
aria-label={t`Sunday`}
id={`schedule-days-of-week-sun-${id}`}
ouiaId={`schedule-days-of-week-sun-${id}`}
name={`${prefix}.daysOfWeek`}
/>
<Checkbox
label={t`Mon`}
isChecked={daysOfWeek.value?.includes(RRule.MO)}
onChange={(checked) => {
updateDaysOfWeek(RRule.MO, checked);
}}
aria-label={t`Monday`}
id={`schedule-days-of-week-mon-${id}`}
ouiaId={`schedule-days-of-week-mon-${id}`}
name={`${prefix}.daysOfWeek`}
/>
<Checkbox
label={t`Tue`}
isChecked={daysOfWeek.value?.includes(RRule.TU)}
onChange={(checked) => {
updateDaysOfWeek(RRule.TU, checked);
}}
aria-label={t`Tuesday`}
id={`schedule-days-of-week-tue-${id}`}
ouiaId={`schedule-days-of-week-tue-${id}`}
name={`${prefix}.daysOfWeek`}
/>
<Checkbox
label={t`Wed`}
isChecked={daysOfWeek.value?.includes(RRule.WE)}
onChange={(checked) => {
updateDaysOfWeek(RRule.WE, checked);
}}
aria-label={t`Wednesday`}
id={`schedule-days-of-week-wed-${id}`}
ouiaId={`schedule-days-of-week-wed-${id}`}
name={`${prefix}.daysOfWeek`}
/>
<Checkbox
label={t`Thu`}
isChecked={daysOfWeek.value?.includes(RRule.TH)}
onChange={(checked) => {
updateDaysOfWeek(RRule.TH, checked);
}}
aria-label={t`Thursday`}
id={`schedule-days-of-week-thu-${id}`}
ouiaId={`schedule-days-of-week-thu-${id}`}
name={`${prefix}.daysOfWeek`}
/>
<Checkbox
label={t`Fri`}
isChecked={daysOfWeek.value?.includes(RRule.FR)}
onChange={(checked) => {
updateDaysOfWeek(RRule.FR, checked);
}}
aria-label={t`Friday`}
id={`schedule-days-of-week-fri-${id}`}
ouiaId={`schedule-days-of-week-fri-${id}`}
name={`${prefix}.daysOfWeek`}
/>
<Checkbox
label={t`Sat`}
isChecked={daysOfWeek.value?.includes(RRule.SA)}
onChange={(checked) => {
updateDaysOfWeek(RRule.SA, checked);
}}
aria-label={t`Saturday`}
id={`schedule-days-of-week-sat-${id}`}
ouiaId={`schedule-days-of-week-sat-${id}`}
name={`${prefix}.daysOfWeek`}
/>
</div>
</FormGroup>
)}
{(frequency === 'month' || frequency === 'year') &&
!Number.isNaN(new Date(startDate.value)) && (
<FormGroup
name={`${prefix}.runOn`}
fieldId={`schedule-run-on-${id}`}
helperTextInvalid={runOnMeta.error}
isRequired
validated={
!runOnMeta.touched || !runOnMeta.error ? 'default' : 'error'
}
label={t`Run on`}
>
<RunOnRadio
id={`schedule-run-on-day-${id}`}
name={`${prefix}.runOn`}
label={
<div css="display: flex;align-items: center;">
{frequency === 'month' && (
<span
id="radio-schedule-run-on-day"
css="margin-right: 10px;"
>
<Trans>Day</Trans>
</span>
)}
{frequency === 'year' && (
<AnsibleSelect
id={`schedule-run-on-day-month-${id}`}
css="margin-right: 10px"
isDisabled={runOn.value !== 'day'}
data={monthOptions}
{...runOnDayMonth}
/>
)}
<TextInput
id={`schedule-run-on-day-number-${id}`}
type="number"
min="1"
max="31"
step="1"
isDisabled={runOn.value !== 'day'}
{...runOnDayNumber}
onChange={(value, event) => {
runOnDayNumber.onChange(event);
}}
/>
</div>
}
value="day"
isChecked={runOn.value === 'day'}
onChange={(value, event) => {
event.target.value = 'day';
runOn.onChange(event);
}}
/>
<RunOnRadio
id={`schedule-run-on-the-${id}`}
name={`${prefix}.runOn`}
label={
<div css="display: flex;align-items: center;">
<span
id={`radio-schedule-run-on-the-${id}`}
css="margin-right: 10px;"
>
<Trans>The</Trans>
</span>
<AnsibleSelect
id={`schedule-run-on-the-occurrence-${id}`}
isDisabled={runOn.value !== 'the'}
data={[
{ value: 1, key: 'first', label: t`First` },
{
value: 2,
key: 'second',
label: t`Second`,
},
{ value: 3, key: 'third', label: t`Third` },
{
value: 4,
key: 'fourth',
label: t`Fourth`,
},
{ value: 5, key: 'fifth', label: t`Fifth` },
{ value: -1, key: 'last', label: t`Last` },
]}
{...runOnTheOccurrence}
/>
<AnsibleSelect
id={`schedule-run-on-the-day-${id}`}
isDisabled={runOn.value !== 'the'}
data={[
{
value: 'sunday',
key: 'sunday',
label: t`Sunday`,
},
{
value: 'monday',
key: 'monday',
label: t`Monday`,
},
{
value: 'tuesday',
key: 'tuesday',
label: t`Tuesday`,
},
{
value: 'wednesday',
key: 'wednesday',
label: t`Wednesday`,
},
{
value: 'thursday',
key: 'thursday',
label: t`Thursday`,
},
{
value: 'friday',
key: 'friday',
label: t`Friday`,
},
{
value: 'saturday',
key: 'saturday',
label: t`Saturday`,
},
{ value: 'day', key: 'day', label: t`Day` },
{
value: 'weekday',
key: 'weekday',
label: t`Weekday`,
},
{
value: 'weekendDay',
key: 'weekendDay',
label: t`Weekend day`,
},
]}
{...runOnTheDay}
/>
{frequency === 'year' && (
<>
<span
id={`of-schedule-run-on-the-month-${id}`}
css="margin-left: 10px;"
>
<Trans>of</Trans>
</span>
<AnsibleSelect
id={`schedule-run-on-the-month-${id}`}
isDisabled={runOn.value !== 'the'}
data={monthOptions}
{...runOnTheMonth}
/>
</>
)}
</div>
}
value="the"
isChecked={runOn.value === 'the'}
onChange={(value, event) => {
event.target.value = 'the';
runOn.onChange(event);
}}
/>
</FormGroup>
)}
<FormGroup
name={`${prefix}.end`}
fieldId={`schedule-end-${id}`}
helperTextInvalid={endMeta.error}
isRequired
validated={!endMeta.touched || !endMeta.error ? 'default' : 'error'}
label={t`End`}
>
<Radio
id={`end-never-${id}`}
name={`${prefix}.end`}
label={t`Never`}
value="never"
isChecked={end.value === 'never'}
onChange={(value, event) => {
event.target.value = 'never';
end.onChange(event);
}}
ouiaId={`end-never-radio-button-${id}`}
/>
<Radio
id={`end-after-${id}`}
name={`${prefix}.end`}
label={t`After number of occurrences`}
value="after"
isChecked={end.value === 'after'}
onChange={(value, event) => {
event.target.value = 'after';
end.onChange(event);
}}
ouiaId={`end-after-radio-button-${id}`}
/>
<Radio
id={`end-on-date-${id}`}
name={`${prefix}.end`}
label={t`On date`}
value="onDate"
isChecked={end.value === 'onDate'}
onChange={(value, event) => {
event.target.value = 'onDate';
end.onChange(event);
}}
ouiaId={`end-on-radio-button-${id}`}
/>
</FormGroup>
{end?.value === 'after' && (
<FormField
id={`schedule-occurrences-${id}`}
label={t`Occurrences`}
name={`${prefix}.occurrences`}
type="number"
min="1"
step="1"
isRequired
/>
)}
{end?.value === 'onDate' && (
<DateTimePicker
dateFieldName={`${prefix}.endDate`}
timeFieldName={`${prefix}.endTime`}
label={t`End date/time`}
/>
)}
</>
);
};
export default FrequencyDetailSubform;

View File

@@ -1,30 +1,12 @@
import React, { useState } from 'react';
import { arrayOf, string } from 'prop-types';
import { t } from '@lingui/macro';
import { useField } from 'formik';
import { RRule } from 'rrule';
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core';
export default function FrequencySelect({
id,
value,
onChange,
onBlur,
placeholderText,
children,
}) {
export default function FrequencySelect({ id, onBlur, placeholderText }) {
const [isOpen, setIsOpen] = useState(false);
const onSelect = (event, selectedValue) => {
if (selectedValue === 'none') {
onChange([]);
setIsOpen(false);
return;
}
const index = value.indexOf(selectedValue);
if (index === -1) {
onChange(value.concat(selectedValue));
} else {
onChange(value.slice(0, index).concat(value.slice(index + 1)));
}
};
const [frequency, , frequencyHelpers] = useField('freq');
const onToggle = (val) => {
if (!val) {
@@ -35,21 +17,26 @@ export default function FrequencySelect({
return (
<Select
variant={SelectVariant.checkbox}
onSelect={onSelect}
selections={value}
onSelect={(e, v) => {
frequencyHelpers.setValue(v);
setIsOpen(false);
}}
selections={frequency.value}
placeholderText={placeholderText}
onToggle={onToggle}
value={frequency.value}
isOpen={isOpen}
ouiaId={`frequency-select-${id}`}
onBlur={() => frequencyHelpers.setTouched(true)}
>
{children}
<SelectOption value={RRule.MINUTELY}>{t`Minute`}</SelectOption>
<SelectOption value={RRule.HOURLY}>{t`Hour`}</SelectOption>
<SelectOption value={RRule.DAILY}>{t`Day`}</SelectOption>
<SelectOption value={RRule.WEEKLY}>{t`Week`}</SelectOption>
<SelectOption value={RRule.MONTHLY}>{t`Month`}</SelectOption>
<SelectOption value={RRule.YEARLY}>{t`Year`}</SelectOption>
</Select>
);
}
FrequencySelect.propTypes = {
value: arrayOf(string).isRequired,
};
export { SelectOption, SelectVariant };

View File

@@ -0,0 +1,77 @@
import React from 'react';
import { t } from '@lingui/macro';
import AnsibleSelect from 'components/AnsibleSelect';
import styled from 'styled-components';
import {
FormGroup,
Checkbox as _Checkbox,
Grid,
GridItem,
} from '@patternfly/react-core';
import { useField } from 'formik';
import { bysetposOptions, monthOptions } from './scheduleFormHelpers';
const GroupWrapper = styled(FormGroup)`
&& .pf-c-form__group-control {
display: flex;
padding-top: 10px;
}
&& .pf-c-form__group-label {
padding-top: 20px;
}
`;
const Checkbox = styled(_Checkbox)`
:not(:last-of-type) {
margin-right: 10px;
}
`;
function MonthandYearForm({ id }) {
const [bySetPos, , bySetPosHelpers] = useField('bysetpos');
const [byMonth, , byMonthHelpers] = useField('bymonth');
return (
<>
<GroupWrapper
fieldId={`schedule-run-on-${id}`}
label={<b>{t`Run on a specific month`}</b>}
>
<Grid hasGutter>
{monthOptions.map((month) => (
<GridItem key={month.label} span={2} rowSpan={2}>
<Checkbox
label={month.label}
isChecked={byMonth.value?.includes(month.value)}
onChange={(checked) => {
if (checked) {
byMonthHelpers.setValue([...byMonth.value, month.value]);
} else {
const removed = byMonth.value.filter(
(i) => i !== month.value
);
byMonthHelpers.setValue(removed);
}
}}
id={`bymonth-${month.label}`}
ouiaId={`bymonth-${month.label}`}
name="bymonth"
/>
</GridItem>
))}
</Grid>
</GroupWrapper>
<GroupWrapper
label={<b>{t`Run on a specific week day at monthly intervals`}</b>}
>
<AnsibleSelect
id={`schedule-run-on-the-occurrence-${id}`}
data={bysetposOptions}
{...bySetPos}
onChange={(e, v) => {
bySetPosHelpers.setValue(v);
}}
/>
</GroupWrapper>
</>
);
}
export default MonthandYearForm;

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { t } from '@lingui/macro';
import styled from 'styled-components';
import { useField } from 'formik';
import { FormGroup, TextInput } from '@patternfly/react-core';
const GroupWrapper = styled(FormGroup)`
&& .pf-c-form__group-control {
display: flex;
padding-top: 10px;
}
&& .pf-c-form__group-label {
padding-top: 20px;
}
`;
function OrdinalDayForm() {
const [byMonthDay] = useField('bymonthday');
const [byYearDay] = useField('byyearday');
return (
<GroupWrapper
label={<b>{t`On a specific number day`}</b>}
name="ordinalDay"
>
<TextInput
placeholder={t`Run on a day of month`}
aria-label={t`Type a numbered day`}
type="number"
onChange={(value, event) => {
byMonthDay.onChange(event);
}}
/>
<TextInput
placeholder={t`Run on a day of year`}
aria-label={t`Type a numbered day`}
type="number"
onChange={(value, event) => {
byYearDay.onChange(event);
}}
/>
</GroupWrapper>
);
}
export default OrdinalDayForm;

View File

@@ -0,0 +1,67 @@
import React from 'react';
import { useField } from 'formik';
import { t } from '@lingui/macro';
import { FormGroup, Radio } from '@patternfly/react-core';
import FormField from 'components/FormField';
import DateTimePicker from './DateTimePicker';
function ScheduleEndForm() {
const [endType, , { setValue }] = useField('endingType');
const [count] = useField('count');
return (
<>
<FormGroup name="end" label={t`End`}>
<Radio
id="endNever"
name={t`Never End`}
label={t`Never`}
value="never"
isChecked={endType.value === 'never'}
onChange={() => {
setValue('never');
}}
/>
<Radio
name="Count"
id="after"
label={t`After number of occurrences`}
value="after"
isChecked={endType.value === 'after'}
onChange={() => {
setValue('after');
}}
/>
<Radio
name="End Date"
label={t`On date`}
value="onDate"
id="endDate"
isChecked={endType.value === 'onDate'}
onChange={() => {
setValue('onDate');
}}
/>
</FormGroup>
{endType.value === 'after' && (
<FormField
label={t`Occurrences`}
name="count"
type="number"
min="1"
step="1"
isRequired
{...count}
/>
)}
{endType.value === 'onDate' && (
<DateTimePicker
dateFieldName="endDate"
timeFieldName="endTime"
label={t`End date/time`}
/>
)}
</>
);
}
export default ScheduleEndForm;

View File

@@ -18,14 +18,9 @@ import SchedulePromptableFields from './SchedulePromptableFields';
import ScheduleFormFields from './ScheduleFormFields';
import UnsupportedScheduleForm from './UnsupportedScheduleForm';
import parseRuleObj, { UnsupportedRRuleError } from './parseRuleObj';
import buildRuleObj from './buildRuleObj';
import buildRuleSet from './buildRuleSet';
const NUM_DAYS_PER_FREQUENCY = {
week: 7,
month: 31,
year: 365,
};
import ScheduleFormWizard from './ScheduleFormWizard';
import FrequenciesList from './FrequenciesList';
// import { validateSchedule } from './scheduleFormHelpers';
function ScheduleForm({
hasDaysToKeepField,
@@ -40,15 +35,16 @@ function ScheduleForm({
}) {
const [isWizardOpen, setIsWizardOpen] = useState(false);
const [isSaveDisabled, setIsSaveDisabled] = useState(false);
const [isScheduleWizardOpen, setIsScheduleWizardOpen] = useState(false);
const originalLabels = useRef([]);
const originalInstanceGroups = useRef([]);
let rruleError;
const now = DateTime.now();
const closestQuarterHour = DateTime.fromMillis(
Math.ceil(now.ts / 900000) * 900000
);
const tomorrow = closestQuarterHour.plus({ days: 1 });
const isTemplate =
resource.type === 'workflow_job_template' ||
resource.type === 'job_template';
@@ -283,69 +279,10 @@ function ScheduleForm({
}
const [currentDate, time] = dateToInputDateTime(closestQuarterHour.toISO());
const [tomorrowDate] = dateToInputDateTime(tomorrow.toISO());
const initialFrequencyOptions = {
minute: {
interval: 1,
end: 'never',
occurrences: 1,
endDate: tomorrowDate,
endTime: time,
},
hour: {
interval: 1,
end: 'never',
occurrences: 1,
endDate: tomorrowDate,
endTime: time,
},
day: {
interval: 1,
end: 'never',
occurrences: 1,
endDate: tomorrowDate,
endTime: time,
},
week: {
interval: 1,
end: 'never',
occurrences: 1,
endDate: tomorrowDate,
endTime: time,
daysOfWeek: [],
},
month: {
interval: 1,
end: 'never',
occurrences: 1,
endDate: tomorrowDate,
endTime: time,
runOn: 'day',
runOnTheOccurrence: 1,
runOnTheDay: 'sunday',
runOnDayNumber: 1,
},
year: {
interval: 1,
end: 'never',
occurrences: 1,
endDate: tomorrowDate,
endTime: time,
runOn: 'day',
runOnTheOccurrence: 1,
runOnTheDay: 'sunday',
runOnTheMonth: 1,
runOnDayMonth: 1,
runOnDayNumber: 1,
},
};
const initialValues = {
description: schedule.description || '',
frequency: [],
frequencies: [],
exceptionFrequency: [],
frequencyOptions: initialFrequencyOptions,
exceptionOptions: initialFrequencyOptions,
name: schedule.name || '',
startDate: currentDate,
startTime: time,
@@ -367,11 +304,9 @@ function ScheduleForm({
}
initialValues.daysToKeep = initialDaysToKeep;
}
let overriddenValues = {};
if (schedule.rrule) {
try {
overriddenValues = parseRuleObj(schedule);
parseRuleObj(schedule);
} catch (error) {
if (error instanceof UnsupportedRRuleError) {
return (
@@ -394,89 +329,33 @@ function ScheduleForm({
if (contentLoading) {
return <ContentLoading />;
}
const validate = (values) => {
const errors = {};
values.frequency.forEach((freq) => {
const options = values.frequencyOptions[freq];
const freqErrors = {};
if (
(freq === 'month' || freq === 'year') &&
options.runOn === 'day' &&
(options.runOnDayNumber < 1 || options.runOnDayNumber > 31)
) {
freqErrors.runOn = t`Please select a day number between 1 and 31.`;
}
if (options.end === 'after' && !options.occurrences) {
freqErrors.occurrences = t`Please enter a number of occurrences.`;
}
if (options.end === 'onDate') {
if (
DateTime.fromFormat(
`${values.startDate} ${values.startTime}`,
'yyyy-LL-dd h:mm a'
).toMillis() >=
DateTime.fromFormat(
`${options.endDate} ${options.endTime}`,
'yyyy-LL-dd h:mm a'
).toMillis()
) {
freqErrors.endDate = t`Please select an end date/time that comes after the start date/time.`;
}
if (
DateTime.fromISO(options.endDate)
.diff(DateTime.fromISO(values.startDate), 'days')
.toObject().days < NUM_DAYS_PER_FREQUENCY[freq]
) {
const rule = new RRule(
buildRuleObj({
startDate: values.startDate,
startTime: values.startTime,
frequency: freq,
...options,
})
);
if (rule.all().length === 0) {
errors.startDate = t`Selected date range must have at least 1 schedule occurrence.`;
freqErrors.endDate = t`Selected date range must have at least 1 schedule occurrence.`;
}
}
}
if (Object.keys(freqErrors).length > 0) {
if (!errors.frequencyOptions) {
errors.frequencyOptions = {};
}
errors.frequencyOptions[freq] = freqErrors;
}
});
if (values.exceptionFrequency.length > 0 && !scheduleHasInstances(values)) {
errors.exceptionFrequency = t`This schedule has no occurrences due to the selected exceptions.`;
}
return errors;
};
const frequencies = [];
frequencies.push(parseRuleObj(schedule));
return (
<Config>
{() => (
<Formik
initialValues={{
...initialValues,
...overriddenValues,
frequencyOptions: {
...initialValues.frequencyOptions,
...overriddenValues.frequencyOptions,
},
exceptionOptions: {
...initialValues.exceptionOptions,
...overriddenValues.exceptionOptions,
},
name: schedule.name || '',
description: schedule.description || '',
frequencies: frequencies || [],
freq: RRule.DAILY,
interval: 1,
wkst: RRule.SU,
byweekday: [],
byweekno: [],
bymonth: [],
bymonthday: '',
byyearday: '',
bysetpos: '',
until: schedule.until || null,
endDate: currentDate,
endTime: time,
count: 1,
endingType: 'never',
timezone: schedule.timezone || now.zoneName,
startDate: currentDate,
startTime: time,
}}
onSubmit={(values) => {
submitSchedule(
@@ -488,73 +367,90 @@ function ScheduleForm({
credentials
);
}}
validate={validate}
validate={() => {}}
>
{(formik) => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormColumnLayout>
<ScheduleFormFields
hasDaysToKeepField={hasDaysToKeepField}
zoneOptions={zoneOptions}
zoneLinks={zoneLinks}
/>
{isWizardOpen && (
<SchedulePromptableFields
schedule={schedule}
credentials={credentials}
surveyConfig={surveyConfig}
launchConfig={launchConfig}
resource={resource}
onCloseWizard={() => {
setIsWizardOpen(false);
}}
onSave={() => {
setIsWizardOpen(false);
setIsSaveDisabled(false);
}}
resourceDefaultCredentials={resourceDefaultCredentials}
labels={originalLabels.current}
instanceGroups={originalInstanceGroups.current}
<>
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<FormColumnLayout>
<ScheduleFormFields
hasDaysToKeepField={hasDaysToKeepField}
zoneOptions={zoneOptions}
zoneLinks={zoneLinks}
/>
)}
<FormSubmitError error={submitError} />
<FormFullWidthLayout>
<ActionGroup>
<Button
ouiaId="schedule-form-save-button"
aria-label={t`Save`}
variant="primary"
type="button"
onClick={formik.handleSubmit}
isDisabled={isSaveDisabled}
>
{t`Save`}
</Button>
{isTemplate && showPromptButton && (
{isWizardOpen && (
<SchedulePromptableFields
schedule={schedule}
credentials={credentials}
surveyConfig={surveyConfig}
launchConfig={launchConfig}
resource={resource}
onCloseWizard={() => {
setIsWizardOpen(false);
}}
onSave={() => {
setIsWizardOpen(false);
setIsSaveDisabled(false);
}}
resourceDefaultCredentials={resourceDefaultCredentials}
labels={originalLabels.current}
instanceGroups={originalInstanceGroups.current}
/>
)}
<FormFullWidthLayout>
<FrequenciesList openWizard={setIsScheduleWizardOpen} />
</FormFullWidthLayout>
<FormSubmitError error={submitError} />
<FormFullWidthLayout>
<ActionGroup>
<Button
ouiaId="schedule-form-prompt-button"
ouiaId="schedule-form-save-button"
aria-label={t`Save`}
variant="primary"
type="button"
onClick={formik.handleSubmit}
isDisabled={isSaveDisabled}
>
{t`Save`}
</Button>
<Button
onClick={() => {}}
>{t`Preview occurances`}</Button>
{isTemplate && showPromptButton && (
<Button
ouiaId="schedule-form-prompt-button"
variant="secondary"
type="button"
aria-label={t`Prompt`}
onClick={() => setIsWizardOpen(true)}
>
{t`Prompt`}
</Button>
)}
<Button
ouiaId="schedule-form-cancel-button"
aria-label={t`Cancel`}
variant="secondary"
type="button"
aria-label={t`Prompt`}
onClick={() => setIsWizardOpen(true)}
onClick={handleCancel}
>
{t`Prompt`}
{t`Cancel`}
</Button>
)}
<Button
ouiaId="schedule-form-cancel-button"
aria-label={t`Cancel`}
variant="secondary"
type="button"
onClick={handleCancel}
>
{t`Cancel`}
</Button>
</ActionGroup>
</FormFullWidthLayout>
</FormColumnLayout>
</Form>
</ActionGroup>
</FormFullWidthLayout>
</FormColumnLayout>
</Form>
{isScheduleWizardOpen && (
<ScheduleFormWizard
staticFormFormkik={formik}
isOpen={isScheduleWizardOpen}
handleSave={() => {}}
setIsOpen={setIsScheduleWizardOpen}
/>
)}
</>
)}
</Formik>
)}
@@ -575,24 +471,3 @@ ScheduleForm.defaultProps = {
};
export default ScheduleForm;
function scheduleHasInstances(values) {
let rangeToCheck = 1;
values.frequency.forEach((freq) => {
if (NUM_DAYS_PER_FREQUENCY[freq] > rangeToCheck) {
rangeToCheck = NUM_DAYS_PER_FREQUENCY[freq];
}
});
const ruleSet = buildRuleSet(values, true);
const startDate = DateTime.fromISO(values.startDate);
const endDate = startDate.plus({ days: rangeToCheck });
const instances = ruleSet.between(
startDate.toJSDate(),
endDate.toJSDate(),
true,
(date, i) => i === 0
);
return instances.length > 0;
}

View File

@@ -1,41 +1,27 @@
import React, { useState } from 'react';
import { useField } from 'formik';
import { FormGroup, Title } from '@patternfly/react-core';
import { FormGroup } from '@patternfly/react-core';
import { t } from '@lingui/macro';
import styled from 'styled-components';
import 'styled-components/macro';
import FormField from 'components/FormField';
import { required } from 'util/validators';
import { useConfig } from 'contexts/Config';
import Popover from '../../Popover';
import AnsibleSelect from '../../AnsibleSelect';
import FrequencySelect, { SelectOption } from './FrequencySelect';
import getHelpText from '../../../screens/Template/shared/JobTemplate.helptext';
import { SubFormLayout, FormColumnLayout } from '../../FormLayout';
import FrequencyDetailSubform from './FrequencyDetailSubform';
import DateTimePicker from './DateTimePicker';
import sortFrequencies from './sortFrequencies';
const SelectClearOption = styled(SelectOption)`
& > input[type='checkbox'] {
display: none;
}
`;
export default function ScheduleFormFields({
hasDaysToKeepField,
zoneOptions,
zoneLinks,
setTimeZone,
}) {
const helpText = getHelpText();
const [timezone, timezoneMeta] = useField({
name: 'timezone',
validate: required(t`Select a value for this field`),
});
const [frequency, frequencyMeta, frequencyHelper] = useField({
name: 'frequency',
validate: required(t`Select a value for this field`),
});
const [timezoneMessage, setTimezoneMessage] = useState('');
const warnLinkedTZ = (event, selectedValue) => {
if (zoneLinks[selectedValue]) {
@@ -46,6 +32,7 @@ export default function ScheduleFormFields({
setTimezoneMessage('');
}
timezone.onChange(event, selectedValue);
setTimeZone(zoneLinks(selectedValue));
};
let timezoneValidatedStatus = 'default';
if (timezoneMeta.touched && timezoneMeta.error) {
@@ -55,16 +42,6 @@ export default function ScheduleFormFields({
}
const config = useConfig();
const [exceptionFrequency, exceptionFrequencyMeta, exceptionFrequencyHelper] =
useField({
name: 'exceptionFrequency',
validate: required(t`Select a value for this field`),
});
const updateFrequency = (setFrequency) => (values) => {
setFrequency(values.sort(sortFrequencies));
};
return (
<>
<FormField
@@ -103,33 +80,7 @@ export default function ScheduleFormFields({
onChange={warnLinkedTZ}
/>
</FormGroup>
<FormGroup
name="frequency"
fieldId="schedule-frequency"
helperTextInvalid={frequencyMeta.error}
validated={
!frequencyMeta.touched || !frequencyMeta.error ? 'default' : 'error'
}
label={t`Repeat frequency`}
>
<FrequencySelect
id="schedule-frequency"
onChange={updateFrequency(frequencyHelper.setValue)}
value={frequency.value}
placeholderText={
frequency.value.length ? t`Select frequency` : t`None (run once)`
}
onBlur={frequencyHelper.setTouched}
>
<SelectClearOption value="none">{t`None (run once)`}</SelectClearOption>
<SelectOption value="minute">{t`Minute`}</SelectOption>
<SelectOption value="hour">{t`Hour`}</SelectOption>
<SelectOption value="day">{t`Day`}</SelectOption>
<SelectOption value="week">{t`Week`}</SelectOption>
<SelectOption value="month">{t`Month`}</SelectOption>
<SelectOption value="year">{t`Year`}</SelectOption>
</FrequencySelect>
</FormGroup>
{hasDaysToKeepField ? (
<FormField
id="schedule-days-to-keep"
@@ -140,68 +91,6 @@ export default function ScheduleFormFields({
isRequired
/>
) : null}
{frequency.value.length ? (
<SubFormLayout>
<Title size="md" headingLevel="h4">
{t`Frequency Details`}
</Title>
{frequency.value.map((val) => (
<FormColumnLayout key={val} stacked>
<FrequencyDetailSubform
frequency={val}
prefix={`frequencyOptions.${val}`}
/>
</FormColumnLayout>
))}
<Title
size="md"
headingLevel="h4"
css="margin-top: var(--pf-c-card--child--PaddingRight)"
>{t`Exceptions`}</Title>
<FormColumnLayout stacked>
<FormGroup
name="exceptions"
fieldId="exception-frequency"
helperTextInvalid={exceptionFrequencyMeta.error}
validated={
!exceptionFrequencyMeta.touched || !exceptionFrequencyMeta.error
? 'default'
: 'error'
}
label={t`Add exceptions`}
>
<FrequencySelect
id="exception-frequency"
onChange={updateFrequency(exceptionFrequencyHelper.setValue)}
value={exceptionFrequency.value}
placeholderText={
exceptionFrequency.value.length
? t`Select frequency`
: t`None`
}
onBlur={exceptionFrequencyHelper.setTouched}
>
<SelectClearOption value="none">{t`None`}</SelectClearOption>
<SelectOption value="minute">{t`Minute`}</SelectOption>
<SelectOption value="hour">{t`Hour`}</SelectOption>
<SelectOption value="day">{t`Day`}</SelectOption>
<SelectOption value="week">{t`Week`}</SelectOption>
<SelectOption value="month">{t`Month`}</SelectOption>
<SelectOption value="year">{t`Year`}</SelectOption>
</FrequencySelect>
</FormGroup>
</FormColumnLayout>
{exceptionFrequency.value.map((val) => (
<FormColumnLayout key={val} stacked>
<FrequencyDetailSubform
frequency={val}
prefix={`exceptionOptions.${val}`}
isException
/>
</FormColumnLayout>
))}
</SubFormLayout>
) : null}
</>
);
}

View File

@@ -0,0 +1,199 @@
import React from 'react';
import {
Button,
FormGroup,
TextInput,
Title,
Wizard,
WizardContextConsumer,
WizardFooter,
} from '@patternfly/react-core';
import { t } from '@lingui/macro';
import styled from 'styled-components';
import { RRule } from 'rrule';
import { useField, useFormikContext } from 'formik';
import { DateTime } from 'luxon';
import { formatDateString } from 'util/dates';
import FrequencySelect from './FrequencySelect';
import MonthandYearForm from './MonthandYearForm';
import OrdinalDayForm from './OrdinalDayForm';
import WeekdayForm from './WeekdayForm';
import ScheduleEndForm from './ScheduleEndForm';
import parseRuleObj from './parseRuleObj';
import { buildDtStartObj } from './buildRuleObj';
const GroupWrapper = styled(FormGroup)`
&& .pf-c-form__group-control {
display: flex;
padding-top: 10px;
}
&& .pf-c-form__group-label {
padding-top: 20px;
}
`;
function ScheduleFormWizard({ isOpen, setIsOpen }) {
const { values, resetForm, initialValues } = useFormikContext();
const [freq, freqMeta] = useField('freq');
const [{ value: frequenciesValue }] = useField('frequencies');
const [interval, , intervalHelpers] = useField('interval');
const handleSubmit = (goToStepById) => {
const {
name,
description,
endingType,
endTime,
endDate,
timezone,
startDate,
startTime,
frequencies,
...rest
} = values;
if (endingType === 'onDate') {
const dt = DateTime.fromFormat(
`${endDate} ${endTime}`,
'yyyy-MM-dd h:mm a',
{
zone: timezone,
}
);
rest.until = formatDateString(dt, timezone);
delete rest.count;
}
if (endingType === 'never') delete rest.count;
const rule = new RRule(rest);
const start = buildDtStartObj({
startDate: values.startDate,
startTime: values.startTime,
timezone: values.timezone,
frequency: values.freq,
});
const newFrequency = parseRuleObj({
timezone,
frequency: freq.value,
rrule: rule.toString(),
dtstart: start,
});
if (goToStepById) {
goToStepById(1);
}
resetForm({
values: {
...initialValues,
description: values.description,
name: values.name,
startDate: values.startDate,
startTime: values.startTime,
timezone: values.timezone,
frequencies: frequenciesValue[0].frequency.length
? [...frequenciesValue, newFrequency]
: [newFrequency],
},
});
};
const CustomFooter = (
<WizardFooter>
<WizardContextConsumer>
{({ activeStep, onNext, onBack, goToStepById }) => (
<>
{activeStep.id === 2 ? (
<>
<Button
variant="primary"
onClick={() => {
handleSubmit(true, goToStepById);
}}
>{t`Finish and create new`}</Button>
<Button
variant="secondary"
onClick={() => {
handleSubmit(false);
setIsOpen(false);
}}
>{t`Finish and close`}</Button>
</>
) : (
<Button variant="primary" onClick={onNext}>{t`Next`}</Button>
)}
<Button variant="secondary" onClick={onBack}>{t`Back`}</Button>
<Button
variant="plain"
onClick={() => {
setIsOpen(false);
resetForm({
values: {
...initialValues,
description: values.description,
name: values.name,
startDate: values.startDate,
startTime: values.startTime,
timezone: values.timezone,
frequencies: values.frequencies,
},
});
}}
>{t`Cancel`}</Button>
</>
)}
</WizardContextConsumer>
</WizardFooter>
);
return (
<Wizard
onClose={() => setIsOpen(false)}
isOpen={isOpen}
footer={CustomFooter}
steps={[
{
key: 'frequency',
name: 'Frequency',
id: 1,
component: (
<>
<Title size="md" headingLevel="h4">{t`Repeat frequency`}</Title>
<GroupWrapper
name="freq"
fieldId="schedule-frequency"
isRequired
helperTextInvalid={freqMeta.error}
validated={
!freqMeta.touched || !freqMeta.error ? 'default' : 'error'
}
label={<b>{t`Frequency`}</b>}
>
<FrequencySelect />
</GroupWrapper>
<GroupWrapper isRequired label={<b>{t`Interval`}</b>}>
<TextInput
type="number"
value={interval.value}
placeholder={t`Choose an interval for the schedule`}
aria-label={t`Choose an interval for the schedule`}
onChange={(v) => intervalHelpers.setValue(v)}
/>
</GroupWrapper>
<WeekdayForm />
<MonthandYearForm />
<OrdinalDayForm />
</>
),
},
{
name: 'End',
key: 'end',
id: 2,
component: <ScheduleEndForm />,
},
]}
/>
);
}
export default ScheduleFormWizard;

View File

@@ -0,0 +1,164 @@
import React, { useState } from 'react';
import { t } from '@lingui/macro';
import {
Checkbox as _Checkbox,
FormGroup,
Select,
SelectOption,
} from '@patternfly/react-core';
import { useField } from 'formik';
import { RRule } from 'rrule';
import styled from 'styled-components';
import { weekdayOptions } from './scheduleFormHelpers';
const Checkbox = styled(_Checkbox)`
:not(:last-of-type) {
margin-right: 10px;
}
`;
const GroupWrapper = styled(FormGroup)`
&& .pf-c-form__group-control {
display: flex;
padding-top: 10px;
}
&& .pf-c-form__group-label {
padding-top: 20px;
}
`;
function WeekdayForm({ id }) {
const [isOpen, setIsOpen] = useState(false);
const [daysOfWeek, daysOfWeekMeta, daysOfWeekHelpers] = useField('byweekday');
const [weekStartDay, , weekStartDayHelpers] = useField('wkst');
const updateDaysOfWeek = (day, checked) => {
const newDaysOfWeek = daysOfWeek.value ? [...daysOfWeek.value] : [];
daysOfWeekHelpers.setTouched(true);
if (checked) {
newDaysOfWeek.push(day);
daysOfWeekHelpers.setValue(newDaysOfWeek);
} else {
daysOfWeekHelpers.setValue(
newDaysOfWeek.filter((selectedDay) => selectedDay !== day)
);
}
};
return (
<>
<GroupWrapper
name="wkst"
label={<b>{t`Select the first day of the week`}</b>}
>
<Select
onSelect={(e, value) => {
weekStartDayHelpers.setValue(value);
setIsOpen(false);
}}
onBlur={() => setIsOpen(false)}
selections={weekStartDay.value}
onToggle={(isopen) => setIsOpen(isopen)}
isOpen={isOpen}
id={`schedule-run-on-the-day-${id}`}
onChange={(e, v) => {
weekStartDayHelpers.setValue(v);
}}
{...weekStartDay}
>
{weekdayOptions.map(({ key, value, label }) => (
<SelectOption key={key} value={value}>
{label}
</SelectOption>
))}
</Select>
</GroupWrapper>
<GroupWrapper
name="byweekday"
fieldId={`schedule-days-of-week-${id}`}
helperTextInvalid={daysOfWeekMeta.error}
validated={
!daysOfWeekMeta.touched || !daysOfWeekMeta.error ? 'default' : 'error'
}
label={<b>{t`On selected day(s) of the week`}</b>}
>
<Checkbox
label={t`Sun`}
isChecked={daysOfWeek.value?.includes(RRule.SU)}
onChange={(checked) => {
updateDaysOfWeek(RRule.SU, checked);
}}
aria-label={t`Sunday`}
id={`schedule-days-of-week-sun-${id}`}
ouiaId={`schedule-days-of-week-sun-${id}`}
name="daysOfWeek"
/>
<Checkbox
label={t`Mon`}
isChecked={daysOfWeek.value?.includes(RRule.MO)}
onChange={(checked) => {
updateDaysOfWeek(RRule.MO, checked);
}}
aria-label={t`Monday`}
id={`schedule-days-of-week-mon-${id}`}
ouiaId={`schedule-days-of-week-mon-${id}`}
name="daysOfWeek"
/>
<Checkbox
label={t`Tue`}
isChecked={daysOfWeek.value?.includes(RRule.TU)}
onChange={(checked) => {
updateDaysOfWeek(RRule.TU, checked);
}}
aria-label={t`Tuesday`}
id={`schedule-days-of-week-tue-${id}`}
ouiaId={`schedule-days-of-week-tue-${id}`}
name="daysOfWeek"
/>
<Checkbox
label={t`Wed`}
isChecked={daysOfWeek.value?.includes(RRule.WE)}
onChange={(checked) => {
updateDaysOfWeek(RRule.WE, checked);
}}
aria-label={t`Wednesday`}
id={`schedule-days-of-week-wed-${id}`}
ouiaId={`schedule-days-of-week-wed-${id}`}
name="daysOfWeek"
/>
<Checkbox
label={t`Thu`}
isChecked={daysOfWeek.value?.includes(RRule.TH)}
onChange={(checked) => {
updateDaysOfWeek(RRule.TH, checked);
}}
aria-label={t`Thursday`}
id={`schedule-days-of-week-thu-${id}`}
ouiaId={`schedule-days-of-week-thu-${id}`}
name="daysOfWeek"
/>
<Checkbox
label={t`Fri`}
isChecked={daysOfWeek.value?.includes(RRule.FR)}
onChange={(checked) => {
updateDaysOfWeek(RRule.FR, checked);
}}
aria-label={t`Friday`}
id={`schedule-days-of-week-fri-${id}`}
ouiaId={`schedule-days-of-week-fri-${id}`}
name="daysOfWeek"
/>
<Checkbox
label={t`Sat`}
isChecked={daysOfWeek.value?.includes(RRule.SA)}
onChange={(checked) => {
updateDaysOfWeek(RRule.SA, checked);
}}
aria-label={t`Saturday`}
id={`schedule-days-of-week-sat-${id}`}
ouiaId={`schedule-days-of-week-sat-${id}`}
name="daysOfWeek"
/>
</GroupWrapper>
</>
);
}
export default WeekdayForm;

View File

@@ -1,7 +1,5 @@
import { t } from '@lingui/macro';
import { RRule } from 'rrule';
import { DateTime } from 'luxon';
import { getRRuleDayConstants } from 'util/dates';
window.RRule = RRule;
window.DateTime = DateTime;
@@ -22,7 +20,7 @@ export function buildDtStartObj(values) {
startHour
)}${pad(startMinute)}00`;
const rruleString = values.timezone
? `DTSTART;TZID=${values.timezone}:${dateString}`
? `DTSTART;TZID=${values.timezone}${dateString}`
: `DTSTART:${dateString}Z`;
const rule = RRule.fromString(rruleString);
@@ -38,7 +36,8 @@ function pad(num) {
export default function buildRuleObj(values, includeStart) {
const ruleObj = {
interval: values.interval,
interval: values.interval || 1,
freq: values.freq,
};
if (includeStart) {
@@ -49,68 +48,6 @@ export default function buildRuleObj(values, includeStart) {
);
}
switch (values.frequency) {
case 'none':
ruleObj.count = 1;
ruleObj.freq = RRule.MINUTELY;
break;
case 'minute':
ruleObj.freq = RRule.MINUTELY;
break;
case 'hour':
ruleObj.freq = RRule.HOURLY;
break;
case 'day':
ruleObj.freq = RRule.DAILY;
break;
case 'week':
ruleObj.freq = RRule.WEEKLY;
ruleObj.byweekday = values.daysOfWeek;
break;
case 'month':
ruleObj.freq = RRule.MONTHLY;
if (values.runOn === 'day') {
ruleObj.bymonthday = values.runOnDayNumber;
} else if (values.runOn === 'the') {
ruleObj.bysetpos = parseInt(values.runOnTheOccurrence, 10);
ruleObj.byweekday = getRRuleDayConstants(values.runOnTheDay);
}
break;
case 'year':
ruleObj.freq = RRule.YEARLY;
if (values.runOn === 'day') {
ruleObj.bymonth = parseInt(values.runOnDayMonth, 10);
ruleObj.bymonthday = values.runOnDayNumber;
} else if (values.runOn === 'the') {
ruleObj.bysetpos = parseInt(values.runOnTheOccurrence, 10);
ruleObj.byweekday = getRRuleDayConstants(values.runOnTheDay);
ruleObj.bymonth = parseInt(values.runOnTheMonth, 10);
}
break;
default:
throw new Error(t`Frequency did not match an expected value`);
}
if (values.frequency !== 'none') {
switch (values.end) {
case 'never':
break;
case 'after':
ruleObj.count = values.occurrences;
break;
case 'onDate': {
ruleObj.until = buildDateTime(
values.endDate,
values.endTime,
values.timezone
);
break;
}
default:
throw new Error(t`End did not match an expected value (${values.end})`);
}
}
return ruleObj;
}

View File

@@ -1,5 +1,6 @@
import { RRule, RRuleSet } from 'rrule';
import buildRuleObj, { buildDtStartObj } from './buildRuleObj';
import { FREQUENCIESCONSTANTS } from './scheduleFormHelpers';
window.RRuleSet = RRuleSet;
@@ -12,42 +13,31 @@ export default function buildRuleSet(values, useUTCStart) {
startDate: values.startDate,
startTime: values.startTime,
timezone: values.timezone,
frequency: values.freq,
});
set.rrule(startRule);
}
if (values.frequency.length === 0) {
const rule = buildRuleObj(
{
startDate: values.startDate,
startTime: values.startTime,
timezone: values.timezone,
frequency: 'none',
interval: 1,
},
useUTCStart
);
set.rrule(new RRule(rule));
}
frequencies.forEach((frequency) => {
if (!values.frequency.includes(frequency)) {
values.frequencies.forEach(({ frequency, rrule }) => {
if (!frequencies.includes(frequency)) {
return;
}
const rule = buildRuleObj(
{
startDate: values.startDate,
startTime: values.startTime,
timezone: values.timezone,
frequency,
...values.frequencyOptions[frequency],
freq: FREQUENCIESCONSTANTS[frequency],
rrule,
},
useUTCStart
true
);
set.rrule(new RRule(rule));
});
frequencies.forEach((frequency) => {
values.exceptions?.forEach(({ frequency, rrule }) => {
if (!values.exceptionFrequency?.includes(frequency)) {
return;
}
@@ -56,8 +46,8 @@ export default function buildRuleSet(values, useUTCStart) {
startDate: values.startDate,
startTime: values.startTime,
timezone: values.timezone,
frequency,
...values.exceptionOptions[frequency],
freq: FREQUENCIESCONSTANTS[frequency],
rrule,
},
useUTCStart
);

View File

@@ -12,12 +12,14 @@ export class UnsupportedRRuleError extends Error {
export default function parseRuleObj(schedule) {
let values = {
frequency: [],
frequencyOptions: {},
exceptionFrequency: [],
exceptionOptions: {},
frequency: '',
rrules: '',
timezone: schedule.timezone,
};
if (Object.values(schedule).length === 0) {
return values;
}
const ruleset = rrulestr(schedule.rrule.replace(' ', '\n'), {
forceset: true,
});
@@ -40,25 +42,9 @@ export default function parseRuleObj(schedule) {
}
});
if (isSingleOccurrence(values)) {
values.frequency = [];
values.frequencyOptions = {};
}
return values;
}
function isSingleOccurrence(values) {
if (values.frequency.length > 1) {
return false;
}
if (values.frequency[0] !== 'minute') {
return false;
}
const options = values.frequencyOptions.minute;
return options.end === 'after' && options.occurrences === 1;
}
function parseDtstart(schedule, values) {
// TODO: should this rely on DTSTART in rruleset rather than schedule.dtstart?
const [startDate, startTime] = dateToInputDateTime(
@@ -81,27 +67,12 @@ const frequencyTypes = {
[RRule.YEARLY]: 'year',
};
function parseRrule(rruleString, schedule, values) {
const { frequency, options } = parseRule(
rruleString,
schedule,
values.exceptionFrequency
);
function parseRrule(rruleString, schedule) {
const { frequency } = parseRule(rruleString, schedule);
if (values.frequencyOptions[frequency]) {
throw new UnsupportedRRuleError(
'Duplicate exception frequency types not supported'
);
}
const freq = { frequency, rrule: rruleString };
return {
...values,
frequency: [...values.frequency, frequency].sort(sortFrequencies),
frequencyOptions: {
...values.frequencyOptions,
[frequency]: options,
},
};
return freq;
}
function parseExRule(exruleString, schedule, values) {
@@ -129,20 +100,10 @@ function parseExRule(exruleString, schedule, values) {
};
}
function parseRule(ruleString, schedule, frequencies) {
function parseRule(ruleString, schedule) {
const {
origOptions: {
bymonth,
bymonthday,
bysetpos,
byweekday,
count,
freq,
interval,
until,
},
origOptions: { count, freq, interval, until, ...rest },
} = RRule.fromString(ruleString);
const now = DateTime.now();
const closestQuarterHour = DateTime.fromMillis(
Math.ceil(now.ts / 900000) * 900000
@@ -156,17 +117,17 @@ function parseRule(ruleString, schedule, frequencies) {
endTime: time,
occurrences: 1,
interval: 1,
end: 'never',
endingType: 'never',
};
if (until) {
options.end = 'onDate';
if (until?.length) {
options.endingType = 'onDate';
const end = DateTime.fromISO(until.toISOString());
const [endDate, endTime] = dateToInputDateTime(end, schedule.timezone);
options.endDate = endDate;
options.endTime = endTime;
} else if (count) {
options.end = 'after';
options.endingType = 'after';
options.occurrences = count;
}
@@ -178,101 +139,10 @@ function parseRule(ruleString, schedule, frequencies) {
throw new Error(`Unexpected rrule frequency: ${freq}`);
}
const frequency = frequencyTypes[freq];
if (frequencies.includes(frequency)) {
throw new Error(`Duplicate frequency types not supported (${frequency})`);
}
if (freq === RRule.WEEKLY && byweekday) {
options.daysOfWeek = byweekday;
}
if (freq === RRule.MONTHLY) {
options.runOn = 'day';
options.runOnTheOccurrence = 1;
options.runOnTheDay = 'sunday';
options.runOnDayNumber = 1;
if (bymonthday) {
options.runOnDayNumber = bymonthday;
}
if (bysetpos) {
options.runOn = 'the';
options.runOnTheOccurrence = bysetpos;
options.runOnTheDay = generateRunOnTheDay(byweekday);
}
}
if (freq === RRule.YEARLY) {
options.runOn = 'day';
options.runOnTheOccurrence = 1;
options.runOnTheDay = 'sunday';
options.runOnTheMonth = 1;
options.runOnDayMonth = 1;
options.runOnDayNumber = 1;
if (bymonthday) {
options.runOnDayNumber = bymonthday;
options.runOnDayMonth = bymonth;
}
if (bysetpos) {
options.runOn = 'the';
options.runOnTheOccurrence = bysetpos;
options.runOnTheDay = generateRunOnTheDay(byweekday);
options.runOnTheMonth = bymonth;
}
}
return {
frequency,
options,
...options,
...rest,
};
}
function generateRunOnTheDay(days = []) {
if (
[
RRule.MO,
RRule.TU,
RRule.WE,
RRule.TH,
RRule.FR,
RRule.SA,
RRule.SU,
].every((element) => days.indexOf(element) > -1)
) {
return 'day';
}
if (
[RRule.MO, RRule.TU, RRule.WE, RRule.TH, RRule.FR].every(
(element) => days.indexOf(element) > -1
)
) {
return 'weekday';
}
if ([RRule.SA, RRule.SU].every((element) => days.indexOf(element) > -1)) {
return 'weekendDay';
}
if (days.indexOf(RRule.MO) > -1) {
return 'monday';
}
if (days.indexOf(RRule.TU) > -1) {
return 'tuesday';
}
if (days.indexOf(RRule.WE) > -1) {
return 'wednesday';
}
if (days.indexOf(RRule.TH) > -1) {
return 'thursday';
}
if (days.indexOf(RRule.FR) > -1) {
return 'friday';
}
if (days.indexOf(RRule.SA) > -1) {
return 'saturday';
}
if (days.indexOf(RRule.SU) > -1) {
return 'sunday';
}
return null;
}

View File

@@ -0,0 +1,232 @@
import { t } from '@lingui/macro';
import { DateTime } from 'luxon';
import { RRule } from 'rrule';
import buildRuleObj from './buildRuleObj';
import buildRuleSet from './buildRuleSet';
// const NUM_DAYS_PER_FREQUENCY = {
// week: 7,
// month: 31,
// year: 365,
// };
// const validateSchedule = () =>
// const errors = {};
// values.frequencies.forEach((freq) => {
// const options = values.frequencyOptions[freq];
// const freqErrors = {};
// if (
// (freq === 'month' || freq === 'year') &&
// options.runOn === 'day' &&
// (options.runOnDayNumber < 1 || options.runOnDayNumber > 31)
// ) {
// freqErrors.runOn = t`Please select a day number between 1 and 31.`;
// }
// if (options.end === 'after' && !options.occurrences) {
// freqErrors.occurrences = t`Please enter a number of occurrences.`;
// }
// if (options.end === 'onDate') {
// if (
// DateTime.fromFormat(
// `${values.startDate} ${values.startTime}`,
// 'yyyy-LL-dd h:mm a'
// ).toMillis() >=
// DateTime.fromFormat(
// `${options.endDate} ${options.endTime}`,
// 'yyyy-LL-dd h:mm a'
// ).toMillis()
// ) {
// freqErrors.endDate = t`Please select an end date/time that comes after the start date/time.`;
// }
// if (
// DateTime.fromISO(options.endDate)
// .diff(DateTime.fromISO(values.startDate), 'days')
// .toObject().days < NUM_DAYS_PER_FREQUENCY[freq]
// ) {
// const rule = new RRule(
// buildRuleObj({
// startDate: values.startDate,
// startTime: values.startTime,
// frequencies: freq,
// ...options,
// })
// );
// if (rule.all().length === 0) {
// errors.startDate = t`Selected date range must have at least 1 schedule occurrence.`;
// freqErrors.endDate = t`Selected date range must have at least 1 schedule occurrence.`;
// }
// }
// }
// if (Object.keys(freqErrors).length > 0) {
// if (!errors.frequencyOptions) {
// errors.frequencyOptions = {};
// }
// errors.frequencyOptions[freq] = freqErrors;
// }
// });
// if (values.exceptionFrequency.length > 0 && !scheduleHasInstances(values)) {
// errors.exceptionFrequency = t`This schedule has no occurrences due to the
// selected exceptions.`;
// }
// ({});
// function scheduleHasInstances(values) {
// let rangeToCheck = 1;
// values.frequencies.forEach((freq) => {
// if (NUM_DAYS_PER_FREQUENCY[freq] > rangeToCheck) {
// rangeToCheck = NUM_DAYS_PER_FREQUENCY[freq];
// }
// });
// const ruleSet = buildRuleSet(values, true);
// const startDate = DateTime.fromISO(values.startDate);
// const endDate = startDate.plus({ days: rangeToCheck });
// const instances = ruleSet.between(
// startDate.toJSDate(),
// endDate.toJSDate(),
// true,
// (date, i) => i === 0
// );
// return instances.length > 0;
// }
const bysetposOptions = [
{ value: '', key: 'none', label: 'None' },
{ value: 1, key: 'first', label: t`First` },
{
value: 2,
key: 'second',
label: t`Second`,
},
{ value: 3, key: 'third', label: t`Third` },
{
value: 4,
key: 'fourth',
label: t`Fourth`,
},
{ value: 5, key: 'fifth', label: t`Fifth` },
{ value: -1, key: 'last', label: t`Last` },
];
const monthOptions = [
{
key: 'january',
value: 1,
label: t`January`,
},
{
key: 'february',
value: 2,
label: t`February`,
},
{
key: 'march',
value: 3,
label: t`March`,
},
{
key: 'april',
value: 4,
label: t`April`,
},
{
key: 'may',
value: 5,
label: t`May`,
},
{
key: 'june',
value: 6,
label: t`June`,
},
{
key: 'july',
value: 7,
label: t`July`,
},
{
key: 'august',
value: 8,
label: t`August`,
},
{
key: 'september',
value: 9,
label: t`September`,
},
{
key: 'october',
value: 10,
label: t`October`,
},
{
key: 'november',
value: 11,
label: t`November`,
},
{
key: 'december',
value: 12,
label: t`December`,
},
];
const weekdayOptions = [
{
value: RRule.SU,
key: 'sunday',
label: t`Sunday`,
},
{
value: RRule.MO,
key: 'monday',
label: t`Monday`,
},
{
value: RRule.TU,
key: 'tuesday',
label: t`Tuesday`,
},
{
value: RRule.WE,
key: 'wednesday',
label: t`Wednesday`,
},
{
value: RRule.TH,
key: 'thursday',
label: t`Thursday`,
},
{
value: RRule.FR,
key: 'friday',
label: t`Friday`,
},
{
value: RRule.SA,
key: 'saturday',
label: t`Saturday`,
},
];
const FREQUENCIESCONSTANTS = {
minute: RRule.MINUTELY,
hour: RRule.HOURLY,
day: RRule.DAILY,
week: RRule.WEEKLY,
month: RRule.MONTHLY,
year: RRule.YEARLY,
};
export {
monthOptions,
weekdayOptions,
bysetposOptions,
// validateSchedule,
FREQUENCIESCONSTANTS,
};

View File

@@ -41,7 +41,7 @@ function JobEvent({
if (lineNumber < 0) {
return null;
}
const canToggle = index === toggleLineIndex;
const canToggle = index === toggleLineIndex && !event.isTracebackOnly;
return (
<JobEventLine
onClick={isClickable ? onJobEventClick : undefined}
@@ -55,7 +55,7 @@ function JobEvent({
onToggle={onToggleCollapsed}
/>
<JobEventLineNumber>
{lineNumber}
{!event.isTracebackOnly ? lineNumber : ''}
<JobEventEllipsis isCollapsed={isCollapsed && canToggle} />
</JobEventLineNumber>
<JobEventLineText

View File

@@ -29,8 +29,11 @@ export function prependTraceback(job, events) {
start_line: 0,
};
const firstIndex = events.findIndex((jobEvent) => jobEvent.counter === 1);
if (firstIndex && events[firstIndex]?.stdout) {
const stdoutLines = events[firstIndex].stdout.split('\r\n');
if (firstIndex > -1) {
if (!events[firstIndex].stdout) {
events[firstIndex].isTracebackOnly = true;
}
const stdoutLines = events[firstIndex].stdout?.split('\r\n') || [];
stdoutLines[0] = tracebackEvent.stdout;
events[firstIndex].stdout = stdoutLines.join('\r\n');
} else {

View File

@@ -0,0 +1,3 @@
---
modules:
python_requires: '>3'

View File

@@ -1,88 +0,0 @@
plugins/module_utils/awxkit.py import-3.9
plugins/module_utils/controller_api.py import-3.9
plugins/modules/ad_hoc_command.py import-3.9
plugins/modules/ad_hoc_command_cancel.py import-3.9
plugins/modules/ad_hoc_command_wait.py import-3.9
plugins/modules/application.py import-3.9
plugins/modules/controller_meta.py import-3.9
plugins/modules/credential.py import-3.92
plugins/modules/credential_input_source.py import-3.9
plugins/modules/credential_type.py import-3.9
plugins/modules/execution_environment.py import-3.9
plugins/modules/export.py import-3.9
plugins/modules/group.py import-3.9
plugins/modules/host.py import-3.9
plugins/modules/import.py import-3.9
plugins/modules/instance.py import-3.9
plugins/modules/instance_group.py import-3.9
plugins/modules/inventory.py import-3.9
plugins/modules/inventory_source.py import-3.9
plugins/modules/inventory_source_update.py import-3.9
plugins/modules/job_cancel.py import-3.9
plugins/modules/job_launch.py import-3.9
plugins/modules/job_list.py import-3.9
plugins/modules/job_template.py import-3.93
plugins/modules/job_wait.py import-3.9
plugins/modules/label.py import-3.9
plugins/modules/license.py import-3.9
plugins/modules/notification_template.py import-3.9
plugins/modules/organization.py import-3.9
plugins/modules/project.py import-3.92
plugins/modules/project_update.py import-3.9
plugins/modules/role.py import-3.9
plugins/modules/schedule.py import-3.9
plugins/modules/settings.py import-3.9
plugins/modules/subscriptions.py import-3.9
plugins/modules/team.py import-3.9
plugins/modules/token.py import-3.9
plugins/modules/user.py import-3.9
plugins/modules/workflow_approval.py import-3.9
plugins/modules/workflow_job_template.py import-3.9
plugins/modules/workflow_job_template_node.py import-3.9
plugins/modules/workflow_launch.py import-3.9
plugins/modules/workflow_node_wait.py import-3.9
plugins/inventory/controller.py import-3.10
plugins/lookup/controller_api.py import-3.10
plugins/module_utils/awxkit.py import-3.10
plugins/module_utils/controller_api.py import-3.10
plugins/modules/ad_hoc_command.py import-3.10
plugins/modules/ad_hoc_command_cancel.py import-3.10
plugins/modules/ad_hoc_command_wait.py import-3.10
plugins/modules/application.py import-3.10
plugins/modules/controller_meta.py import-3.10
plugins/modules/credential.py import-3.10
plugins/modules/credential_input_source.py import-3.10
plugins/modules/credential_type.py import-3.10
plugins/modules/execution_environment.py import-3.10
plugins/modules/export.py import-3.10
plugins/modules/group.py import-3.10
plugins/modules/host.py import-3.10
plugins/modules/import.py import-3.10
plugins/modules/instance.py import-3.10
plugins/modules/instance_group.py import-3.10
plugins/modules/inventory.py import-3.10
plugins/modules/inventory_source.py import-3.10
plugins/modules/inventory_source_update.py import-3.10
plugins/modules/job_cancel.py import-3.10
plugins/modules/job_launch.py import-3.10
plugins/modules/job_list.py import-3.10
plugins/modules/job_template.py import-3.10
plugins/modules/job_wait.py import-3.10
plugins/modules/label.py import-3.10
plugins/modules/license.py import-3.10
plugins/modules/notification_template.py import-3.10
plugins/modules/organization.py import-3.10
plugins/modules/project.py import-3.10
plugins/modules/project_update.py import-3.10
plugins/modules/role.py import-3.10
plugins/modules/schedule.py import-3.10
plugins/modules/settings.py import-3.10
plugins/modules/subscriptions.py import-3.10
plugins/modules/team.py import-3.10
plugins/modules/token.py import-3.10
plugins/modules/user.py import-3.10
plugins/modules/workflow_approval.py import-3.10
plugins/modules/workflow_job_template.py import-3.10
plugins/modules/workflow_job_template_node.py import-3.10
plugins/modules/workflow_launch.py import-3.10
plugins/modules/workflow_node_wait.py import-3.10

View File

@@ -0,0 +1 @@
plugins/modules/export.py validate-modules:nonexistent-parameter-documented # needs awxkit to construct argspec

View File

@@ -0,0 +1 @@
plugins/modules/export.py validate-modules:nonexistent-parameter-documented # needs awxkit to construct argspec

View File

@@ -157,6 +157,15 @@ For now, we pin to the old version, 3.4.1
* https://github.com/django/channels_redis/issues/332
* https://github.com/ansible/awx/issues/13313
### hiredis
The hiredis 2.1.0 release doesn't provide source distribution on PyPI which prevents users to build that python package from the
sources.
Downgrading to 2.0.0 (which provides source distribution) until the channels-redis issue is fixed or a newer hiredis version is
available on PyPi with source distribution.
* https://github.com/redis/hiredis-py/issues/138
## Library Notes
### pexpect

View File

@@ -26,6 +26,7 @@ djangorestframework==3.13.1
djangorestframework-yaml
filelock
GitPython
hiredis==2.0.0 # see UPGRADE BLOCKERs
irc
jinja2
JSON-log-formatter
@@ -42,7 +43,7 @@ python-dsv-sdk
python-tss-sdk==1.0.0
python-ldap
pyyaml
receptorctl==1.2.3
receptorctl==1.3.0
schedule==0.6.0
social-auth-core[openidconnect]==4.3.0 # see UPGRADE BLOCKERs
social-auth-app-django==5.0.0 # see UPGRADE BLOCKERs

View File

@@ -158,8 +158,10 @@ gitpython==3.1.29
# via -r /awx_devel/requirements/requirements.in
google-auth==2.14.1
# via kubernetes
hiredis==2.1.0
# via aioredis
hiredis==2.0.0
# via
# -r /awx_devel/requirements/requirements.in
# aioredis
hyperlink==21.0.0
# via
# autobahn
@@ -336,7 +338,7 @@ pyyaml==6.0
# djangorestframework-yaml
# kubernetes
# receptorctl
receptorctl==1.2.3
receptorctl==1.3.0
# via -r /awx_devel/requirements/requirements.in
redis==4.3.5
# via

View File

@@ -116,7 +116,7 @@ RUN dnf -y update && dnf install -y 'dnf-command(config-manager)' && \
python3-psycopg2 \
python3-setuptools \
rsync \
"rsyslog >= 8.1911.0" \
rsyslog-8.2102.0-106.el9 \
subversion \
sudo \
vim-minimal \