[ML] calendar eui conversion (#26741) (#27054)

* Create calendar list in react

* wip: create new_calendar page

* Update new calendar settings directory name

* Edit button action + update utils

* Adds ability to create new calendar

* Display calendar data on edit

* rename directory to settings/calendar

* Add scss files to calendar dir

* Create new group from form

* Adds event table and partial event modal.

* adds datepicker to modal

* Time range event functionality

* add import event functionality

* upate new event modal design

* Add error handling to list/edit

* calendarId validity check

* Create/delete permission. List/form style tweak

* Update calendarList to match filterList

* Add missing newlines in scss files

* Initial tests for calendar list

* Update classnames to meet guidelines

* ImportedEvents component + create utils

* remove unnecessary import

* rename calendars dir

* include past evens in import if checkbox checked

* code review updates

* move components into own directories

* update index.scss with dir name change

* skip irrelevant tests

* fix unsaved event deletion. rename scss file.

* Add modal tests

* Show calendarId and description as header on edit

* update snapshot for refactor

* update classnames to BEM guidelines

* Update snapshot for classname change
This commit is contained in:
Melissa Alvarez 2018-12-12 13:22:59 -06:00 committed by GitHub
parent 5b82c0692b
commit 00ab45a26d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 3179 additions and 6 deletions

View file

@ -1,3 +1,3 @@
@import 'settings';
@import 'filter_lists/index';
@import 'scheduled_events/index';
@import 'calendars/index';

View file

@ -0,0 +1,4 @@
.mlCalendarManagement {
background: $euiColorLightestShade;
min-height: 100vh;
}

View file

@ -0,0 +1,3 @@
@import 'calendars';
@import 'edit/index';
@import 'list/index';

View file

@ -0,0 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NewCalendar Renders new calendar form 1`] = `
<EuiPage
className="mlCalendarEditForm"
restrictWidth={false}
>
<EuiPageContent
className="mlCalendarEditForm__content"
horizontalPosition="center"
panelPaddingSize="l"
verticalPosition="center"
>
<CalendarForm
calendarId=""
description=""
eventsList={Array []}
groupIds={Array []}
isEdit={false}
isNewCalendarIdValid={true}
jobIds={Array []}
onCalendarIdChange={[Function]}
onCreate={[Function]}
onCreateGroupOption={[Function]}
onDescriptionChange={[Function]}
onEdit={[Function]}
onEventDelete={[Function]}
onGroupSelection={[Function]}
onJobSelection={[Function]}
saving={false}
selectedGroupOptions={Array []}
selectedJobOptions={Array []}
showImportModal={[Function]}
showNewEventModal={[Function]}
/>
</EuiPageContent>
</EuiPage>
`;

View file

@ -0,0 +1,8 @@
.mlCalendarEditForm {
.mlCalendarEditForm__content {
max-width: map-get($euiBreakpoints, 'xl');
width: 100%;
margin-top: $euiSize;
margin-bottom: $euiSize;
}
}

View file

@ -0,0 +1 @@
@import 'edit';

View file

@ -0,0 +1,142 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CalendarForm Renders calendar form 1`] = `
<EuiForm>
<React.Fragment>
<EuiFormRow
describedByIds={Array []}
error={
Array [
"Use lowercase alphanumerics (a-z and 0-9), hyphens or underscores;
must start and end with an alphanumeric character",
]
}
fullWidth={false}
hasEmptyLabelSpace={false}
isInvalid={true}
label="Calendar ID"
>
<EuiFieldText
compressed={false}
disabled={false}
fullWidth={false}
isLoading={false}
name="calendarId"
onChange={[MockFunction]}
value=""
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label="Description"
>
<EuiFieldText
compressed={false}
disabled={false}
fullWidth={false}
isLoading={false}
name="description"
onChange={[MockFunction]}
value=""
/>
</EuiFormRow>
</React.Fragment>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label="Jobs"
>
<EuiComboBox
compressed={false}
disabled={false}
fullWidth={false}
isClearable={true}
onChange={[MockFunction]}
options={Array []}
selectedOptions={Array []}
singleSelection={false}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label="Groups"
>
<EuiComboBox
compressed={false}
disabled={false}
fullWidth={false}
isClearable={true}
onChange={[MockFunction]}
onCreateOption={[MockFunction]}
options={Array []}
selectedOptions={Array []}
singleSelection={false}
/>
</EuiFormRow>
<EuiSpacer
size="xl"
/>
<EuiFormRow
describedByIds={Array []}
fullWidth={true}
hasEmptyLabelSpace={false}
label="Events"
>
<EventsTable
eventsList={Array []}
onDeleteClick={[MockFunction]}
showImportModal={[MockFunction]}
showNewEventModal={[MockFunction]}
showSearchBar={true}
/>
</EuiFormRow>
<EuiSpacer
size="l"
/>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexEnd"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButton
color="primary"
disabled={true}
fill={true}
iconSide="left"
onClick={[MockFunction]}
type="button"
>
Save
</EuiButton>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EuiButton
color="primary"
disabled={false}
fill={false}
href="undefined/app/ml#/settings/calendars_list"
iconSide="left"
type="button"
>
Cancel
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiForm>
`;

View file

@ -0,0 +1,191 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment } from 'react';
import { PropTypes } from 'prop-types';
import {
EuiButton,
EuiComboBox,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import chrome from 'ui/chrome';
import { EventsTable } from '../events_table/';
function EditHeader({
calendarId,
description
}) {
return (
<Fragment>
<EuiTitle>
<h1>Calendar {calendarId}</h1>
</EuiTitle>
<EuiText>
<p>
{description}
</p>
</EuiText>
<EuiSpacer size="l"/>
</Fragment>
);
}
export function CalendarForm({
calendarId,
description,
eventsList,
groupIds,
isEdit,
isNewCalendarIdValid,
jobIds,
onCalendarIdChange,
onCreate,
onCreateGroupOption,
onDescriptionChange,
onEdit,
onEventDelete,
onGroupSelection,
showImportModal,
onJobSelection,
saving,
selectedGroupOptions,
selectedJobOptions,
showNewEventModal
}) {
const msg = `Use lowercase alphanumerics (a-z and 0-9), hyphens or underscores;
must start and end with an alphanumeric character`;
const helpText = (isNewCalendarIdValid === true && !isEdit) ? msg : undefined;
const error = (isNewCalendarIdValid === false && !isEdit) ? [msg] : undefined;
return (
<EuiForm>
{!isEdit &&
<Fragment>
<EuiFormRow
label="Calendar ID"
helpText={helpText}
error={error}
isInvalid={!isNewCalendarIdValid}
>
<EuiFieldText
name="calendarId"
value={calendarId}
onChange={onCalendarIdChange}
disabled={isEdit === true || saving === true}
/>
</EuiFormRow>
<EuiFormRow
label="Description"
>
<EuiFieldText
name="description"
value={description}
onChange={onDescriptionChange}
disabled={isEdit === true || saving === true}
/>
</EuiFormRow>
</Fragment>
}
{isEdit &&
<EditHeader
calendarId={calendarId}
description={description}
/>}
<EuiFormRow
label="Jobs"
>
<EuiComboBox
options={jobIds}
selectedOptions={selectedJobOptions}
onChange={onJobSelection}
disabled={saving === true}
/>
</EuiFormRow>
<EuiFormRow
label="Groups"
>
<EuiComboBox
onCreateOption={onCreateGroupOption}
options={groupIds}
selectedOptions={selectedGroupOptions}
onChange={onGroupSelection}
disabled={saving === true}
/>
</EuiFormRow>
<EuiSpacer size="xl" />
<EuiFormRow
label="Events"
fullWidth
>
<EventsTable
eventsList={eventsList}
onDeleteClick={onEventDelete}
showImportModal={showImportModal}
showNewEventModal={showNewEventModal}
showSearchBar
/>
</EuiFormRow>
<EuiSpacer size="l" />
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton
fill
onClick={isEdit ? onEdit : onCreate}
disabled={saving || !isNewCalendarIdValid || calendarId === ''}
>
{saving ? 'Saving...' : 'Save'}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
disabled={saving}
href={`${chrome.getBasePath()}/app/ml#/settings/calendars_list`}
>
Cancel
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiForm>
);
}
CalendarForm.propTypes = {
calendarId: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
groupIds: PropTypes.array.isRequired,
isEdit: PropTypes.bool.isRequired,
isNewCalendarIdValid: PropTypes.bool.isRequired,
jobIds: PropTypes.array.isRequired,
onCalendarIdChange: PropTypes.func.isRequired,
onCreate: PropTypes.func.isRequired,
onCreateGroupOption: PropTypes.func.isRequired,
onDescriptionChange: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired,
onEventDelete: PropTypes.func.isRequired,
onGroupSelection: PropTypes.func.isRequired,
showImportModal: PropTypes.func.isRequired,
onJobSelection: PropTypes.func.isRequired,
saving: PropTypes.bool.isRequired,
selectedGroupOptions: PropTypes.array.isRequired,
selectedJobOptions: PropTypes.array.isRequired,
showNewEventModal: PropTypes.func.isRequired,
};

View file

@ -0,0 +1,70 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
jest.mock('ui/chrome', () => ({
getBasePath: jest.fn()
}));
import { shallow, mount } from 'enzyme';
import React from 'react';
import { CalendarForm } from './calendar_form';
const testProps = {
calendarId: '',
description: '',
eventsList: [],
groupIds: [],
isEdit: false,
isNewCalendarIdValid: false,
jobIds: [],
onCalendarIdChange: jest.fn(),
onCreate: jest.fn(),
onCreateGroupOption: jest.fn(),
onDescriptionChange: jest.fn(),
onEdit: jest.fn(),
onEventDelete: jest.fn(),
onGroupSelection: jest.fn(),
showImportModal: jest.fn(),
onJobSelection: jest.fn(),
saving: false,
selectedGroupOptions: [],
selectedJobOptions: [],
showNewEventModal: jest.fn()
};
describe('CalendarForm', () => {
test('Renders calendar form', () => {
const wrapper = shallow(
<CalendarForm {...testProps}/>
);
expect(wrapper).toMatchSnapshot();
});
test('CalendarId shown as title when editing', () => {
const editProps = {
...testProps,
isEdit: true,
calendarId: 'test-calendar',
description: 'test description',
};
const wrapper = mount(
<CalendarForm {...editProps} />
);
const calendarId = wrapper.find('EuiTitle');
expect(
calendarId.containsMatchingElement(
<h1>Calendar test-calendar</h1>
)
).toBeTruthy();
});
});

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { CalendarForm } from './calendar_form';

View file

@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import 'ngreact';
import React from 'react';
import ReactDOM from 'react-dom';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml', ['react']);
import { checkFullLicense } from '../../../license/check_license';
import { checkGetJobsPrivilege } from '../../../privilege/check_privilege';
import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes';
import { initPromise } from 'plugins/ml/util/promise';
import uiRoutes from 'ui/routes';
const template = `
<ml-nav-menu name="settings" />
<div class="mlCalendarManagement">
<ml-new-calendar />
</div>
`;
uiRoutes
.when('/settings/calendars_list/new_calendar', {
template,
resolve: {
CheckLicense: checkFullLicense,
privileges: checkGetJobsPrivilege,
checkMlNodesAvailable,
initPromise: initPromise(false)
}
})
.when('/settings/calendars_list/edit_calendar/:calendarId', {
template,
resolve: {
CheckLicense: checkFullLicense,
privileges: checkGetJobsPrivilege,
checkMlNodesAvailable,
initPromise: initPromise(false)
}
});
import { NewCalendar } from './new_calendar.js';
module.directive('mlNewCalendar', function ($route) {
return {
restrict: 'E',
replace: false,
scope: {},
link: function (scope, element) {
const props = {
calendarId: $route.current.params.calendarId
};
ReactDOM.render(
React.createElement(NewCalendar, props),
element[0]
);
}
};
});

View file

@ -0,0 +1,169 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EventsTable Renders events table with no search bar 1`] = `
<React.Fragment>
<EuiSpacer
size="m"
/>
<EuiInMemoryTable
columns={
Array [
Object {
"field": "description",
"name": "Description",
"sortable": true,
"truncateText": true,
},
Object {
"field": "start_time",
"name": "Start",
"render": [Function],
"sortable": true,
},
Object {
"field": "end_time",
"name": "End",
"render": [Function],
"sortable": true,
},
Object {
"field": "",
"name": "",
"render": [Function],
},
]
}
itemId="event_id"
items={
Array [
Object {
"calendar_id": "test-calendar",
"description": "test description",
"end_time": 1486657800000,
"event_id": "test-event-one",
"start_time": 1486656600000,
},
]
}
pagination={
Object {
"initialPageSize": 5,
"pageSizeOptions": Array [
5,
10,
],
}
}
responsive={true}
sorting={
Object {
"sort": Object {
"direction": "asc",
"field": "description",
},
}
}
/>
</React.Fragment>
`;
exports[`EventsTable Renders events table with search bar 1`] = `
<React.Fragment>
<EuiSpacer
size="m"
/>
<EuiInMemoryTable
columns={
Array [
Object {
"field": "description",
"name": "Description",
"sortable": true,
"truncateText": true,
},
Object {
"field": "start_time",
"name": "Start",
"render": [Function],
"sortable": true,
},
Object {
"field": "end_time",
"name": "End",
"render": [Function],
"sortable": true,
},
Object {
"field": "",
"name": "",
"render": [Function],
},
]
}
itemId="event_id"
items={
Array [
Object {
"calendar_id": "test-calendar",
"description": "test description",
"end_time": 1486657800000,
"event_id": "test-event-one",
"start_time": 1486656600000,
},
]
}
pagination={
Object {
"initialPageSize": 5,
"pageSizeOptions": Array [
5,
10,
],
}
}
responsive={true}
search={
Object {
"box": Object {
"incremental": true,
},
"filters": Array [],
"toolsRight": Array [
<EuiButton
color="primary"
data-testid="ml_new_event"
fill={false}
iconSide="left"
iconType="plusInCircle"
onClick={[MockFunction]}
size="s"
type="button"
>
New event
</EuiButton>,
<EuiButton
color="primary"
data-testid="ml_import_events"
fill={false}
iconSide="left"
iconType="importAction"
onClick={[MockFunction]}
size="s"
type="button"
>
Import events
</EuiButton>,
],
}
}
sorting={
Object {
"sort": Object {
"direction": "asc",
"field": "description",
},
}
}
/>
</React.Fragment>
`;

View file

@ -0,0 +1,145 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import PropTypes from 'prop-types';
import React, { Fragment } from 'react';
import moment from 'moment';
import {
EuiButton,
EuiButtonEmpty,
EuiInMemoryTable,
EuiSpacer,
} from '@elastic/eui';
export const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
function DeleteButton({ onClick }) {
return (
<Fragment>
<EuiButtonEmpty
size="xs"
color="danger"
onClick={onClick}
>
Delete
</EuiButtonEmpty>
</Fragment>
);
}
export function EventsTable({
eventsList,
onDeleteClick,
showSearchBar,
showImportModal,
showNewEventModal
}) {
const sorting = {
sort: {
field: 'description',
direction: 'asc',
}
};
const pagination = {
initialPageSize: 5,
pageSizeOptions: [5, 10]
};
const columns = [
{
field: 'description',
name: 'Description',
sortable: true,
truncateText: true
},
{
field: 'start_time',
name: 'Start',
sortable: true,
render: (timeMs) => {
const time = moment(timeMs);
return time.format(TIME_FORMAT);
}
},
{
field: 'end_time',
name: 'End',
sortable: true,
render: (timeMs) => {
const time = moment(timeMs);
return time.format(TIME_FORMAT);
}
},
{
field: '',
name: '',
render: (event) => (
<DeleteButton
data-testid="event_delete"
onClick={() => { onDeleteClick(event.event_id); }}
/>
)
},
];
const search = {
toolsRight: [(
<EuiButton
key="ml_new_event"
data-testid="ml_new_event"
size="s"
iconType="plusInCircle"
onClick={showNewEventModal}
>
New event
</EuiButton>),
(
<EuiButton
key="ml_import_event"
data-testid="ml_import_events"
size="s"
iconType="importAction"
onClick={showImportModal}
>
Import events
</EuiButton>
)],
box: {
incremental: true,
},
filters: []
};
return (
<Fragment>
<EuiSpacer size="m" />
<EuiInMemoryTable
items={eventsList}
itemId="event_id"
columns={columns}
pagination={pagination}
sorting={sorting}
search={showSearchBar ? search : undefined}
/>
</Fragment>
);
}
EventsTable.propTypes = {
eventsList: PropTypes.array.isRequired,
onDeleteClick: PropTypes.func.isRequired,
showImportModal: PropTypes.func,
showNewEventModal: PropTypes.func,
showSearchBar: PropTypes.bool,
};
EventsTable.defaultProps = {
showSearchBar: false,
};

View file

@ -0,0 +1,55 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
jest.mock('ui/chrome', () => ({
getBasePath: jest.fn()
}));
import { shallow } from 'enzyme';
import React from 'react';
import { EventsTable } from './events_table';
const testProps = {
eventsList: [{
calendar_id: 'test-calendar',
description: 'test description',
start_time: 1486656600000,
end_time: 1486657800000,
event_id: 'test-event-one'
}],
onDeleteClick: jest.fn(),
showSearchBar: false,
showImportModal: jest.fn(),
showNewEventModal: jest.fn()
};
describe('EventsTable', () => {
test('Renders events table with no search bar', () => {
const wrapper = shallow(
<EventsTable {...testProps}/>
);
expect(wrapper).toMatchSnapshot();
});
test('Renders events table with search bar', () => {
const showSearchBarProps = {
...testProps,
showSearchBar: true,
};
const wrapper = shallow(
<EventsTable {...showSearchBarProps} />
);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { EventsTable, TIME_FORMAT } from './events_table';

View file

@ -0,0 +1,82 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ImportModal Renders import modal 1`] = `
<React.Fragment>
<EuiModal
maxWidth={true}
onClose={[MockFunction]}
>
<EuiModalHeader>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="column"
gutterSize="none"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={true}
>
<EuiModalHeaderTitle>
Import events
</EuiModalHeaderTitle>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={true}
>
<p>
Import events from an ICS file.
</p>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalHeader>
<EuiModalBody>
<EuiFlexGroup
alignItems="stretch"
component="div"
direction="column"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={true}
>
<EuiFilePicker
compressed={true}
disabled={false}
initialPromptText="Select or drag and drop a file"
onChange={[Function]}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalBody>
<EuiModalFooter>
<EuiButton
color="primary"
disabled={true}
fill={true}
iconSide="left"
onClick={[Function]}
type="button"
>
Import
</EuiButton>
<EuiButtonEmpty
color="primary"
iconSide="left"
onClick={[MockFunction]}
type="button"
>
Cancel
</EuiButtonEmpty>
</EuiModalFooter>
</EuiModal>
</React.Fragment>
`;

View file

@ -0,0 +1,202 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, {
Component,
Fragment
} from 'react';
import { PropTypes } from 'prop-types';
import {
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiFilePicker,
EuiModal,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalBody,
EuiModalFooter,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { ImportedEvents } from '../imported_events';
import { readFile, parseICSFile, filterEvents } from './utils';
const MAX_FILE_SIZE_MB = 100;
export class ImportModal extends Component {
constructor(props) {
super(props);
this.state = {
includePastEvents: false,
allImportedEvents: [],
selectedEvents: [],
fileLoading: false,
fileLoaded: false,
errorMessage: null,
};
}
handleImport = async (loadedFile) => {
const incomingFile = loadedFile[0];
const errorMessage = 'Could not parse ICS file.';
let events = [];
if (incomingFile && incomingFile.size <= (MAX_FILE_SIZE_MB * 1000000)) {
this.setState({ fileLoading: true, fileLoaded: true });
try {
const parsedFile = await readFile(incomingFile);
events = parseICSFile(parsedFile.data);
this.setState({
allImportedEvents: events,
selectedEvents: filterEvents(events),
fileLoading: false,
errorMessage: null,
includePastEvents: false
});
} catch (error) {
console.log(errorMessage, error);
this.setState({ errorMessage, fileLoading: false });
}
} else if (incomingFile && incomingFile.size > (MAX_FILE_SIZE_MB * 1000000)) {
this.setState({ fileLoading: false, errorMessage });
} else {
this.setState({ fileLoading: false, errorMessage: null });
}
}
onEventDelete = (eventId) => {
this.setState(prevState => ({
allImportedEvents: prevState.allImportedEvents.filter(event => event.event_id !== eventId),
selectedEvents: prevState.selectedEvents.filter(event => event.event_id !== eventId),
}));
}
onCheckboxToggle = (e) => {
this.setState({
includePastEvents: e.target.checked,
});
};
handleEventsAdd = () => {
const { allImportedEvents, selectedEvents, includePastEvents } = this.state;
const eventsToImport = includePastEvents ? allImportedEvents : selectedEvents;
const events = eventsToImport.map((event) => ({
description: event.description,
start_time: event.start_time,
end_time: event.end_time,
event_id: event.event_id
}));
this.props.addImportedEvents(events);
}
renderCallout = () => (
<EuiCallOut color="danger">
<p>{this.state.errorMessage}</p>
</EuiCallOut>
);
render() {
const { closeImportModal } = this.props;
const {
fileLoading,
fileLoaded,
allImportedEvents,
selectedEvents,
errorMessage,
includePastEvents
} = this.state;
let showRecurringWarning = false;
let importedEvents;
if (includePastEvents) {
importedEvents = allImportedEvents;
} else {
importedEvents = selectedEvents;
}
if (importedEvents.find(e => e.asterisk) !== undefined) {
showRecurringWarning = true;
}
return (
<Fragment>
<EuiModal
onClose={closeImportModal}
maxWidth={true}
>
<EuiModalHeader>
<EuiFlexGroup
direction="column"
gutterSize="none"
>
<EuiFlexItem>
<EuiModalHeaderTitle >
Import events
</EuiModalHeaderTitle>
</EuiFlexItem>
<EuiFlexItem>
<p>Import events from an ICS file.</p>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalHeader>
<EuiModalBody>
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiFilePicker
compressed
initialPromptText="Select or drag and drop a file"
onChange={this.handleImport}
disabled={fileLoading}
/>
</EuiFlexItem>
{errorMessage !== null && this.renderCallout()}
{
allImportedEvents.length > 0 &&
<ImportedEvents
events={importedEvents}
showRecurringWarning={showRecurringWarning}
includePastEvents={includePastEvents}
onCheckboxToggle={this.onCheckboxToggle}
onEventDelete={this.onEventDelete}
/>
}
</EuiFlexGroup>
</EuiModalBody>
<EuiModalFooter>
<EuiButton
onClick={this.handleEventsAdd}
fill
disabled={fileLoaded === false || errorMessage !== null}
>
Import
</EuiButton>
<EuiButtonEmpty
onClick={closeImportModal}
>
Cancel
</EuiButtonEmpty>
</EuiModalFooter>
</EuiModal>
</Fragment>
);
}
}
ImportModal.propTypes = {
addImportedEvents: PropTypes.func.isRequired,
closeImportModal: PropTypes.func.isRequired,
};

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { shallow, mount } from 'enzyme';
import React from 'react';
import { ImportModal } from './import_modal';
const testProps = {
addImportedEvents: jest.fn(),
closeImportModal: jest.fn()
};
const events = [{
'description': 'Downtime feb 9 2017 10:10 to 10:30',
'start_time': 1486656600000,
'end_time': 1486657800000,
'calendar_id': 'farequote-calendar',
'event_id': 'Ee-YgGcBxHgQWEhCO_xj'
},
{
'description': 'New event!',
'start_time': 1544076000000,
'end_time': 1544162400000,
'calendar_id': 'this-is-a-new-calendar',
'event_id': 'ehWKhGcBqHkXuWNrIrSV'
}];
describe('ImportModal', () => {
test('Renders import modal', () => {
const wrapper = shallow(
<ImportModal {...testProps}/>
);
expect(wrapper).toMatchSnapshot();
});
test('Deletes selected event from event table', () => {
const wrapper = mount(
<ImportModal {...testProps} />
);
const testState = {
allImportedEvents: events,
selectedEvents: events,
};
const instance = wrapper.instance();
instance.setState(testState);
wrapper.update();
expect(wrapper.state('selectedEvents').length).toBe(2);
const deleteButton = wrapper.find('[data-testid="event_delete"]');
const button = deleteButton.find('EuiButtonEmpty').first();
button.simulate('click');
expect(wrapper.state('selectedEvents').length).toBe(1);
});
});

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { ImportModal } from './import_modal';

View file

@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
const icalendar = require('icalendar');
import moment from 'moment';
import { generateTempId } from '../utils';
function createEvents(ical) {
const events = ical.events();
const mlEvents = [];
events.forEach((e) => {
if (e.element === 'VEVENT') {
const description = e.properties.SUMMARY;
const start = e.properties.DTSTART;
const end = e.properties.DTEND;
const recurring = (e.properties.RRULE !== undefined);
if (description && start && end && description.length && start.length && end.length) {
// Temp reference to unsaved events to allow removal from table
const tempId = generateTempId();
mlEvents.push({
event_id: tempId,
description: description[0].value,
start_time: start[0].value.valueOf(),
end_time: end[0].value.valueOf(),
asterisk: recurring
});
}
}
});
return mlEvents;
}
export function filterEvents(events) {
const now = moment().valueOf();
return events.filter(e => e.start_time > now);
}
export function parseICSFile(data) {
const cal = icalendar.parse_calendar(data);
return createEvents(cal);
}
export function readFile(file) {
return new Promise((resolve, reject) => {
if (file && file.size) {
const reader = new FileReader();
reader.readAsText(file);
reader.onload = (() => {
return () => {
const data = reader.result;
if (data === '') {
reject();
} else {
resolve({ data });
}
};
})(file);
} else {
reject();
}
});
}

View file

@ -0,0 +1,60 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ImportedEvents Renders imported events 1`] = `
<React.Fragment>
<EuiSpacer
size="s"
/>
<EuiFlexItem
component="div"
grow={true}
>
<EuiText
grow={true}
size="m"
>
<h4>
Events to import:
1
</h4>
</EuiText>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
>
<EventsTable
eventsList={
Array [
Object {
"calendar_id": "test-calendar",
"description": "test description",
"end_time": 1486657800000,
"event_id": "test-event-one",
"start_time": 1486656600000,
},
]
}
onDeleteClick={[MockFunction]}
showSearchBar={false}
/>
</EuiFlexItem>
<EuiSpacer
size="m"
/>
<EuiFlexItem
component="div"
grow={false}
>
<EuiCheckbox
checked={false}
compressed={false}
disabled={false}
id="ml-include-past-events"
indeterminate={false}
label="Include past events"
onChange={[MockFunction]}
/>
</EuiFlexItem>
</React.Fragment>
`;

View file

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment } from 'react';
import { PropTypes } from 'prop-types';
import {
EuiCheckbox,
EuiFlexItem,
EuiText,
EuiSpacer
} from '@elastic/eui';
import { EventsTable } from '../events_table/';
export function ImportedEvents({
events,
showRecurringWarning,
includePastEvents,
onCheckboxToggle,
onEventDelete,
}) {
return (
<Fragment>
<EuiSpacer size="s"/>
<EuiFlexItem>
<EuiText>
<h4>Events to import: {events.length}</h4>
{showRecurringWarning && (
<EuiText color="danger">
<p>Recurring events not supported. Only the first event will be imported.</p>
</EuiText>)
}
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EventsTable
eventsList={events}
onDeleteClick={onEventDelete}
/>
</EuiFlexItem>
<EuiSpacer size="m" />
<EuiFlexItem grow={false}>
<EuiCheckbox
id="ml-include-past-events"
label="Include past events"
checked={includePastEvents}
onChange={onCheckboxToggle}
/>
</EuiFlexItem>
</Fragment>
);
}
ImportedEvents.propTypes = {
events: PropTypes.array.isRequired,
showRecurringWarning: PropTypes.bool.isRequired,
includePastEvents: PropTypes.bool.isRequired,
onCheckboxToggle: PropTypes.func.isRequired,
onEventDelete: PropTypes.func.isRequired,
};

View file

@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
jest.mock('ui/chrome', () => ({
getBasePath: jest.fn()
}));
import { shallow } from 'enzyme';
import React from 'react';
import { ImportedEvents } from './imported_events';
const testProps = {
events: [{
calendar_id: 'test-calendar',
description: 'test description',
start_time: 1486656600000,
end_time: 1486657800000,
event_id: 'test-event-one'
}],
showRecurringWarning: false,
includePastEvents: false,
onCheckboxToggle: jest.fn(),
onEventDelete: jest.fn(),
};
describe('ImportedEvents', () => {
test('Renders imported events', () => {
const wrapper = shallow(
<ImportedEvents {...testProps} />
);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { ImportedEvents } from './imported_events';

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import './directive';

View file

@ -0,0 +1,317 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, {
Component
} from 'react';
import { PropTypes } from 'prop-types';
import {
EuiPage,
EuiPageContent,
EuiOverlayMask,
} from '@elastic/eui';
import chrome from 'ui/chrome';
import { getCalendarSettingsData, validateCalendarId } from './utils';
import { CalendarForm } from './calendar_form/';
import { NewEventModal } from './new_event_modal/';
import { ImportModal } from './import_modal';
import { ml } from '../../../services/ml_api_service';
import { toastNotifications } from 'ui/notify';
export class NewCalendar extends Component {
constructor(props) {
super(props);
this.state = {
isNewEventModalVisible: false,
isImportModalVisible: false,
isNewCalendarIdValid: null,
loading: true,
jobIds: [],
jobIdOptions: [],
groupIds: [],
groupIdOptions: [],
calendars: [],
formCalendarId: '',
description: '',
selectedJobOptions: [],
selectedGroupOptions: [],
events: [],
saving: false,
selectedCalendar: undefined,
};
}
componentDidMount() {
this.formSetup();
}
async formSetup() {
try {
const { jobIds, groupIds, calendars } = await getCalendarSettingsData();
const jobIdOptions = jobIds.map((jobId) => ({ label: jobId }));
const groupIdOptions = groupIds.map((groupId) => ({ label: groupId }));
const selectedJobOptions = [];
const selectedGroupOptions = [];
let eventsList = [];
let selectedCalendar;
let formCalendarId = '';
// Editing existing calendar.
if (this.props.calendarId !== undefined) {
selectedCalendar = calendars.find((cal) => cal.calendar_id === this.props.calendarId);
if (selectedCalendar) {
formCalendarId = selectedCalendar.calendar_id;
eventsList = selectedCalendar.events;
selectedCalendar.job_ids.forEach(id => {
if (jobIds.find((jobId) => jobId === id)) {
selectedJobOptions.push({ label: id });
} else if (groupIds.find((groupId) => groupId === id)) {
selectedGroupOptions.push({ label: id });
}
});
}
}
this.setState({
events: eventsList,
formCalendarId,
jobIds,
jobIdOptions,
groupIds,
groupIdOptions,
calendars,
loading: false,
selectedJobOptions,
selectedGroupOptions,
selectedCalendar
});
} catch (error) {
console.log(error);
this.setState({ loading: false });
toastNotifications.addDanger('An error occurred loading calendar form data. Try refreshing the page.');
}
}
onCreate = async () => {
const calendar = this.setUpCalendarForApi();
this.setState({ saving: true });
try {
await ml.addCalendar(calendar);
window.location = `${chrome.getBasePath()}/app/ml#/settings/calendars_list`;
} catch (error) {
console.log('Error saving calendar', error);
this.setState({ saving: false });
toastNotifications.addDanger(`An error occurred creating calendar ${calendar.calendarId}`);
}
}
onEdit = async () => {
const calendar = this.setUpCalendarForApi();
this.setState({ saving: true });
try {
await ml.updateCalendar(calendar);
window.location = `${chrome.getBasePath()}/app/ml#/settings/calendars_list`;
} catch (error) {
console.log('Error saving calendar', error);
this.setState({ saving: false });
toastNotifications.addDanger(`An error occurred saving calendar ${calendar.calendarId}. Try refreshing the page.`);
}
}
setUpCalendarForApi = () => {
const {
formCalendarId,
description,
events,
selectedGroupOptions,
selectedJobOptions,
} = this.state;
const jobIds = selectedJobOptions.map((option) => option.label);
const groupIds = selectedGroupOptions.map((option) => option.label);
// Reduce events to fields expected by api
const eventsToSave = events.map((event) => ({
description: event.description,
start_time: event.start_time,
end_time: event.end_time
}));
// set up calendar
const calendar = {
calendarId: formCalendarId,
description,
events: eventsToSave,
job_ids: [...jobIds, ...groupIds]
};
return calendar;
}
onCreateGroupOption = (newGroup) => {
const newOption = {
label: newGroup,
};
// Select the option.
this.setState(prevState => ({
selectedGroupOptions: prevState.selectedGroupOptions.concat(newOption),
}));
};
onJobSelection = (selectedJobOptions) => {
this.setState({
selectedJobOptions,
});
};
onGroupSelection = (selectedGroupOptions) => {
this.setState({
selectedGroupOptions,
});
};
onCalendarIdChange = (e) => {
const isValid = validateCalendarId(e.target.value);
this.setState({
formCalendarId: e.target.value,
isNewCalendarIdValid: isValid
});
};
onDescriptionChange = (e) => {
this.setState({
description: e.target.value,
});
};
showImportModal = () => {
this.setState(prevState => ({
isImportModalVisible: !prevState.isImportModalVisible,
}));
}
closeImportModal = () => {
this.setState({
isImportModalVisible: false,
});
}
onEventDelete = (eventId) => {
this.setState(prevState => ({
events: prevState.events.filter(event => event.event_id !== eventId)
}));
}
closeNewEventModal = () => {
this.setState({ isNewEventModalVisible: false });
}
showNewEventModal = () => {
this.setState({ isNewEventModalVisible: true });
}
addEvent = (event) => {
this.setState(prevState => ({
events: [...prevState.events, event],
isNewEventModalVisible: false
}));
}
addImportedEvents = (events) => {
this.setState(prevState => ({
events: [...prevState.events, ...events],
isImportModalVisible: false
}));
}
render() {
const {
events,
isNewEventModalVisible,
isImportModalVisible,
isNewCalendarIdValid,
formCalendarId,
description,
groupIdOptions,
jobIdOptions,
saving,
selectedCalendar,
selectedJobOptions,
selectedGroupOptions
} = this.state;
let modal = '';
if (isNewEventModalVisible) {
modal = (
<EuiOverlayMask>
<NewEventModal
addEvent={this.addEvent}
closeModal={this.closeNewEventModal}
/>
</EuiOverlayMask>
);
} else if (isImportModalVisible) {
modal = (
<EuiOverlayMask>
<ImportModal
addImportedEvents={this.addImportedEvents}
closeImportModal={this.closeImportModal}
/>
</EuiOverlayMask>
);
}
return (
<EuiPage className="mlCalendarEditForm">
<EuiPageContent
className="mlCalendarEditForm__content"
verticalPosition="center"
horizontalPosition="center"
>
<CalendarForm
calendarId={selectedCalendar ? selectedCalendar.calendar_id : formCalendarId}
description={selectedCalendar ? selectedCalendar.description : description}
eventsList={events}
groupIds={groupIdOptions}
isEdit={selectedCalendar !== undefined}
isNewCalendarIdValid={(selectedCalendar || isNewCalendarIdValid === null) ? true : isNewCalendarIdValid}
jobIds={jobIdOptions}
onCalendarIdChange={this.onCalendarIdChange}
onCreate={this.onCreate}
onDescriptionChange={this.onDescriptionChange}
onEdit={this.onEdit}
onEventDelete={this.onEventDelete}
onGroupSelection={this.onGroupSelection}
showImportModal={this.showImportModal}
onJobSelection={this.onJobSelection}
saving={saving}
selectedGroupOptions={selectedGroupOptions}
selectedJobOptions={selectedJobOptions}
onCreateGroupOption={this.onCreateGroupOption}
showNewEventModal={this.showNewEventModal}
/>
</EuiPageContent>
{modal}
</EuiPage>
);
}
}
NewCalendar.propTypes = {
calendarId: PropTypes.string,
};

View file

@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
jest.mock('../../../privilege/check_privilege', () => ({
checkPermission: () => true
}));
jest.mock('../../../license/check_license', () => ({
hasLicenseExpired: () => false
}));
jest.mock('../../../privilege/get_privileges', () => ({
getPrivileges: () => {}
}));
jest.mock('../../../ml_nodes_check/check_ml_nodes', () => ({
mlNodesAvailable: () => true
}));
jest.mock('ui/chrome', () => ({
getBasePath: jest.fn()
}));
jest.mock('../../../services/ml_api_service', () => ({
ml: {
calendars: () => {
return Promise.resolve([]);
},
jobs: {
jobsSummary: () => {
return Promise.resolve([]);
},
groups: () => {
return Promise.resolve([]);
},
},
}
}));
jest.mock('./utils', () => ({
getCalendarSettingsData: jest.fn().mockImplementation(() => new Promise((resolve) => {
resolve({
jobIds: ['test-job-one', 'test-job-2'],
groupIds: ['test-group-one', 'test-group-two'],
calendars: []
});
})),
}));
import { shallow, mount } from 'enzyme';
import React from 'react';
import { NewCalendar } from './new_calendar';
describe('NewCalendar', () => {
test('Renders new calendar form', () => {
const wrapper = shallow(
<NewCalendar />
);
expect(wrapper).toMatchSnapshot();
});
test('Import modal shown on Import Events button click', () => {
const wrapper = mount(
<NewCalendar />
);
const importButton = wrapper.find('[data-testid="ml_import_events"]');
const button = importButton.find('EuiButton');
button.simulate('click');
expect(wrapper.state('isImportModalVisible')).toBe(true);
});
test('New event modal shown on New event button click', () => {
const wrapper = mount(
<NewCalendar />
);
const importButton = wrapper.find('[data-testid="ml_new_event"]');
const button = importButton.find('EuiButton');
button.simulate('click');
expect(wrapper.state('isNewEventModalVisible')).toBe(true);
});
});

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { NewEventModal } from './new_event_modal';

View file

@ -0,0 +1,289 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, {
Component,
Fragment
} from 'react';
import { PropTypes } from 'prop-types';
import {
EuiButton,
EuiButtonEmpty,
EuiDatePicker,
EuiDatePickerRange,
EuiFieldText,
EuiForm,
EuiFormRow,
EuiModal,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalBody,
EuiModalFooter,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import moment from 'moment';
import { TIME_FORMAT } from '../events_table/';
import { generateTempId } from '../utils';
const VALID_DATE_STRING_LENGTH = 19;
export class NewEventModal extends Component {
constructor(props) {
super(props);
const startDate = moment().startOf('day');
const endDate = moment().startOf('day').add(1, 'days');
this.state = {
startDate,
endDate,
description: '',
startDateString: startDate.format(TIME_FORMAT),
endDateString: endDate.format(TIME_FORMAT)
};
}
onDescriptionChange = (e) => {
this.setState({
description: e.target.value,
});
};
handleAddEvent = () => {
const { description, startDate, endDate } = this.state;
// Temp reference to unsaved events to allow removal from table
const tempId = generateTempId();
const event = {
description,
start_time: startDate.valueOf(),
end_time: endDate.valueOf(),
event_id: tempId
};
this.props.addEvent(event);
}
handleChangeStart = (date) => {
let start = null;
let end = this.state.endDate;
const startMoment = moment(date);
const endMoment = moment(date);
start = startMoment.startOf('day');
if (start > end) {
end = endMoment.startOf('day').add(1, 'days');
}
this.setState({
startDate: start,
endDate: end,
startDateString: start.format(TIME_FORMAT),
endDateString: end.format(TIME_FORMAT)
});
}
handleChangeEnd = (date) => {
let start = this.state.startDate;
let end = null;
const startMoment = moment(date);
const endMoment = moment(date);
end = endMoment.startOf('day');
if (start > end) {
start = startMoment.startOf('day').subtract(1, 'days');
}
this.setState({
startDate: start,
endDate: end,
startDateString: start.format(TIME_FORMAT),
endDateString: end.format(TIME_FORMAT)
});
}
handleTimeStartChange = (event) => {
const dateString = event.target.value;
let isValidDate = false;
if (dateString.length === VALID_DATE_STRING_LENGTH) {
isValidDate = moment(dateString).isValid(TIME_FORMAT, true);
} else {
this.setState({
startDateString: dateString,
});
}
if (isValidDate) {
this.setState({
startDateString: dateString,
startDate: moment(dateString)
});
}
}
handleTimeEndChange = (event) => {
const dateString = event.target.value;
let isValidDate = false;
if (dateString.length === VALID_DATE_STRING_LENGTH) {
isValidDate = moment(dateString).isValid(TIME_FORMAT, true);
} else {
this.setState({
endDateString: dateString,
});
}
if (isValidDate) {
this.setState({
endDateString: dateString,
endDate: moment(dateString)
});
}
}
renderRangedDatePicker = () => {
const {
startDate,
endDate,
startDateString,
endDateString,
} = this.state;
const timeInputs = (
<Fragment>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow label="From:" helpText={TIME_FORMAT}>
<EuiFieldText
name="startTime"
onChange={this.handleTimeStartChange}
placeholder={TIME_FORMAT}
value={startDateString}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow label="To:" helpText={TIME_FORMAT}>
<EuiFieldText
name="endTime"
onChange={this.handleTimeEndChange}
placeholder={TIME_FORMAT}
value={endDateString}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
);
return (
<Fragment>
<EuiSpacer size="s" />
{timeInputs}
<EuiSpacer size="s" />
<EuiFormRow fullWidth>
<EuiDatePickerRange
fullWidth
iconType={false}
startDateControl={
<EuiDatePicker
fullWidth
inline
selected={startDate}
onChange={this.handleChangeStart}
startDate={startDate}
endDate={endDate}
isInvalid={startDate > endDate}
aria-label="Start date"
timeFormat={TIME_FORMAT}
dateFormat={TIME_FORMAT}
/>
}
endDateControl={
<EuiDatePicker
fullWidth
inline
selected={endDate}
onChange={this.handleChangeEnd}
startDate={startDate}
endDate={endDate}
isInvalid={startDate > endDate}
aria-label="End date"
timeFormat={TIME_FORMAT}
dateFormat={TIME_FORMAT}
/>
}
/>
</EuiFormRow>
</Fragment>
);
}
render() {
const { closeModal } = this.props;
const { description } = this.state;
return (
<Fragment>
<EuiModal
onClose={closeModal}
initialFocus="[name=description]"
maxWidth={false}
>
<EuiModalHeader>
<EuiModalHeaderTitle >
Create new event
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiForm>
<EuiFormRow
label="Description"
fullWidth
>
<EuiFieldText
name="description"
onChange={this.onDescriptionChange}
isInvalid={!description}
fullWidth
/>
</EuiFormRow>
{this.renderRangedDatePicker()}
</EuiForm>
</EuiModalBody>
<EuiModalFooter>
<EuiButton
onClick={this.handleAddEvent}
fill
disabled={!description}
>
Add
</EuiButton>
<EuiButtonEmpty
onClick={closeModal}
>
Cancel
</EuiButtonEmpty>
</EuiModalFooter>
</EuiModal>
</Fragment>
);
}
}
NewEventModal.propTypes = {
closeModal: PropTypes.func.isRequired,
addEvent: PropTypes.func.isRequired,
};

View file

@ -0,0 +1,76 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { shallow } from 'enzyme';
import React from 'react';
import { NewEventModal } from './new_event_modal';
import moment from 'moment';
const testProps = {
closeModal: jest.fn(),
addEvent: jest.fn(),
};
const stateTimestamps = {
startDate: 1544508000000,
endDate: 1544594400000
};
describe('NewEventModal', () => {
it('Add button disabled if description empty', () => {
const wrapper = shallow(
<NewEventModal {...testProps} />
);
const addButton = wrapper.find('EuiButton').first();
expect(addButton.prop('disabled')).toBe(true);
});
it('if endDate is less than startDate should set startDate one day before endDate', () => {
const wrapper = shallow(<NewEventModal {...testProps} />);
const instance = wrapper.instance();
instance.setState({
startDate: moment(stateTimestamps.startDate),
endDate: moment(stateTimestamps.endDate)
});
// set to Dec 11, 2018 and Dec 12, 2018
const startMoment = moment(stateTimestamps.startDate);
const endMoment = moment(stateTimestamps.endDate);
// make startMoment greater than current end Date
startMoment.startOf('day').add(3, 'days');
// trigger handleChangeStart directly with startMoment
instance.handleChangeStart(startMoment);
// add 3 days to endMoment as it will be adjusted to be one day after startDate
const expected = endMoment.startOf('day').add(3, 'days').format();
expect(wrapper.state('endDate').format()).toBe(expected);
});
it('if startDate is greater than endDate should set endDate one day after startDate', () => {
const wrapper = shallow(<NewEventModal {...testProps} />);
const instance = wrapper.instance();
instance.setState({
startDate: moment(stateTimestamps.startDate),
endDate: moment(stateTimestamps.endDate)
});
// set to Dec 11, 2018 and Dec 12, 2018
const startMoment = moment(stateTimestamps.startDate);
const endMoment = moment(stateTimestamps.endDate);
// make endMoment less than current start Date
endMoment.startOf('day').subtract(3, 'days');
// trigger handleChangeStart directly with endMoment
instance.handleChangeStart(endMoment);
// subtract 3 days from startDate as it will be adjusted to be one day before endDate
const expected = startMoment.startOf('day').subtract(2, 'days').format();
expect(wrapper.state('startDate').format()).toBe(expected);
});
});

View file

@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ml } from '../../../services/ml_api_service';
import { isJobIdValid } from '../../../../common/util/job_utils';
function getJobIds() {
return new Promise((resolve, reject) => {
ml.jobs.jobsSummary()
.then((resp) => {
resolve(resp.map((job) => job.id));
})
.catch((err) => {
const errorMessage = `Error fetching job summaries: ${err}`;
console.log(errorMessage);
reject(errorMessage);
});
});
}
function getGroupIds() {
return new Promise((resolve, reject) => {
ml.jobs.groups()
.then((resp) => {
resolve(resp.map((group) => group.id));
})
.catch((err) => {
const errorMessage = `Error loading groups: ${err}`;
console.log(errorMessage);
reject(errorMessage);
});
});
}
function getCalendars() {
return new Promise((resolve, reject) => {
ml.calendars()
.then((resp) => {
resolve(resp);
})
.catch((err) => {
const errorMessage = `Error loading calendars: ${err}`;
console.log(errorMessage);
reject(errorMessage);
});
});
}
export function getCalendarSettingsData() {
return new Promise(async (resolve, reject) => {
try {
const data = await Promise.all([getJobIds(), getGroupIds(), getCalendars()]);
const formattedData = {
jobIds: data[0],
groupIds: data[1],
calendars: data[2]
};
resolve(formattedData);
} catch (error) {
console.log(error);
reject(error);
}
});
}
export function validateCalendarId(calendarId) {
let valid = true;
if (calendarId === '' || calendarId === undefined) {
valid = false;
} else if (isJobIdValid(calendarId) === false) {
valid = false;
}
return valid;
}
export function generateTempId() {
return Math.random().toString(36).substr(2, 9);
}

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import './list';
import './edit';

View file

@ -0,0 +1,65 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CalendarsList Renders calendar list with calendars 1`] = `
<EuiPage
className="mlCalendarList"
restrictWidth={false}
>
<EuiPageContent
className="mlCalendarList__content"
horizontalPosition="center"
panelPaddingSize="l"
verticalPosition="center"
>
<CalendarsListTable
calendarsList={
Array [
Object {
"calendar_id": "farequote-calendar",
"description": "test ",
"events": Array [
Object {
"calendar_id": "farequote-calendar",
"description": "Downtime feb 9 2017 10:10 to 10:30",
"end_time": 1486657800000,
"event_id": "Ee-YgGcBxHgQWEhCO_xj",
"start_time": 1486656600000,
},
],
"events_length": "1 event",
"job_ids": Array [
"farequote",
],
"job_ids_string": "farequote",
},
Object {
"calendar_id": "this-is-a-new-calendar",
"description": "new calendar",
"events": Array [
Object {
"calendar_id": "this-is-a-new-calendar",
"description": "New event!",
"end_time": 1544162400000,
"event_id": "ehWKhGcBqHkXuWNrIrSV",
"start_time": 1544076000000,
},
],
"events_length": "1 event",
"job_ids": Array [
"test",
],
"job_ids_string": "test",
},
]
}
canCreateCalendar={true}
canDeleteCalendar={true}
itemsSelected={false}
loading={false}
mlNodesAvailable={true}
onDeleteClick={[Function]}
setSelectedCalendarList={[Function]}
/>
</EuiPageContent>
</EuiPage>
`;

View file

@ -0,0 +1 @@
@import 'list';

View file

@ -0,0 +1,9 @@
.mlCalendarList {
.mlCalendarList__content {
max-width: map-get($euiBreakpoints, 'xl');
margin-top: $euiSize;
margin-bottom: $euiSize;
}
}

View file

@ -0,0 +1,149 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, {
Component
} from 'react';
import {
EuiConfirmModal,
EuiOverlayMask,
EuiPage,
EuiPageContent,
EUI_MODAL_CONFIRM_BUTTON,
} from '@elastic/eui';
import { CalendarsListTable } from './table/';
import { ml } from '../../../services/ml_api_service';
import { toastNotifications } from 'ui/notify';
import { checkPermission } from '../../../privilege/check_privilege';
import { mlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes';
import { deleteCalendars } from './delete_calendars';
export class CalendarsList extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
calendars: [],
isDestroyModalVisible: false,
calendarId: null,
selectedForDeletion: [],
canCreateCalendar: checkPermission('canCreateCalendar'),
canDeleteCalendar: checkPermission('canDeleteCalendar'),
nodesAvailable: mlNodesAvailable()
};
}
loadCalendars = async () => {
try {
const calendars = await ml.calendars();
this.setState({
calendars,
loading: false,
isDestroyModalVisible: false,
});
} catch (error) {
console.log(error);
this.setState({ loading: false });
toastNotifications.addDanger('An error occurred loading the list of calendars.');
}
}
closeDestroyModal = () => {
this.setState({ isDestroyModalVisible: false, calendarId: null });
}
showDestroyModal = () => {
this.setState({ isDestroyModalVisible: true });
}
setSelectedCalendarList = (selectedCalendars) => {
this.setState({ selectedForDeletion: selectedCalendars });
}
deleteCalendars = () => {
const { selectedForDeletion } = this.state;
this.closeDestroyModal();
deleteCalendars(selectedForDeletion, this.loadCalendars);
}
addRequiredFieldsToList = (calendarsList = []) => {
for (let i = 0; i < calendarsList.length; i++) {
const eventLength = calendarsList[i].events.length;
calendarsList[i].job_ids_string = calendarsList[i].job_ids.join(', ');
calendarsList[i].events_length = `${eventLength} ${eventLength === 1 ? 'event' : 'events'}`;
}
return calendarsList;
}
componentDidMount() {
this.loadCalendars();
}
render() {
const {
calendars,
selectedForDeletion,
loading,
canCreateCalendar,
canDeleteCalendar,
nodesAvailable
} = this.state;
let destroyModal = '';
if (this.state.isDestroyModalVisible) {
destroyModal = (
<EuiOverlayMask>
<EuiConfirmModal
title="Delete calendar"
onCancel={this.closeDestroyModal}
onConfirm={this.deleteCalendars}
cancelButtonText="Cancel"
confirmButtonText="Delete"
buttonColor="danger"
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
>
<p>
{
`Delete ${selectedForDeletion.length === 1 ? 'this' : 'these'}
calendar${selectedForDeletion.length === 1 ? '' : 's'}?
${selectedForDeletion.map((c) => c.calendar_id).join(', ')}`
}
</p>
</EuiConfirmModal>
</EuiOverlayMask>
);
}
return (
<EuiPage className="mlCalendarList">
<EuiPageContent
className="mlCalendarList__content"
verticalPosition="center"
horizontalPosition="center"
>
<CalendarsListTable
loading={loading}
calendarsList={this.addRequiredFieldsToList(calendars)}
onDeleteClick={this.showDestroyModal}
canCreateCalendar={canCreateCalendar}
canDeleteCalendar={canDeleteCalendar}
mlNodesAvailable={nodesAvailable}
setSelectedCalendarList={this.setSelectedCalendarList}
itemsSelected={selectedForDeletion.length > 0}
/>
</EuiPageContent>
{destroyModal}
</EuiPage>
);
}
}

View file

@ -0,0 +1,110 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
jest.mock('../../../privilege/check_privilege', () => ({
checkPermission: () => true
}));
jest.mock('../../../license/check_license', () => ({
hasLicenseExpired: () => false
}));
jest.mock('../../../privilege/get_privileges', () => ({
getPrivileges: () => {}
}));
jest.mock('../../../ml_nodes_check/check_ml_nodes', () => ({
mlNodesAvailable: () => true
}));
jest.mock('ui/chrome', () => ({
getBasePath: jest.fn()
}));
jest.mock('../../../services/ml_api_service', () => ({
ml: {
calendars: () => {
return Promise.resolve([]);
},
delete: jest.fn(),
}
}));
import { shallow, mount } from 'enzyme';
import React from 'react';
import { ml } from '../../../services/ml_api_service';
import { CalendarsList } from './calendars_list';
const testingState = {
loading: false,
calendars: [
{
'calendar_id': 'farequote-calendar',
'job_ids': ['farequote'],
'description': 'test ',
'events': [{
'description': 'Downtime feb 9 2017 10:10 to 10:30',
'start_time': 1486656600000,
'end_time': 1486657800000,
'calendar_id': 'farequote-calendar',
'event_id': 'Ee-YgGcBxHgQWEhCO_xj'
}]
},
{
'calendar_id': 'this-is-a-new-calendar',
'job_ids': ['test'],
'description': 'new calendar',
'events': [{
'description': 'New event!',
'start_time': 1544076000000,
'end_time': 1544162400000,
'calendar_id': 'this-is-a-new-calendar',
'event_id': 'ehWKhGcBqHkXuWNrIrSV'
}]
}],
isDestroyModalVisible: false,
calendarId: null,
selectedForDeletion: [],
canCreateCalendar: true,
canDeleteCalendar: true,
nodesAvailable: true,
};
describe('CalendarsList', () => {
test('loads calendars on mount', () => {
ml.calendars = jest.fn();
shallow(
<CalendarsList />
);
expect(ml.calendars).toHaveBeenCalled();
});
test('Renders calendar list with calendars', () => {
const wrapper = shallow(
<CalendarsList />
);
wrapper.instance().setState(testingState);
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
test('Sets selected calendars list on checkbox change', () => {
const wrapper = mount(
<CalendarsList />
);
const instance = wrapper.instance();
const spy = jest.spyOn(instance, 'setSelectedCalendarList');
instance.setState(testingState);
wrapper.update();
const checkbox = wrapper.find('input[type="checkbox"]').first();
checkbox.simulate('change');
expect(spy).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { toastNotifications } from 'ui/notify';
import { ml } from '../../../services/ml_api_service';
export async function deleteCalendars(calendarsToDelete, callback) {
if (calendarsToDelete === undefined || calendarsToDelete.length === 0) {
return;
}
// Delete each of the specified calendars in turn, waiting for each response
// before deleting the next to minimize load on the cluster.
const messageId = `${(calendarsToDelete.length > 1) ?
`${calendarsToDelete.length} calendars` : calendarsToDelete[0].calendar_id}`;
toastNotifications.add(`Deleting ${messageId}`);
for(const calendar of calendarsToDelete) {
const calendarId = calendar.calendar_id;
try {
await ml.deleteCalendar({ calendarId });
} catch (error) {
console.log('Error deleting calendar:', error);
let errorMessage = `An error occurred deleting calendar ${calendar.calendar_id}`;
if (error.message) {
errorMessage += ` : ${error.message}`;
}
toastNotifications.addDanger(errorMessage);
}
}
toastNotifications.addSuccess(`${messageId} deleted`);
callback();
}

View file

@ -0,0 +1,55 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import 'ngreact';
import React from 'react';
import ReactDOM from 'react-dom';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml', ['react']);
import { checkFullLicense } from '../../../license/check_license';
import { checkGetJobsPrivilege } from '../../../privilege/check_privilege';
import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes';
import { initPromise } from '../../../util/promise';
import uiRoutes from 'ui/routes';
const template = `
<ml-nav-menu name="settings" />
<div class="mlCalendarManagement">
<ml-calendars-list />
</div>
`;
uiRoutes
.when('/settings/calendars_list', {
template,
resolve: {
CheckLicense: checkFullLicense,
privileges: checkGetJobsPrivilege,
mlNodeCount: getMlNodeCount,
initPromise: initPromise(false)
}
});
import { CalendarsList } from './calendars_list';
module.directive('mlCalendarsList', function () {
return {
restrict: 'E',
replace: false,
scope: {},
link: function (scope, element) {
ReactDOM.render(
React.createElement(CalendarsList),
element[0]
);
}
};
});

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import './directive';

View file

@ -0,0 +1,110 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CalendarsListTable renders the table with all calendars 1`] = `
<React.Fragment>
<EuiInMemoryTable
columns={
Array [
Object {
"field": "calendar_id",
"name": "ID",
"render": [Function],
"sortable": true,
"truncateText": true,
},
Object {
"field": "job_ids_string",
"name": "Jobs",
"sortable": true,
"truncateText": true,
},
Object {
"field": "events_length",
"name": "Events",
"sortable": true,
},
]
}
isSelectable={true}
itemId="calendar_id"
items={
Array [
Object {
"calendar_id": "farequote-calendar",
"description": "test ",
"events": Array [],
"job_ids": Array [
"farequote",
],
},
Object {
"calendar_id": "this-is-a-new-calendar",
"description": "new calendar",
"events": Array [],
"job_ids": Array [
"test",
],
},
]
}
loading={false}
pagination={
Object {
"initialPageSize": 20,
"pageSizeOptions": Array [
10,
20,
],
}
}
responsive={true}
search={
Object {
"box": Object {
"incremental": true,
},
"filters": Array [],
"toolsRight": Array [
<EuiButton
color="primary"
data-testid="new_calendar_button"
fill={false}
href="undefined/app/ml#/settings/calendars_list/new_calendar"
iconSide="left"
isDisabled={false}
size="s"
type="button"
>
New
</EuiButton>,
<EuiButton
color="danger"
fill={false}
iconSide="left"
iconType="trash"
isDisabled={true}
onClick={[Function]}
size="s"
type="button"
>
Delete
</EuiButton>,
],
}
}
selection={
Object {
"onSelectionChange": [Function],
}
}
sorting={
Object {
"sort": Object {
"direction": "asc",
"field": "calendar_id",
},
}
}
/>
</React.Fragment>
`;

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { CalendarsListTable } from './table';

View file

@ -0,0 +1,132 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import {
EuiButton,
EuiLink,
EuiInMemoryTable,
} from '@elastic/eui';
import chrome from 'ui/chrome';
export function CalendarsListTable({
calendarsList,
onDeleteClick,
setSelectedCalendarList,
loading,
canCreateCalendar,
canDeleteCalendar,
mlNodesAvailable,
itemsSelected
}) {
const sorting = {
sort: {
field: 'calendar_id',
direction: 'asc',
}
};
const pagination = {
initialPageSize: 20,
pageSizeOptions: [10, 20]
};
const columns = [
{
field: 'calendar_id',
name: 'ID',
sortable: true,
truncateText: true,
render: (id) => (
<EuiLink
href={`${chrome.getBasePath()}/app/ml#/settings/calendars_list/edit_calendar/${id}`}
>
{id}
</EuiLink>
)
},
{
field: 'job_ids_string',
name: 'Jobs',
sortable: true,
truncateText: true,
},
{
field: 'events_length',
name: 'Events',
sortable: true
}
];
const tableSelection = {
onSelectionChange: (selection) => setSelectedCalendarList(selection)
};
const search = {
toolsRight: [
(
<EuiButton
size="s"
data-testid="new_calendar_button"
key="new_calendar_button"
href={`${chrome.getBasePath()}/app/ml#/settings/calendars_list/new_calendar`}
isDisabled={(canCreateCalendar === false || mlNodesAvailable === false)}
>
New
</EuiButton>
),
(
<EuiButton
size="s"
color="danger"
iconType="trash"
onClick={onDeleteClick}
isDisabled={(canDeleteCalendar === false || mlNodesAvailable === false || itemsSelected === false)}
>
Delete
</EuiButton>
)
],
box: {
incremental: true,
},
filters: []
};
return (
<React.Fragment>
<EuiInMemoryTable
items={calendarsList}
itemId="calendar_id"
columns={columns}
search={search}
pagination={pagination}
sorting={sorting}
loading={loading}
selection={tableSelection}
isSelectable={true}
/>
</React.Fragment>
);
}
CalendarsListTable.propTypes = {
calendarsList: PropTypes.array.isRequired,
onDeleteClick: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
canCreateCalendar: PropTypes.bool.isRequired,
canDeleteCalendar: PropTypes.bool.isRequired,
mlNodesAvailable: PropTypes.bool.isRequired,
setSelectedCalendarList: PropTypes.func.isRequired,
itemsSelected: PropTypes.bool.isRequired,
};

View file

@ -0,0 +1,94 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { shallow, mount } from 'enzyme';
import React from 'react';
import { CalendarsListTable } from './table';
jest.mock('ui/chrome', () => ({
getBasePath: jest.fn()
}));
const calendars = [
{
'calendar_id': 'farequote-calendar',
'job_ids': ['farequote'],
'description': 'test ',
'events': [] },
{
'calendar_id': 'this-is-a-new-calendar',
'job_ids': ['test'],
'description': 'new calendar',
'events': [] }];
const props = {
calendarsList: calendars,
canCreateCalendar: true,
canDeleteCalendar: true,
itemsSelected: false,
loading: false,
mlNodesAvailable: true,
onDeleteClick: () => { },
setSelectedCalendarList: () => { }
};
describe('CalendarsListTable', () => {
test('renders the table with all calendars', () => {
const wrapper = shallow(
<CalendarsListTable {...props} />
);
expect(wrapper).toMatchSnapshot();
});
test('New button enabled if permission available', () => {
const wrapper = mount(
<CalendarsListTable {...props} />
);
const buttons = wrapper.find('[data-testid="new_calendar_button"]');
const button = buttons.find('EuiButton');
expect(button.prop('isDisabled')).toEqual(false);
});
test('New button disabled if no permission available', () => {
const disableProps = {
...props,
canCreateCalendar: false
};
const wrapper = mount(
<CalendarsListTable {...disableProps} />
);
const buttons = wrapper.find('[data-testid="new_calendar_button"]');
const button = buttons.find('EuiButton');
expect(button.prop('isDisabled')).toEqual(true);
});
test('New button disabled if no ML nodes available', () => {
const disableProps = {
...props,
mlNodesAvailable: false
};
const wrapper = mount(
<CalendarsListTable {...disableProps} />
);
const buttons = wrapper.find('[data-testid="new_calendar_button"]');
const button = buttons.find('EuiButton');
expect(button.prop('isDisabled')).toEqual(true);
});
});

View file

@ -7,5 +7,5 @@
import './settings_controller';
import './scheduled_events';
import './calendars';
import './filter_lists';

View file

@ -9,7 +9,7 @@
import ngMock from 'ng_mock';
import expect from 'expect.js';
describe('ML - Calendars List Controller', () => {
xdescribe('ML - Calendars List Controller', () => {
beforeEach(() => {
ngMock.module('kibana');
});

View file

@ -11,7 +11,7 @@ import expect from 'expect.js';
const mockModalInstance = { close: function () { }, dismiss: function () { } };
describe('ML - Import Events Modal Controller', () => {
xdescribe('ML - Import Events Modal Controller', () => {
beforeEach(() => {
ngMock.module('kibana');
});

View file

@ -11,7 +11,7 @@ import expect from 'expect.js';
const mockModalInstance = { close: function () { }, dismiss: function () { } };
describe('ML - New Event Modal Controller', () => {
xdescribe('ML - New Event Modal Controller', () => {
beforeEach(() => {
ngMock.module('kibana');
});

View file

@ -9,7 +9,7 @@
import ngMock from 'ng_mock';
import expect from 'expect.js';
describe('ML - Create Calendar Controller', () => {
xdescribe('ML - Create Calendar Controller', () => {
beforeEach(() => {
ngMock.module('kibana');
});