mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* add buttons and links to create/edit policy * set up add policy form * start create policy form, including loading/error states and redirect for repository select field. add inline option to SectionLoading. add actions prop to SectionError * add snapshot name field * Change page title upon app navigation, improve breadcrumbs * Add on cancel to policy form, reorder fields * Add simple cron field * First pass at create/edit policy functionality * Adjust permissions for SLM tab * Adjust no snapshots prompt based on if policies exist or not * Add selectable indices to policy form * Move cron editor from rollup jobs to ES UI shared folder * Used shared cron editor for slm policy create/edit * Adjust copies; add duplicate schedule warning callout * Surface in progress information * Fix doc link for 7.x * Fix rollup tests * Copy edits from review * Add ES endpoint to request review * Remove unused imports * Fix i18n by cleaning up typo'd text * Remove unused import * Fix permissions and i18n * Revert change to Logistics copy * Fix bugs and PR feedback * Add cancel button to form and add comment for list * Adjust timeout comment * Fix bug with list of indices in detail panel when clicking through table * Add comment about EUI bug
This commit is contained in:
parent
9194883c0d
commit
925f21600d
84 changed files with 3696 additions and 708 deletions
|
@ -27,7 +27,8 @@
|
|||
"tsvb": "src/legacy/core_plugins/metrics",
|
||||
"kbnESQuery": "packages/kbn-es-query",
|
||||
"inspector": "src/plugins/inspector",
|
||||
"kibana-react": "src/plugins/kibana_react"
|
||||
"kibana-react": "src/plugins/kibana_react",
|
||||
"esUi": "src/plugins/es_ui_shared"
|
||||
},
|
||||
"exclude": ["src/legacy/ui/ui_render/ui_render_mixin.js"],
|
||||
"translations": []
|
||||
|
|
|
@ -1,7 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
|
@ -27,12 +40,12 @@ export const CronDaily = ({
|
|||
<EuiFormRow
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.cronDaily.fieldTimeLabel"
|
||||
id="esUi.cronEditor.cronDaily.fieldTimeLabel"
|
||||
defaultMessage="Time"
|
||||
/>
|
||||
)}
|
||||
fullWidth
|
||||
data-test-subj="rollupCronFrequencyConfiguration"
|
||||
data-test-subj="cronFrequencyConfiguration"
|
||||
>
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -45,13 +58,13 @@ export const CronDaily = ({
|
|||
<EuiText size="xs">
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.cronDaily.fieldHour.textAtLabel"
|
||||
id="esUi.cronEditor.cronDaily.fieldHour.textAtLabel"
|
||||
defaultMessage="At"
|
||||
/>
|
||||
</strong>
|
||||
</EuiText>
|
||||
)}
|
||||
data-test-subj="rollupJobCreateFrequencyDailyHourSelect"
|
||||
data-test-subj="cronFrequencyDailyHourSelect"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
|
@ -68,7 +81,7 @@ export const CronDaily = ({
|
|||
</strong>
|
||||
</EuiText>
|
||||
)}
|
||||
data-test-subj="rollupJobCreateFrequencyDailyMinuteSelect"
|
||||
data-test-subj="cronFrequencyDailyMinuteSelect"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
|
@ -1,7 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { Component, Fragment } from 'react';
|
||||
|
@ -27,7 +40,7 @@ import {
|
|||
WEEK,
|
||||
MONTH,
|
||||
YEAR,
|
||||
} from '../../../../../services';
|
||||
} from './services';
|
||||
|
||||
import { CronHourly } from './cron_hourly';
|
||||
import { CronDaily } from './cron_daily';
|
||||
|
@ -331,7 +344,7 @@ export class CronEditor extends Component {
|
|||
<EuiFormRow
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.fieldFrequencyLabel"
|
||||
id="esUi.cronEditor.fieldFrequencyLabel"
|
||||
defaultMessage="Frequency"
|
||||
/>
|
||||
)}
|
||||
|
@ -346,13 +359,13 @@ export class CronEditor extends Component {
|
|||
<EuiText size="xs">
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.textEveryLabel"
|
||||
id="esUi.cronEditor.textEveryLabel"
|
||||
defaultMessage="Every"
|
||||
/>
|
||||
</strong>
|
||||
</EuiText>
|
||||
)}
|
||||
data-test-subj="rollupJobCreateFrequencySelect"
|
||||
data-test-subj="cronFrequencySelect"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiFormRow,
|
||||
EuiSelect,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
export const CronHourly = ({
|
||||
minute,
|
||||
minuteOptions,
|
||||
onChange,
|
||||
}) => (
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="esUi.cronEditor.cronHourly.fieldTimeLabel"
|
||||
defaultMessage="Minute"
|
||||
/>
|
||||
)}
|
||||
fullWidth
|
||||
data-test-subj="cronFrequencyConfiguration"
|
||||
>
|
||||
<EuiSelect
|
||||
options={minuteOptions}
|
||||
value={minute}
|
||||
onChange={e => onChange({ minute: e.target.value })}
|
||||
fullWidth
|
||||
prepend={(
|
||||
<EuiText size="xs">
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="esUi.cronEditor.cronHourly.fieldMinute.textAtLabel"
|
||||
defaultMessage="At"
|
||||
/>
|
||||
</strong>
|
||||
</EuiText>
|
||||
)}
|
||||
data-test-subj="cronFrequencyHourlyMinuteSelect"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
CronHourly.propTypes = {
|
||||
minute: PropTypes.string.isRequired,
|
||||
minuteOptions: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
|
@ -1,7 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
|
@ -29,12 +42,12 @@ export const CronMonthly = ({
|
|||
<EuiFormRow
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.cronMonthly.fieldDateLabel"
|
||||
id="esUi.cronEditor.cronMonthly.fieldDateLabel"
|
||||
defaultMessage="Date"
|
||||
/>
|
||||
)}
|
||||
fullWidth
|
||||
data-test-subj="rollupCronFrequencyConfiguration"
|
||||
data-test-subj="cronFrequencyConfiguration"
|
||||
>
|
||||
<EuiSelect
|
||||
options={dateOptions}
|
||||
|
@ -45,25 +58,25 @@ export const CronMonthly = ({
|
|||
<EuiText size="xs">
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.cronMonthly.textOnTheLabel"
|
||||
id="esUi.cronEditor.cronMonthly.textOnTheLabel"
|
||||
defaultMessage="On the"
|
||||
/>
|
||||
</strong>
|
||||
</EuiText>
|
||||
)}
|
||||
data-test-subj="rollupJobCreateFrequencyMonthlyDateSelect"
|
||||
data-test-subj="cronFrequencyMonthlyDateSelect"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.cronMonthly.fieldTimeLabel"
|
||||
id="esUi.cronEditor.cronMonthly.fieldTimeLabel"
|
||||
defaultMessage="Time"
|
||||
/>
|
||||
)}
|
||||
fullWidth
|
||||
data-test-subj="rollupCronFrequencyConfiguration"
|
||||
data-test-subj="cronFrequencyConfiguration"
|
||||
>
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -76,13 +89,13 @@ export const CronMonthly = ({
|
|||
<EuiText size="xs">
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.cronMonthly.fieldHour.textAtLabel"
|
||||
id="esUi.cronEditor.cronMonthly.fieldHour.textAtLabel"
|
||||
defaultMessage="At"
|
||||
/>
|
||||
</strong>
|
||||
</EuiText>
|
||||
)}
|
||||
data-test-subj="rollupJobCreateFrequencyMonthlyHourSelect"
|
||||
data-test-subj="cronFrequencyMonthlyHourSelect"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
|
@ -99,7 +112,7 @@ export const CronMonthly = ({
|
|||
</strong>
|
||||
</EuiText>
|
||||
)}
|
||||
data-test-subj="rollupJobCreateFrequencyMonthlyMinuteSelect"
|
||||
data-test-subj="cronFrequencyMonthlyMinuteSelect"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
|
@ -1,7 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
|
@ -29,12 +42,12 @@ export const CronWeekly = ({
|
|||
<EuiFormRow
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.cronWeekly.fieldDateLabel"
|
||||
id="esUi.cronEditor.cronWeekly.fieldDateLabel"
|
||||
defaultMessage="Day"
|
||||
/>
|
||||
)}
|
||||
fullWidth
|
||||
data-test-subj="rollupCronFrequencyConfiguration"
|
||||
data-test-subj="cronFrequencyConfiguration"
|
||||
>
|
||||
<EuiSelect
|
||||
options={dayOptions}
|
||||
|
@ -45,25 +58,25 @@ export const CronWeekly = ({
|
|||
<EuiText size="xs">
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.cronWeekly.textOnLabel"
|
||||
id="esUi.cronEditor.cronWeekly.textOnLabel"
|
||||
defaultMessage="On"
|
||||
/>
|
||||
</strong>
|
||||
</EuiText>
|
||||
)}
|
||||
data-test-subj="rollupJobCreateFrequencyWeeklyDaySelect"
|
||||
data-test-subj="cronFrequencyWeeklyDaySelect"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.cronWeekly.fieldTimeLabel"
|
||||
id="esUi.cronEditor.cronWeekly.fieldTimeLabel"
|
||||
defaultMessage="Time"
|
||||
/>
|
||||
)}
|
||||
fullWidth
|
||||
data-test-subj="rollupCronFrequencyConfiguration"
|
||||
data-test-subj="cronFrequencyConfiguration"
|
||||
>
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -76,13 +89,13 @@ export const CronWeekly = ({
|
|||
<EuiText size="xs">
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.cronWeekly.fieldHour.textAtLabel"
|
||||
id="esUi.cronEditor.cronWeekly.fieldHour.textAtLabel"
|
||||
defaultMessage="At"
|
||||
/>
|
||||
</strong>
|
||||
</EuiText>
|
||||
)}
|
||||
data-test-subj="rollupJobCreateFrequencyWeeklyHourSelect"
|
||||
data-test-subj="cronFrequencyWeeklyHourSelect"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
|
@ -99,7 +112,7 @@ export const CronWeekly = ({
|
|||
</strong>
|
||||
</EuiText>
|
||||
)}
|
||||
data-test-subj="rollupJobCreateFrequencyWeeklyMinuteSelect"
|
||||
data-test-subj="cronFrequencyWeeklyMinuteSelect"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
|
@ -1,7 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
|
@ -31,12 +44,12 @@ export const CronYearly = ({
|
|||
<EuiFormRow
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.cronYearly.fieldMonthLabel"
|
||||
id="esUi.cronEditor.cronYearly.fieldMonthLabel"
|
||||
defaultMessage="Month"
|
||||
/>
|
||||
)}
|
||||
fullWidth
|
||||
data-test-subj="rollupCronFrequencyConfiguration"
|
||||
data-test-subj="cronFrequencyConfiguration"
|
||||
>
|
||||
<EuiSelect
|
||||
options={monthOptions}
|
||||
|
@ -47,25 +60,25 @@ export const CronYearly = ({
|
|||
<EuiText size="xs">
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.cronYearly.fieldMonth.textInLabel"
|
||||
id="esUi.cronEditor.cronYearly.fieldMonth.textInLabel"
|
||||
defaultMessage="In"
|
||||
/>
|
||||
</strong>
|
||||
</EuiText>
|
||||
)}
|
||||
data-test-subj="rollupJobCreateFrequencyYearlyMonthSelect"
|
||||
data-test-subj="cronFrequencyYearlyMonthSelect"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.cronYearly.fieldDateLabel"
|
||||
id="esUi.cronEditor.cronYearly.fieldDateLabel"
|
||||
defaultMessage="Date"
|
||||
/>
|
||||
)}
|
||||
fullWidth
|
||||
data-test-subj="rollupCronFrequencyConfiguration"
|
||||
data-test-subj="cronFrequencyConfiguration"
|
||||
>
|
||||
<EuiSelect
|
||||
options={dateOptions}
|
||||
|
@ -76,25 +89,25 @@ export const CronYearly = ({
|
|||
<EuiText size="xs">
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.cronYearly.fieldDate.textOnTheLabel"
|
||||
id="esUi.cronEditor.cronYearly.fieldDate.textOnTheLabel"
|
||||
defaultMessage="On the"
|
||||
/>
|
||||
</strong>
|
||||
</EuiText>
|
||||
)}
|
||||
data-test-subj="rollupJobCreateFrequencyYearlyDateSelect"
|
||||
data-test-subj="cronFrequencyYearlyDateSelect"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.cronYearly.fieldTimeLabel"
|
||||
id="esUi.cronEditor.cronYearly.fieldTimeLabel"
|
||||
defaultMessage="Time"
|
||||
/>
|
||||
)}
|
||||
fullWidth
|
||||
data-test-subj="rollupCronFrequencyConfiguration"
|
||||
data-test-subj="cronFrequencyConfiguration"
|
||||
>
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -107,13 +120,13 @@ export const CronYearly = ({
|
|||
<EuiText size="xs">
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.cronYearly.fieldHour.textAtLabel"
|
||||
id="esUi.cronEditor.cronYearly.fieldHour.textAtLabel"
|
||||
defaultMessage="At"
|
||||
/>
|
||||
</strong>
|
||||
</EuiText>
|
||||
)}
|
||||
data-test-subj="rollupJobCreateFrequencyYearlyHourSelect"
|
||||
data-test-subj="cronFrequencyYearlyHourSelect"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
|
@ -130,7 +143,7 @@ export const CronYearly = ({
|
|||
</strong>
|
||||
</EuiText>
|
||||
)}
|
||||
data-test-subj="rollupJobCreateFrequencyYearlyMinuteSelect"
|
||||
data-test-subj="cronFrequencyYearlyMinuteSelect"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
26
src/plugins/es_ui_shared/public/components/cron_editor/index.d.ts
vendored
Normal file
26
src/plugins/es_ui_shared/public/components/cron_editor/index.d.ts
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export declare const MINUTE: string;
|
||||
export declare const HOUR: string;
|
||||
export declare const DAY: string;
|
||||
export declare const WEEK: string;
|
||||
export declare const MONTH: string;
|
||||
export declare const YEAR: string;
|
||||
export declare const CronEditor: any;
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export { CronEditor } from './cron_editor';
|
||||
export { MINUTE, HOUR, DAY, WEEK, MONTH, YEAR } from './services';
|
|
@ -1,7 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const MINUTE = 'MINUTE';
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
// The international ISO standard dictates Monday as the first day of the week, but cron patterns
|
||||
// use Sunday as the first day, so we're going with the cron way.
|
||||
const dayOrdinalToDayNameMap = {
|
||||
0: i18n.translate('esUi.cronEditor.day.sunday', { defaultMessage: 'Sunday' }),
|
||||
1: i18n.translate('esUi.cronEditor.day.monday', { defaultMessage: 'Monday' }),
|
||||
2: i18n.translate('esUi.cronEditor.day.tuesday', { defaultMessage: 'Tuesday' }),
|
||||
3: i18n.translate('esUi.cronEditor.day.wednesday', { defaultMessage: 'Wednesday' }),
|
||||
4: i18n.translate('esUi.cronEditor.day.thursday', { defaultMessage: 'Thursday' }),
|
||||
5: i18n.translate('esUi.cronEditor.day.friday', { defaultMessage: 'Friday' }),
|
||||
6: i18n.translate('esUi.cronEditor.day.saturday', { defaultMessage: 'Saturday' }),
|
||||
};
|
||||
|
||||
const monthOrdinalToMonthNameMap = {
|
||||
0: i18n.translate('esUi.cronEditor.month.january', { defaultMessage: 'January' }),
|
||||
1: i18n.translate('esUi.cronEditor.month.february', { defaultMessage: 'February' }),
|
||||
2: i18n.translate('esUi.cronEditor.month.march', { defaultMessage: 'March' }),
|
||||
3: i18n.translate('esUi.cronEditor.month.april', { defaultMessage: 'April' }),
|
||||
4: i18n.translate('esUi.cronEditor.month.may', { defaultMessage: 'May' }),
|
||||
5: i18n.translate('esUi.cronEditor.month.june', { defaultMessage: 'June' }),
|
||||
6: i18n.translate('esUi.cronEditor.month.july', { defaultMessage: 'July' }),
|
||||
7: i18n.translate('esUi.cronEditor.month.august', { defaultMessage: 'August' }),
|
||||
8: i18n.translate('esUi.cronEditor.month.september', { defaultMessage: 'September' }),
|
||||
9: i18n.translate('esUi.cronEditor.month.october', { defaultMessage: 'October' }),
|
||||
10: i18n.translate('esUi.cronEditor.month.november', { defaultMessage: 'November' }),
|
||||
11: i18n.translate('esUi.cronEditor.month.december', { defaultMessage: 'December' }),
|
||||
};
|
||||
|
||||
export function getOrdinalValue(number) {
|
||||
// TODO: This is breaking reporting pdf generation. Possibly due to phantom not setting locale,
|
||||
// which is needed by i18n (formatjs). Need to verify, fix, and restore i18n in place of static stings.
|
||||
// return i18n.translate('esUi.cronEditor.number.ordinal', {
|
||||
// defaultMessage: '{number, selectordinal, one{#st} two{#nd} few{#rd} other{#th}}',
|
||||
// values: { number },
|
||||
// });
|
||||
// TODO: https://github.com/elastic/kibana/issues/27136
|
||||
|
||||
// Protects against falsey (including 0) values
|
||||
const num = number && number.toString();
|
||||
let lastDigit = num && num.substr(-1);
|
||||
let ordinal;
|
||||
|
||||
if(!lastDigit) {
|
||||
return number;
|
||||
}
|
||||
lastDigit = parseFloat(lastDigit);
|
||||
|
||||
switch(lastDigit) {
|
||||
case 1:
|
||||
ordinal = 'st';
|
||||
break;
|
||||
case 2:
|
||||
ordinal = 'nd';
|
||||
break;
|
||||
case 3:
|
||||
ordinal = 'rd';
|
||||
break;
|
||||
default:
|
||||
ordinal = 'th';
|
||||
}
|
||||
|
||||
return `${num}${ordinal}`;
|
||||
}
|
||||
|
||||
export function getDayName(dayOrdinal) {
|
||||
return dayOrdinalToDayNameMap[dayOrdinal];
|
||||
}
|
||||
|
||||
export function getMonthName(monthOrdinal) {
|
||||
return monthOrdinalToMonthNameMap[monthOrdinal];
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './cron';
|
||||
export * from './humanized_numbers';
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { MINUTE, HOUR, DAY, WEEK, MONTH, YEAR } from '../../public/crud_app/services';
|
||||
import { MINUTE, HOUR, DAY, WEEK, MONTH, YEAR } from '../../../../../../src/plugins/es_ui_shared/public/components/cron_editor';
|
||||
import { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } from '../../../../../../src/legacy/ui/public/index_patterns';
|
||||
import { setupEnvironment, pageHelpers } from './helpers';
|
||||
|
||||
|
@ -162,7 +162,7 @@ describe('Create Rollup Job, step 1: Logistics', () => {
|
|||
|
||||
describe('rollup cron', () => {
|
||||
const changeFrequency = (value) => {
|
||||
find('rollupJobCreateFrequencySelect').simulate('change', { target: { value } });
|
||||
find('cronFrequencySelect').simulate('change', { target: { value } });
|
||||
};
|
||||
|
||||
const generateStringSequenceOfNumbers = (total) => (
|
||||
|
@ -171,7 +171,7 @@ describe('Create Rollup Job, step 1: Logistics', () => {
|
|||
|
||||
describe('frequency', () => {
|
||||
it('should allow "minute", "hour", "day", "week", "month", "year"', () => {
|
||||
const frequencySelect = find('rollupJobCreateFrequencySelect');
|
||||
const frequencySelect = find('cronFrequencySelect');
|
||||
const options = frequencySelect.find('option').map(option => option.text());
|
||||
expect(options).toEqual(['minute', 'hour', 'day', 'week', 'month', 'year']);
|
||||
});
|
||||
|
@ -179,7 +179,7 @@ describe('Create Rollup Job, step 1: Logistics', () => {
|
|||
describe('every minute', () => {
|
||||
it('should not have any additional configuration', () => {
|
||||
changeFrequency(MINUTE);
|
||||
expect(find('rollupCronFrequencyConfiguration').length).toBe(0);
|
||||
expect(find('cronFrequencyConfiguration').length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -189,12 +189,12 @@ describe('Create Rollup Job, step 1: Logistics', () => {
|
|||
});
|
||||
|
||||
it('should have 1 additional configuration', () => {
|
||||
expect(find('rollupCronFrequencyConfiguration').length).toBe(1);
|
||||
expect(exists('rollupJobCreateFrequencyHourlyMinuteSelect')).toBe(true);
|
||||
expect(find('cronFrequencyConfiguration').length).toBe(1);
|
||||
expect(exists('cronFrequencyHourlyMinuteSelect')).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow to select any minute from 00 -> 59', () => {
|
||||
const minutSelect = find('rollupJobCreateFrequencyHourlyMinuteSelect');
|
||||
const minutSelect = find('cronFrequencyHourlyMinuteSelect');
|
||||
const options = minutSelect.find('option').map(option => option.text());
|
||||
expect(options).toEqual(generateStringSequenceOfNumbers(60));
|
||||
});
|
||||
|
@ -206,19 +206,19 @@ describe('Create Rollup Job, step 1: Logistics', () => {
|
|||
});
|
||||
|
||||
it('should have 1 additional configuration with hour and minute selects', () => {
|
||||
expect(find('rollupCronFrequencyConfiguration').length).toBe(1);
|
||||
expect(exists('rollupJobCreateFrequencyDailyHourSelect')).toBe(true);
|
||||
expect(exists('rollupJobCreateFrequencyDailyMinuteSelect')).toBe(true);
|
||||
expect(find('cronFrequencyConfiguration').length).toBe(1);
|
||||
expect(exists('cronFrequencyDailyHourSelect')).toBe(true);
|
||||
expect(exists('cronFrequencyDailyMinuteSelect')).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow to select any hour from 00 -> 23', () => {
|
||||
const hourSelect = find('rollupJobCreateFrequencyDailyHourSelect');
|
||||
const hourSelect = find('cronFrequencyDailyHourSelect');
|
||||
const options = hourSelect.find('option').map(option => option.text());
|
||||
expect(options).toEqual(generateStringSequenceOfNumbers(24));
|
||||
});
|
||||
|
||||
it('should allow to select any miute from 00 -> 59', () => {
|
||||
const minutSelect = find('rollupJobCreateFrequencyDailyMinuteSelect');
|
||||
const minutSelect = find('cronFrequencyDailyMinuteSelect');
|
||||
const options = minutSelect.find('option').map(option => option.text());
|
||||
expect(options).toEqual(generateStringSequenceOfNumbers(60));
|
||||
});
|
||||
|
@ -230,14 +230,14 @@ describe('Create Rollup Job, step 1: Logistics', () => {
|
|||
});
|
||||
|
||||
it('should have 2 additional configurations with day, hour and minute selects', () => {
|
||||
expect(find('rollupCronFrequencyConfiguration').length).toBe(2);
|
||||
expect(exists('rollupJobCreateFrequencyWeeklyDaySelect')).toBe(true);
|
||||
expect(exists('rollupJobCreateFrequencyWeeklyHourSelect')).toBe(true);
|
||||
expect(exists('rollupJobCreateFrequencyWeeklyMinuteSelect')).toBe(true);
|
||||
expect(find('cronFrequencyConfiguration').length).toBe(2);
|
||||
expect(exists('cronFrequencyWeeklyDaySelect')).toBe(true);
|
||||
expect(exists('cronFrequencyWeeklyHourSelect')).toBe(true);
|
||||
expect(exists('cronFrequencyWeeklyMinuteSelect')).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow to select any day of the week', () => {
|
||||
const hourSelect = find('rollupJobCreateFrequencyWeeklyDaySelect');
|
||||
const hourSelect = find('cronFrequencyWeeklyDaySelect');
|
||||
const options = hourSelect.find('option').map(option => option.text());
|
||||
expect(options).toEqual([
|
||||
'Sunday',
|
||||
|
@ -251,13 +251,13 @@ describe('Create Rollup Job, step 1: Logistics', () => {
|
|||
});
|
||||
|
||||
it('should allow to select any hour from 00 -> 23', () => {
|
||||
const hourSelect = find('rollupJobCreateFrequencyWeeklyHourSelect');
|
||||
const hourSelect = find('cronFrequencyWeeklyHourSelect');
|
||||
const options = hourSelect.find('option').map(option => option.text());
|
||||
expect(options).toEqual(generateStringSequenceOfNumbers(24));
|
||||
});
|
||||
|
||||
it('should allow to select any miute from 00 -> 59', () => {
|
||||
const minutSelect = find('rollupJobCreateFrequencyWeeklyMinuteSelect');
|
||||
const minutSelect = find('cronFrequencyWeeklyMinuteSelect');
|
||||
const options = minutSelect.find('option').map(option => option.text());
|
||||
expect(options).toEqual(generateStringSequenceOfNumbers(60));
|
||||
});
|
||||
|
@ -269,26 +269,26 @@ describe('Create Rollup Job, step 1: Logistics', () => {
|
|||
});
|
||||
|
||||
it('should have 2 additional configurations with date, hour and minute selects', () => {
|
||||
expect(find('rollupCronFrequencyConfiguration').length).toBe(2);
|
||||
expect(exists('rollupJobCreateFrequencyMonthlyDateSelect')).toBe(true);
|
||||
expect(exists('rollupJobCreateFrequencyMonthlyHourSelect')).toBe(true);
|
||||
expect(exists('rollupJobCreateFrequencyMonthlyMinuteSelect')).toBe(true);
|
||||
expect(find('cronFrequencyConfiguration').length).toBe(2);
|
||||
expect(exists('cronFrequencyMonthlyDateSelect')).toBe(true);
|
||||
expect(exists('cronFrequencyMonthlyHourSelect')).toBe(true);
|
||||
expect(exists('cronFrequencyMonthlyMinuteSelect')).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow to select any date of the month from 1st to 31st', () => {
|
||||
const dateSelect = find('rollupJobCreateFrequencyMonthlyDateSelect');
|
||||
const dateSelect = find('cronFrequencyMonthlyDateSelect');
|
||||
const options = dateSelect.find('option').map(option => option.text());
|
||||
expect(options.length).toEqual(31);
|
||||
});
|
||||
|
||||
it('should allow to select any hour from 00 -> 23', () => {
|
||||
const hourSelect = find('rollupJobCreateFrequencyMonthlyHourSelect');
|
||||
const hourSelect = find('cronFrequencyMonthlyHourSelect');
|
||||
const options = hourSelect.find('option').map(option => option.text());
|
||||
expect(options).toEqual(generateStringSequenceOfNumbers(24));
|
||||
});
|
||||
|
||||
it('should allow to select any miute from 00 -> 59', () => {
|
||||
const minutSelect = find('rollupJobCreateFrequencyMonthlyMinuteSelect');
|
||||
const minutSelect = find('cronFrequencyMonthlyMinuteSelect');
|
||||
const options = minutSelect.find('option').map(option => option.text());
|
||||
expect(options).toEqual(generateStringSequenceOfNumbers(60));
|
||||
});
|
||||
|
@ -300,15 +300,15 @@ describe('Create Rollup Job, step 1: Logistics', () => {
|
|||
});
|
||||
|
||||
it('should have 3 additional configurations with month, date, hour and minute selects', () => {
|
||||
expect(find('rollupCronFrequencyConfiguration').length).toBe(3);
|
||||
expect(exists('rollupJobCreateFrequencyYearlyMonthSelect')).toBe(true);
|
||||
expect(exists('rollupJobCreateFrequencyYearlyDateSelect')).toBe(true);
|
||||
expect(exists('rollupJobCreateFrequencyYearlyHourSelect')).toBe(true);
|
||||
expect(exists('rollupJobCreateFrequencyYearlyMinuteSelect')).toBe(true);
|
||||
expect(find('cronFrequencyConfiguration').length).toBe(3);
|
||||
expect(exists('cronFrequencyYearlyMonthSelect')).toBe(true);
|
||||
expect(exists('cronFrequencyYearlyDateSelect')).toBe(true);
|
||||
expect(exists('cronFrequencyYearlyHourSelect')).toBe(true);
|
||||
expect(exists('cronFrequencyYearlyMinuteSelect')).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow to select any month of the year', () => {
|
||||
const monthSelect = find('rollupJobCreateFrequencyYearlyMonthSelect');
|
||||
const monthSelect = find('cronFrequencyYearlyMonthSelect');
|
||||
const options = monthSelect.find('option').map(option => option.text());
|
||||
expect(options).toEqual([
|
||||
'January',
|
||||
|
@ -327,19 +327,19 @@ describe('Create Rollup Job, step 1: Logistics', () => {
|
|||
});
|
||||
|
||||
it('should allow to select any date of the month from 1st to 31st', () => {
|
||||
const dateSelect = find('rollupJobCreateFrequencyYearlyDateSelect');
|
||||
const dateSelect = find('cronFrequencyYearlyDateSelect');
|
||||
const options = dateSelect.find('option').map(option => option.text());
|
||||
expect(options.length).toEqual(31);
|
||||
});
|
||||
|
||||
it('should allow to select any hour from 00 -> 23', () => {
|
||||
const hourSelect = find('rollupJobCreateFrequencyYearlyHourSelect');
|
||||
const hourSelect = find('cronFrequencyYearlyHourSelect');
|
||||
const options = hourSelect.find('option').map(option => option.text());
|
||||
expect(options).toEqual(generateStringSequenceOfNumbers(24));
|
||||
});
|
||||
|
||||
it('should allow to select any miute from 00 -> 59', () => {
|
||||
const minutSelect = find('rollupJobCreateFrequencyYearlyMinuteSelect');
|
||||
const minutSelect = find('cronFrequencyYearlyMinuteSelect');
|
||||
const options = minutSelect.find('option').map(option => option.text());
|
||||
expect(options).toEqual(generateStringSequenceOfNumbers(60));
|
||||
});
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiFormRow,
|
||||
EuiSelect,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
export const CronHourly = ({
|
||||
minute,
|
||||
minuteOptions,
|
||||
onChange,
|
||||
}) => (
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.cronHourly.fieldTimeLabel"
|
||||
defaultMessage="Minute"
|
||||
/>
|
||||
)}
|
||||
fullWidth
|
||||
data-test-subj="rollupCronFrequencyConfiguration"
|
||||
>
|
||||
<EuiSelect
|
||||
options={minuteOptions}
|
||||
value={minute}
|
||||
onChange={e => onChange({ minute: e.target.value })}
|
||||
fullWidth
|
||||
prepend={(
|
||||
<EuiText size="xs">
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.rollupJobs.cronEditor.cronHourly.fieldMinute.textAtLabel"
|
||||
defaultMessage="At"
|
||||
/>
|
||||
</strong>
|
||||
</EuiText>
|
||||
)}
|
||||
data-test-subj="rollupJobCreateFrequencyHourlyMinuteSelect"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
CronHourly.propTypes = {
|
||||
minute: PropTypes.string.isRequired,
|
||||
minuteOptions: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
|
@ -5,5 +5,4 @@
|
|||
*/
|
||||
|
||||
export { FieldChooser } from './field_chooser';
|
||||
export { CronEditor } from './cron_editor';
|
||||
export { StepError } from './step_error';
|
||||
|
|
|
@ -24,10 +24,12 @@ import {
|
|||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { CronEditor } from '../../../../../../../../../src/plugins/es_ui_shared/public/components/cron_editor';
|
||||
import { INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/index_patterns';
|
||||
import { INDEX_ILLEGAL_CHARACTERS_VISIBLE } from 'ui/indices';
|
||||
import { logisticalDetailsUrl, cronUrl } from '../../../services';
|
||||
import { CronEditor, StepError } from './components';
|
||||
import { StepError } from './components';
|
||||
|
||||
const indexPatternIllegalCharacters = INDEX_PATTERN_ILLEGAL_CHARACTERS_VISIBLE.join(' ');
|
||||
const indexIllegalCharacters = INDEX_ILLEGAL_CHARACTERS_VISIBLE.join(' ');
|
||||
|
|
|
@ -8,7 +8,8 @@ import cloneDeep from 'lodash/lang/cloneDeep';
|
|||
import get from 'lodash/object/get';
|
||||
import pick from 'lodash/object/pick';
|
||||
|
||||
import { WEEK } from '../../../services';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { WEEK } from '../../../../../../../../../src/plugins/es_ui_shared/public/components/cron_editor';
|
||||
|
||||
import { validateId } from './validate_id';
|
||||
import { validateIndexPattern } from './validate_index_pattern';
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
// The international ISO standard dictates Monday as the first day of the week, but cron patterns
|
||||
// use Sunday as the first day, so we're going with the cron way.
|
||||
const dayOrdinalToDayNameMap = {
|
||||
0: i18n.translate('xpack.rollupJobs.util.day.sunday', { defaultMessage: 'Sunday' }),
|
||||
1: i18n.translate('xpack.rollupJobs.util.day.monday', { defaultMessage: 'Monday' }),
|
||||
2: i18n.translate('xpack.rollupJobs.util.day.tuesday', { defaultMessage: 'Tuesday' }),
|
||||
3: i18n.translate('xpack.rollupJobs.util.day.wednesday', { defaultMessage: 'Wednesday' }),
|
||||
4: i18n.translate('xpack.rollupJobs.util.day.thursday', { defaultMessage: 'Thursday' }),
|
||||
5: i18n.translate('xpack.rollupJobs.util.day.friday', { defaultMessage: 'Friday' }),
|
||||
6: i18n.translate('xpack.rollupJobs.util.day.saturday', { defaultMessage: 'Saturday' }),
|
||||
};
|
||||
|
||||
const monthOrdinalToMonthNameMap = {
|
||||
0: i18n.translate('xpack.rollupJobs.util.month.january', { defaultMessage: 'January' }),
|
||||
1: i18n.translate('xpack.rollupJobs.util.month.february', { defaultMessage: 'February' }),
|
||||
2: i18n.translate('xpack.rollupJobs.util.month.march', { defaultMessage: 'March' }),
|
||||
3: i18n.translate('xpack.rollupJobs.util.month.april', { defaultMessage: 'April' }),
|
||||
4: i18n.translate('xpack.rollupJobs.util.month.may', { defaultMessage: 'May' }),
|
||||
5: i18n.translate('xpack.rollupJobs.util.month.june', { defaultMessage: 'June' }),
|
||||
6: i18n.translate('xpack.rollupJobs.util.month.july', { defaultMessage: 'July' }),
|
||||
7: i18n.translate('xpack.rollupJobs.util.month.august', { defaultMessage: 'August' }),
|
||||
8: i18n.translate('xpack.rollupJobs.util.month.september', { defaultMessage: 'September' }),
|
||||
9: i18n.translate('xpack.rollupJobs.util.month.october', { defaultMessage: 'October' }),
|
||||
10: i18n.translate('xpack.rollupJobs.util.month.november', { defaultMessage: 'November' }),
|
||||
11: i18n.translate('xpack.rollupJobs.util.month.december', { defaultMessage: 'December' }),
|
||||
};
|
||||
|
||||
export function getOrdinalValue(number) {
|
||||
// TODO: This is breaking reporting pdf generation. Possibly due to phantom not setting locale,
|
||||
// which is needed by i18n (formatjs). Need to verify, fix, and restore i18n in place of static stings.
|
||||
// return i18n.translate('xpack.rollupJobs.util.number.ordinal', {
|
||||
// defaultMessage: '{number, selectordinal, one{#st} two{#nd} few{#rd} other{#th}}',
|
||||
// values: { number },
|
||||
// });
|
||||
// TODO: https://github.com/elastic/kibana/issues/27136
|
||||
|
||||
// Protects against falsey (including 0) values
|
||||
const num = number && number.toString();
|
||||
let lastDigit = num && num.substr(-1);
|
||||
let ordinal;
|
||||
|
||||
if(!lastDigit) {
|
||||
return number;
|
||||
}
|
||||
lastDigit = parseFloat(lastDigit);
|
||||
|
||||
switch(lastDigit) {
|
||||
case 1:
|
||||
ordinal = 'st';
|
||||
break;
|
||||
case 2:
|
||||
ordinal = 'nd';
|
||||
break;
|
||||
case 3:
|
||||
ordinal = 'rd';
|
||||
break;
|
||||
default:
|
||||
ordinal = 'th';
|
||||
}
|
||||
|
||||
return `${num}${ordinal}`;
|
||||
}
|
||||
|
||||
export function getDayName(dayOrdinal) {
|
||||
return dayOrdinalToDayNameMap[dayOrdinal];
|
||||
}
|
||||
|
||||
export function getMonthName(monthOrdinal) {
|
||||
return monthOrdinalToMonthNameMap[monthOrdinal];
|
||||
}
|
|
@ -23,17 +23,6 @@ export {
|
|||
createBreadcrumb,
|
||||
} from './breadcrumbs';
|
||||
|
||||
export {
|
||||
cronExpressionToParts,
|
||||
cronPartsToExpression,
|
||||
MINUTE,
|
||||
HOUR,
|
||||
DAY,
|
||||
WEEK,
|
||||
MONTH,
|
||||
YEAR,
|
||||
} from './cron';
|
||||
|
||||
export {
|
||||
logisticalDetailsUrl,
|
||||
dateHistogramDetailsUrl,
|
||||
|
@ -61,12 +50,6 @@ export {
|
|||
getHttp,
|
||||
} from './http_provider';
|
||||
|
||||
export {
|
||||
getOrdinalValue,
|
||||
getDayName,
|
||||
getMonthName,
|
||||
} from './humanized_numbers';
|
||||
|
||||
export {
|
||||
serializeJob,
|
||||
deserializeJob,
|
||||
|
|
|
@ -53,3 +53,4 @@ export const APP_REQUIRED_CLUSTER_PRIVILEGES = [
|
|||
'cluster:admin/repository',
|
||||
];
|
||||
export const APP_RESTORE_INDEX_PRIVILEGES = ['monitor'];
|
||||
export const APP_SLM_CLUSTER_PRIVILEGES = ['manage_slm'];
|
||||
|
|
|
@ -8,3 +8,9 @@ export {
|
|||
deserializeRestoreSettings,
|
||||
serializeRestoreSettings,
|
||||
} from './restore_settings_serialization';
|
||||
export {
|
||||
deserializeSnapshotDetails,
|
||||
deserializeSnapshotConfig,
|
||||
serializeSnapshotConfig,
|
||||
} from './snapshot_serialization';
|
||||
export { deserializePolicy, serializePolicy } from './policy_serialization';
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { SlmPolicy, SlmPolicyEs } from '../../common/types';
|
||||
import { deserializeSnapshotConfig } from './';
|
||||
import { SlmPolicy, SlmPolicyEs, SlmPolicyPayload } from '../types';
|
||||
import { deserializeSnapshotConfig, serializeSnapshotConfig } from './';
|
||||
|
||||
export const deserializePolicy = (name: string, esPolicy: SlmPolicyEs): SlmPolicy => {
|
||||
const {
|
||||
|
@ -16,6 +16,7 @@ export const deserializePolicy = (name: string, esPolicy: SlmPolicyEs): SlmPolic
|
|||
next_execution_millis: nextExecutionMillis,
|
||||
last_failure: lastFailure,
|
||||
last_success: lastSuccess,
|
||||
in_progress: inProgress,
|
||||
} = esPolicy;
|
||||
|
||||
const policy: SlmPolicy = {
|
||||
|
@ -26,11 +27,14 @@ export const deserializePolicy = (name: string, esPolicy: SlmPolicyEs): SlmPolic
|
|||
snapshotName,
|
||||
schedule,
|
||||
repository,
|
||||
config: deserializeSnapshotConfig(config),
|
||||
nextExecution,
|
||||
nextExecutionMillis,
|
||||
};
|
||||
|
||||
if (config) {
|
||||
policy.config = deserializeSnapshotConfig(config);
|
||||
}
|
||||
|
||||
if (lastFailure) {
|
||||
const {
|
||||
snapshot_name: failureSnapshotName,
|
||||
|
@ -70,5 +74,28 @@ export const deserializePolicy = (name: string, esPolicy: SlmPolicyEs): SlmPolic
|
|||
};
|
||||
}
|
||||
|
||||
if (inProgress) {
|
||||
const { name: inProgressSnapshotName } = inProgress;
|
||||
|
||||
policy.inProgress = {
|
||||
snapshotName: inProgressSnapshotName,
|
||||
};
|
||||
}
|
||||
|
||||
return policy;
|
||||
};
|
||||
|
||||
export const serializePolicy = (policy: SlmPolicyPayload): SlmPolicyEs['policy'] => {
|
||||
const { snapshotName: name, schedule, repository, config } = policy;
|
||||
const policyEs: SlmPolicyEs['policy'] = {
|
||||
name,
|
||||
schedule,
|
||||
repository,
|
||||
};
|
||||
|
||||
if (config) {
|
||||
policyEs.config = serializeSnapshotConfig(config);
|
||||
}
|
||||
|
||||
return policyEs;
|
||||
};
|
|
@ -6,12 +6,7 @@
|
|||
|
||||
import { sortBy } from 'lodash';
|
||||
|
||||
import {
|
||||
SnapshotDetails,
|
||||
SnapshotDetailsEs,
|
||||
SnapshotConfig,
|
||||
SnapshotConfigEs,
|
||||
} from '../../common/types';
|
||||
import { SnapshotDetails, SnapshotDetailsEs, SnapshotConfig, SnapshotConfigEs } from '../types';
|
||||
|
||||
export function deserializeSnapshotDetails(
|
||||
repository: string,
|
||||
|
@ -114,3 +109,22 @@ export function deserializeSnapshotConfig(snapshotConfigEs: SnapshotConfigEs): S
|
|||
return config;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function serializeSnapshotConfig(snapshotConfig: SnapshotConfig): SnapshotConfigEs {
|
||||
const { indices, ignoreUnavailable, includeGlobalState, partial, metadata } = snapshotConfig;
|
||||
|
||||
const snapshotConfigEs: SnapshotConfigEs = {
|
||||
indices,
|
||||
ignore_unavailable: ignoreUnavailable,
|
||||
include_global_state: includeGlobalState,
|
||||
partial,
|
||||
metadata,
|
||||
};
|
||||
|
||||
return Object.entries(snapshotConfigEs).reduce((config: any, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
config[key] = value;
|
||||
}
|
||||
return config;
|
||||
}, {});
|
||||
}
|
|
@ -6,15 +6,18 @@
|
|||
|
||||
import { SnapshotConfig, SnapshotConfigEs } from './snapshot';
|
||||
|
||||
export interface SlmPolicy {
|
||||
export interface SlmPolicyPayload {
|
||||
name: string;
|
||||
version: number;
|
||||
modifiedDate: string;
|
||||
modifiedDateMillis: number;
|
||||
snapshotName: string;
|
||||
schedule: string;
|
||||
repository: string;
|
||||
config: SnapshotConfig;
|
||||
config?: SnapshotConfig;
|
||||
}
|
||||
|
||||
export interface SlmPolicy extends SlmPolicyPayload {
|
||||
version: number;
|
||||
modifiedDate: string;
|
||||
modifiedDateMillis: number;
|
||||
nextExecution: string;
|
||||
nextExecutionMillis: number;
|
||||
lastSuccess?: {
|
||||
|
@ -28,6 +31,9 @@ export interface SlmPolicy {
|
|||
time: number;
|
||||
details: object | string;
|
||||
};
|
||||
inProgress?: {
|
||||
snapshotName: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SlmPolicyEs {
|
||||
|
@ -38,7 +44,7 @@ export interface SlmPolicyEs {
|
|||
name: string;
|
||||
schedule: string;
|
||||
repository: string;
|
||||
config: SnapshotConfigEs;
|
||||
config?: SnapshotConfigEs;
|
||||
};
|
||||
next_execution: string;
|
||||
next_execution_millis: number;
|
||||
|
@ -53,4 +59,11 @@ export interface SlmPolicyEs {
|
|||
time: number;
|
||||
details: string;
|
||||
};
|
||||
in_progress?: {
|
||||
name: string;
|
||||
uuid: string;
|
||||
state: string;
|
||||
start_time: string;
|
||||
start_time_millis: number;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
export interface SnapshotConfig {
|
||||
indices?: string[];
|
||||
indices?: string | string[];
|
||||
ignoreUnavailable?: boolean;
|
||||
includeGlobalState?: boolean;
|
||||
partial?: boolean;
|
||||
|
@ -14,7 +14,7 @@ export interface SnapshotConfig {
|
|||
}
|
||||
|
||||
export interface SnapshotConfigEs {
|
||||
indices?: string[];
|
||||
indices?: string | string[];
|
||||
ignore_unavailable?: boolean;
|
||||
include_global_state?: boolean;
|
||||
partial?: boolean;
|
||||
|
|
|
@ -8,9 +8,17 @@ import React, { useContext } from 'react';
|
|||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
import { EuiPageContent } from '@elastic/eui';
|
||||
|
||||
import { APP_REQUIRED_CLUSTER_PRIVILEGES } from '../../common/constants';
|
||||
import { SectionLoading, SectionError } from './components';
|
||||
import { BASE_PATH, DEFAULT_SECTION, Section } from './constants';
|
||||
import { RepositoryAdd, RepositoryEdit, RestoreSnapshot, SnapshotRestoreHome } from './sections';
|
||||
import {
|
||||
RepositoryAdd,
|
||||
RepositoryEdit,
|
||||
RestoreSnapshot,
|
||||
SnapshotRestoreHome,
|
||||
PolicyAdd,
|
||||
PolicyEdit,
|
||||
} from './sections';
|
||||
import { useAppDependencies } from './index';
|
||||
import { AuthorizationContext, WithPrivileges, NotAuthorizedSection } from './lib/authorization';
|
||||
|
||||
|
@ -36,7 +44,7 @@ export const App: React.FunctionComponent = () => {
|
|||
error={apiError}
|
||||
/>
|
||||
) : (
|
||||
<WithPrivileges privileges="cluster.*">
|
||||
<WithPrivileges privileges={APP_REQUIRED_CLUSTER_PRIVILEGES.map(name => `cluster.${name}`)}>
|
||||
{({ isLoading, hasPrivileges, privilegesMissing }) =>
|
||||
isLoading ? (
|
||||
<SectionLoading>
|
||||
|
@ -69,6 +77,8 @@ export const App: React.FunctionComponent = () => {
|
|||
path={`${BASE_PATH}/restore/:repositoryName/:snapshotId*`}
|
||||
component={RestoreSnapshot}
|
||||
/>
|
||||
<Route exact path={`${BASE_PATH}/add_policy`} component={PolicyAdd} />
|
||||
<Route exact path={`${BASE_PATH}/edit_policy/:name*`} component={PolicyEdit} />
|
||||
<Redirect from={`${BASE_PATH}`} to={`${BASE_PATH}/${DEFAULT_SECTION}`} />
|
||||
</Switch>
|
||||
</div>
|
||||
|
|
|
@ -16,3 +16,4 @@ export { SnapshotDeleteProvider } from './snapshot_delete_provider';
|
|||
export { RestoreSnapshotForm } from './restore_snapshot_form';
|
||||
export { PolicyExecuteProvider } from './policy_execute_provider';
|
||||
export { PolicyDeleteProvider } from './policy_delete_provider';
|
||||
export { PolicyForm } from './policy_form';
|
||||
|
|
|
@ -87,7 +87,7 @@ export const PolicyExecuteProvider: React.FunctionComponent<Props> = ({ children
|
|||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.executePolicy.confirmModal.executePolicyTitle"
|
||||
defaultMessage="Run policy '{name}'?"
|
||||
defaultMessage="Run '{name}' now?"
|
||||
values={{ name: policyName }}
|
||||
/>
|
||||
}
|
||||
|
@ -102,18 +102,11 @@ export const PolicyExecuteProvider: React.FunctionComponent<Props> = ({ children
|
|||
confirmButtonText={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.executePolicy.confirmModal.confirmButtonLabel"
|
||||
defaultMessage="Run"
|
||||
defaultMessage="Run policy"
|
||||
/>
|
||||
}
|
||||
data-test-subj="srExecutePolicyConfirmationModal"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.executePolicy.confirmModal.executeDescription"
|
||||
defaultMessage="A snapshot will be taken immediately using this policy configuration."
|
||||
/>
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
/>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Prevent switch controls from moving around when toggling content
|
||||
*/
|
||||
.snapshotRestore__policyForm__stepSettings {
|
||||
.euiFormRow--hasEmptyLabelSpace {
|
||||
min-height: auto;
|
||||
margin-top: $euiFontSizeXS + $euiSizeS + ($euiSizeXXL / 4);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Allow toggle mode link in indices field label to be flushed right
|
||||
*/
|
||||
.snapshotRestore__policyForm__stepSettings__indicesFieldWrapper .euiFormLabel {
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
/*
|
||||
* 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 { PolicyForm } from './policy_form';
|
|
@ -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 React from 'react';
|
||||
import { EuiStepsHorizontal } from '@elastic/eui';
|
||||
import { useAppDependencies } from '../../index';
|
||||
|
||||
interface Props {
|
||||
currentStep: number;
|
||||
maxCompletedStep: number;
|
||||
updateCurrentStep: (step: number) => void;
|
||||
}
|
||||
|
||||
export const PolicyNavigation: React.FunctionComponent<Props> = ({
|
||||
currentStep,
|
||||
maxCompletedStep,
|
||||
updateCurrentStep,
|
||||
}) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: i18n.translate('xpack.snapshotRestore.policyForm.navigation.stepLogisticsName', {
|
||||
defaultMessage: 'Logistics',
|
||||
}),
|
||||
isComplete: maxCompletedStep >= 1,
|
||||
isSelected: currentStep === 1,
|
||||
onClick: () => updateCurrentStep(1),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.snapshotRestore.policyForm.navigation.stepSettingsName', {
|
||||
defaultMessage: 'Snapshot settings',
|
||||
}),
|
||||
isComplete: maxCompletedStep >= 2,
|
||||
isSelected: currentStep === 2,
|
||||
disabled: maxCompletedStep < 1,
|
||||
onClick: () => updateCurrentStep(2),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.snapshotRestore.policyForm.navigation.stepReviewName', {
|
||||
defaultMessage: 'Review',
|
||||
}),
|
||||
isComplete: maxCompletedStep >= 2,
|
||||
isSelected: currentStep === 3,
|
||||
disabled: maxCompletedStep < 2,
|
||||
onClick: () => updateCurrentStep(3),
|
||||
},
|
||||
];
|
||||
|
||||
return <EuiStepsHorizontal steps={steps} />;
|
||||
};
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* 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, useState } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { SlmPolicyPayload } from '../../../../common/types';
|
||||
import { PolicyValidation, validatePolicy } from '../../services/validation';
|
||||
import { useAppDependencies } from '../../index';
|
||||
import { PolicyStepLogistics, PolicyStepSettings, PolicyStepReview } from './steps';
|
||||
import { PolicyNavigation } from './navigation';
|
||||
|
||||
interface Props {
|
||||
policy: SlmPolicyPayload;
|
||||
indices: string[];
|
||||
currentUrl: string;
|
||||
isEditing?: boolean;
|
||||
isSaving: boolean;
|
||||
saveError?: React.ReactNode;
|
||||
clearSaveError: () => void;
|
||||
onCancel: () => void;
|
||||
onSave: (policy: SlmPolicyPayload) => void;
|
||||
}
|
||||
|
||||
export const PolicyForm: React.FunctionComponent<Props> = ({
|
||||
policy: originalPolicy,
|
||||
indices,
|
||||
currentUrl,
|
||||
isEditing,
|
||||
isSaving,
|
||||
saveError,
|
||||
clearSaveError,
|
||||
onCancel,
|
||||
onSave,
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
|
||||
// Step state
|
||||
const [currentStep, setCurrentStep] = useState<number>(1);
|
||||
const [maxCompletedStep, setMaxCompletedStep] = useState<number>(0);
|
||||
const stepMap: { [key: number]: any } = {
|
||||
1: PolicyStepLogistics,
|
||||
2: PolicyStepSettings,
|
||||
3: PolicyStepReview,
|
||||
};
|
||||
const CurrentStepForm = stepMap[currentStep];
|
||||
|
||||
// Policy state
|
||||
const [policy, setPolicy] = useState<SlmPolicyPayload>({
|
||||
...originalPolicy,
|
||||
config: {
|
||||
...(originalPolicy.config || {}),
|
||||
},
|
||||
});
|
||||
|
||||
// Policy validation state
|
||||
const [validation, setValidation] = useState<PolicyValidation>({
|
||||
isValid: true,
|
||||
errors: {},
|
||||
});
|
||||
|
||||
const updatePolicy = (updatedFields: any): void => {
|
||||
const newPolicy = { ...policy, ...updatedFields };
|
||||
const newValidation = validatePolicy(newPolicy);
|
||||
setPolicy(newPolicy);
|
||||
setValidation(newValidation);
|
||||
};
|
||||
|
||||
const updateCurrentStep = (step: number) => {
|
||||
if (maxCompletedStep < step - 1) {
|
||||
return;
|
||||
}
|
||||
setCurrentStep(step);
|
||||
setMaxCompletedStep(step - 1);
|
||||
clearSaveError();
|
||||
};
|
||||
|
||||
const onBack = () => {
|
||||
const previousStep = currentStep - 1;
|
||||
setCurrentStep(previousStep);
|
||||
setMaxCompletedStep(previousStep - 1);
|
||||
clearSaveError();
|
||||
};
|
||||
|
||||
const onNext = () => {
|
||||
if (!validation.isValid) {
|
||||
return;
|
||||
}
|
||||
const nextStep = currentStep + 1;
|
||||
setMaxCompletedStep(Math.max(currentStep, maxCompletedStep));
|
||||
setCurrentStep(nextStep);
|
||||
};
|
||||
|
||||
const savePolicy = () => {
|
||||
if (validation.isValid) {
|
||||
onSave(policy);
|
||||
}
|
||||
};
|
||||
|
||||
const lastStep = Object.keys(stepMap).length;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PolicyNavigation
|
||||
currentStep={currentStep}
|
||||
maxCompletedStep={maxCompletedStep}
|
||||
updateCurrentStep={updateCurrentStep}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiForm>
|
||||
<CurrentStepForm
|
||||
policy={policy}
|
||||
indices={indices}
|
||||
updatePolicy={updatePolicy}
|
||||
isEditing={isEditing}
|
||||
currentUrl={currentUrl}
|
||||
errors={validation.errors}
|
||||
updateCurrentStep={updateCurrentStep}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
{saveError ? (
|
||||
<Fragment>
|
||||
{saveError}
|
||||
<EuiSpacer size="m" />
|
||||
</Fragment>
|
||||
) : null}
|
||||
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup>
|
||||
{currentStep > 1 ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
iconType="arrowLeft"
|
||||
onClick={() => onBack()}
|
||||
disabled={!validation.isValid}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.backButtonLabel"
|
||||
defaultMessage="Back"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
{currentStep < lastStep ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
iconType="arrowRight"
|
||||
onClick={() => onNext()}
|
||||
disabled={!validation.isValid}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.nextButtonLabel"
|
||||
defaultMessage="Next"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
{currentStep === lastStep ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
color="secondary"
|
||||
iconType="check"
|
||||
onClick={() => savePolicy()}
|
||||
isLoading={isSaving}
|
||||
>
|
||||
{isSaving ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.savingButtonLabel"
|
||||
defaultMessage="Saving…"
|
||||
/>
|
||||
) : isEditing ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.saveButtonLabel"
|
||||
defaultMessage="Save policy"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.createButtonLabel"
|
||||
defaultMessage="Create policy"
|
||||
/>
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={() => onCancel()}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
<EuiSpacer size="m" />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { SlmPolicyPayload } from '../../../../../common/types';
|
||||
import { PolicyValidation } from '../../../services/validation';
|
||||
|
||||
export interface StepProps {
|
||||
policy: SlmPolicyPayload;
|
||||
indices: string[];
|
||||
updatePolicy: (updatedSettings: Partial<SlmPolicyPayload>) => void;
|
||||
isEditing: boolean;
|
||||
currentUrl: string;
|
||||
errors: PolicyValidation['errors'];
|
||||
updateCurrentStep: (step: number) => void;
|
||||
}
|
||||
|
||||
export { PolicyStepLogistics } from './step_logistics';
|
||||
export { PolicyStepSettings } from './step_settings';
|
||||
export { PolicyStepReview } from './step_review';
|
|
@ -0,0 +1,507 @@
|
|||
/*
|
||||
* 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, useState } from 'react';
|
||||
|
||||
import {
|
||||
EuiDescribedFormGroup,
|
||||
EuiTitle,
|
||||
EuiFormRow,
|
||||
EuiFieldText,
|
||||
EuiSelect,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiLink,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { Repository } from '../../../../../common/types';
|
||||
import { CronEditor } from '../../../../shared_imports';
|
||||
import { DEFAULT_POLICY_SCHEDULE, DEFAULT_POLICY_FREQUENCY } from '../../../constants';
|
||||
import { useLoadRepositories } from '../../../services/http';
|
||||
import { linkToAddRepository } from '../../../services/navigation';
|
||||
import { documentationLinksService } from '../../../services/documentation';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { SectionLoading, SectionError } from '../../';
|
||||
import { StepProps } from './';
|
||||
|
||||
export const PolicyStepLogistics: React.FunctionComponent<StepProps> = ({
|
||||
policy,
|
||||
updatePolicy,
|
||||
isEditing,
|
||||
currentUrl,
|
||||
errors,
|
||||
}) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
const { FormattedMessage } = i18n;
|
||||
|
||||
// Load repositories for repository dropdown field
|
||||
const {
|
||||
error: errorLoadingRepositories,
|
||||
isLoading: isLoadingRepositories,
|
||||
data: { repositories } = {
|
||||
repositories: [],
|
||||
},
|
||||
sendRequest: reloadRepositories,
|
||||
} = useLoadRepositories();
|
||||
|
||||
// State for touched inputs
|
||||
const [touched, setTouched] = useState({
|
||||
name: false,
|
||||
snapshotName: false,
|
||||
repository: false,
|
||||
schedule: false,
|
||||
});
|
||||
|
||||
// State for cron editor
|
||||
const [simpleCron, setSimpleCron] = useState<{
|
||||
expression: string;
|
||||
frequency: string;
|
||||
}>({
|
||||
expression: DEFAULT_POLICY_SCHEDULE,
|
||||
frequency: DEFAULT_POLICY_FREQUENCY,
|
||||
});
|
||||
const [isAdvancedCronVisible, setIsAdvancedCronVisible] = useState<boolean>(
|
||||
Boolean(policy.schedule && policy.schedule !== DEFAULT_POLICY_SCHEDULE)
|
||||
);
|
||||
const [fieldToPreferredValueMap, setFieldToPreferredValueMap] = useState<any>({});
|
||||
|
||||
const renderNameField = () => (
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.nameDescriptionTitle"
|
||||
defaultMessage="Policy name"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.nameDescription"
|
||||
defaultMessage="A unique identifier for this policy."
|
||||
/>
|
||||
}
|
||||
idAria="nameDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.nameLabel"
|
||||
defaultMessage="Name"
|
||||
/>
|
||||
}
|
||||
describedByIds={['nameDescription']}
|
||||
isInvalid={touched.name && Boolean(errors.name)}
|
||||
error={errors.name}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={policy.name}
|
||||
fullWidth
|
||||
onBlur={() => setTouched({ ...touched, name: true })}
|
||||
onChange={e => {
|
||||
updatePolicy({
|
||||
name: e.target.value,
|
||||
});
|
||||
}}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.snapshotRestore.policyForm.stepLogistics.namePlaceholder',
|
||||
{
|
||||
defaultMessage: 'daily-snapshots',
|
||||
description:
|
||||
'Example SLM policy name. Similar to index names, do not use spaces in translation.',
|
||||
}
|
||||
)}
|
||||
data-test-subj="nameInput"
|
||||
disabled={isEditing}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
|
||||
const renderRepositoryField = () => (
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.repositoryDescriptionTitle"
|
||||
defaultMessage="Repository"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.repositoryDescription"
|
||||
defaultMessage="The repository where you want to store the snapshots."
|
||||
/>
|
||||
}
|
||||
idAria="policyRepositoryDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.policyRepositoryLabel"
|
||||
defaultMessage="Repository"
|
||||
/>
|
||||
}
|
||||
describedByIds={['policyRepositoryDescription']}
|
||||
isInvalid={touched.repository && Boolean(errors.repository)}
|
||||
error={errors.repository}
|
||||
fullWidth
|
||||
>
|
||||
{renderRepositorySelect()}
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
|
||||
const renderRepositorySelect = () => {
|
||||
if (isLoadingRepositories) {
|
||||
return (
|
||||
<SectionLoading inline={true}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.loadingRepositoriesDescription"
|
||||
defaultMessage="Loading repositories…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
}
|
||||
|
||||
if (errorLoadingRepositories) {
|
||||
return (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.loadingRepositoriesErrorMessage"
|
||||
defaultMessage="Error loading repositories"
|
||||
/>
|
||||
}
|
||||
error={{ data: { error: 'test' } } || errorLoadingRepositories}
|
||||
actions={
|
||||
<EuiButton
|
||||
onClick={() => reloadRepositories()}
|
||||
color="danger"
|
||||
iconType="refresh"
|
||||
data-test-subj="reloadRepositoriesButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.reloadRepositoriesButtonLabel"
|
||||
defaultMessage="Reload repositories"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (repositories.length === 0) {
|
||||
return (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.noRepositoriesErrorTitle"
|
||||
defaultMessage="You don't have any repositories"
|
||||
/>
|
||||
}
|
||||
error={{
|
||||
data: {
|
||||
error: i18n.translate('xpack.snapshotRestore.policyForm.noRepositoriesErrorMessage', {
|
||||
defaultMessage: 'You must register a repository to store your snapshots.',
|
||||
}),
|
||||
},
|
||||
}}
|
||||
actions={
|
||||
<EuiButton
|
||||
href={linkToAddRepository(currentUrl)}
|
||||
color="danger"
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="addRepositoryButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.addRepositoryButtonLabel"
|
||||
defaultMessage="Register a repository"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
if (!policy.repository) {
|
||||
updatePolicy({
|
||||
repository: repositories[0].name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiSelect
|
||||
options={repositories.map(({ name }: Repository) => ({
|
||||
value: name,
|
||||
text: name,
|
||||
}))}
|
||||
value={policy.repository || repositories[0].name}
|
||||
onBlur={() => setTouched({ ...touched, repository: true })}
|
||||
onChange={e => {
|
||||
updatePolicy({
|
||||
repository: e.target.value,
|
||||
});
|
||||
}}
|
||||
fullWidth
|
||||
data-test-subj="repositorySelect"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSnapshotNameField = () => (
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.snapshotNameDescriptionTitle"
|
||||
defaultMessage="Snapshot name"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.snapshotNameDescription"
|
||||
defaultMessage="The name for the snapshots. A unique identifier is automatically added to each name."
|
||||
/>
|
||||
}
|
||||
idAria="policySnapshotNameDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.policySnapshotNameLabel"
|
||||
defaultMessage="Snapshot name"
|
||||
/>
|
||||
}
|
||||
describedByIds={['policySnapshotNameDescription']}
|
||||
isInvalid={touched.snapshotName && Boolean(errors.snapshotName)}
|
||||
error={errors.snapshotName}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.policySnapshotNameHelpText"
|
||||
defaultMessage="Supports date math expressions. {docLink}"
|
||||
values={{
|
||||
docLink: (
|
||||
<EuiLink
|
||||
href={documentationLinksService.getDateMathIndexNamesUrl()}
|
||||
target="_blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.policySnapshotNameHelpTextDocLink"
|
||||
defaultMessage="Learn more"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={policy.snapshotName}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updatePolicy({
|
||||
snapshotName: e.target.value.toLowerCase(),
|
||||
});
|
||||
}}
|
||||
onBlur={() => setTouched({ ...touched, snapshotName: true })}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.snapshotRestore.policyForm.stepLogistics.policySnapshotNamePlaceholder',
|
||||
{
|
||||
defaultMessage: '<daily-snap-\\{now/d\\}>',
|
||||
description:
|
||||
'Example date math snapshot name. Keeping the same syntax is important: <SOME-TRANSLATION-{now/d}>',
|
||||
}
|
||||
)}
|
||||
data-test-subj="snapshotNameInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
|
||||
const renderScheduleField = () => (
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.scheduleDescriptionTitle"
|
||||
defaultMessage="Schedule"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.scheduleDescription"
|
||||
defaultMessage="The frequency at which to take the snapshots."
|
||||
/>
|
||||
}
|
||||
idAria="policyScheduleDescription"
|
||||
fullWidth
|
||||
>
|
||||
{isAdvancedCronVisible ? (
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.policyScheduleLabel"
|
||||
defaultMessage="Schedule"
|
||||
/>
|
||||
}
|
||||
describedByIds={['policyScheduleDescription']}
|
||||
isInvalid={touched.schedule && Boolean(errors.schedule)}
|
||||
error={errors.schedule}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.policyScheduleHelpText"
|
||||
defaultMessage="Use cron expression. {docLink}"
|
||||
values={{
|
||||
docLink: (
|
||||
<EuiLink href={documentationLinksService.getCronUrl()} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.policyScheduleHelpTextDocLink"
|
||||
defaultMessage="Learn more"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={policy.schedule}
|
||||
fullWidth
|
||||
onChange={e => {
|
||||
updatePolicy({
|
||||
schedule: e.target.value,
|
||||
});
|
||||
}}
|
||||
onBlur={() => setTouched({ ...touched, schedule: true })}
|
||||
placeholder={DEFAULT_POLICY_SCHEDULE}
|
||||
data-test-subj="snapshotNameInput"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiText size="s">
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
setIsAdvancedCronVisible(false);
|
||||
updatePolicy({
|
||||
schedule: simpleCron.expression,
|
||||
});
|
||||
}}
|
||||
data-test-subj="showBasicCronLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.policyScheduleButtonBasicLabel"
|
||||
defaultMessage="Create basic interval"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<CronEditor
|
||||
fieldToPreferredValueMap={fieldToPreferredValueMap}
|
||||
cronExpression={simpleCron.expression}
|
||||
frequency={simpleCron.frequency}
|
||||
onChange={({
|
||||
cronExpression: expression,
|
||||
frequency,
|
||||
fieldToPreferredValueMap: newFieldToPreferredValueMap,
|
||||
}: {
|
||||
cronExpression: string;
|
||||
frequency: string;
|
||||
fieldToPreferredValueMap: any;
|
||||
}) => {
|
||||
setSimpleCron({
|
||||
expression,
|
||||
frequency,
|
||||
});
|
||||
setFieldToPreferredValueMap(newFieldToPreferredValueMap);
|
||||
updatePolicy({
|
||||
schedule: expression,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<EuiText size="s">
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
setIsAdvancedCronVisible(true);
|
||||
}}
|
||||
data-test-subj="showAdvancedCronLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.policyScheduleButtonAdvancedLabel"
|
||||
defaultMessage="Create cron expression"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{/* Step title and doc link */}
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogisticsTitle"
|
||||
defaultMessage="Logistics"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
flush="right"
|
||||
href={documentationLinksService.getSlmUrl()}
|
||||
target="_blank"
|
||||
iconType="help"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepLogistics.docsButtonLabel"
|
||||
defaultMessage="Logistics docs"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
{renderNameField()}
|
||||
{renderSnapshotNameField()}
|
||||
{renderRepositoryField()}
|
||||
{renderScheduleField()}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,329 @@
|
|||
/*
|
||||
* 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, useState } from 'react';
|
||||
import {
|
||||
EuiCodeBlock,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
EuiSpacer,
|
||||
EuiTabbedContent,
|
||||
EuiTitle,
|
||||
EuiLink,
|
||||
EuiIcon,
|
||||
EuiToolTip,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { serializePolicy } from '../../../../../common/lib';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { StepProps } from './';
|
||||
|
||||
export const PolicyStepReview: React.FunctionComponent<StepProps> = ({
|
||||
policy,
|
||||
updateCurrentStep,
|
||||
}) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
const { FormattedMessage } = i18n;
|
||||
const { name, snapshotName, schedule, repository, config } = policy;
|
||||
const { indices, includeGlobalState, ignoreUnavailable, partial } = config || {
|
||||
indices: undefined,
|
||||
includeGlobalState: undefined,
|
||||
ignoreUnavailable: undefined,
|
||||
partial: undefined,
|
||||
};
|
||||
|
||||
const [isShowingFullIndicesList, setIsShowingFullIndicesList] = useState<boolean>(false);
|
||||
const displayIndices = indices
|
||||
? typeof indices === 'string'
|
||||
? indices.split(',')
|
||||
: indices
|
||||
: undefined;
|
||||
const hiddenIndicesCount =
|
||||
displayIndices && displayIndices.length > 10 ? displayIndices.length - 10 : 0;
|
||||
|
||||
const renderSummaryTab = () => (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.sectionLogisticsTitle"
|
||||
defaultMessage="Logistics"
|
||||
/>{' '}
|
||||
<EuiToolTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.editStepTooltip"
|
||||
defaultMessage="Edit"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiLink onClick={() => updateCurrentStep(1)}>
|
||||
<EuiIcon type="pencil" />
|
||||
</EuiLink>
|
||||
</EuiToolTip>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.nameLabel"
|
||||
defaultMessage="Policy name"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>{name}</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.snapshotNameLabel"
|
||||
defaultMessage="Snapshot name"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>{snapshotName}</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.repositoryLabel"
|
||||
defaultMessage="Repository"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>{repository}</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.scheduleLabel"
|
||||
defaultMessage="Schedule"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>{schedule}</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.sectionSettingsTitle"
|
||||
defaultMessage="Snapshot settings"
|
||||
/>{' '}
|
||||
<EuiToolTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.editStepTooltip"
|
||||
defaultMessage="Edit"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiLink onClick={() => updateCurrentStep(2)}>
|
||||
<EuiIcon type="pencil" />
|
||||
</EuiLink>
|
||||
</EuiToolTip>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.indicesLabel"
|
||||
defaultMessage="Indices"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
{displayIndices ? (
|
||||
<EuiText>
|
||||
<ul>
|
||||
{(isShowingFullIndicesList
|
||||
? displayIndices
|
||||
: [...displayIndices].splice(0, 10)
|
||||
).map(index => (
|
||||
<li key={index}>
|
||||
<EuiTitle size="xs">
|
||||
<span>{index}</span>
|
||||
</EuiTitle>
|
||||
</li>
|
||||
))}
|
||||
{hiddenIndicesCount ? (
|
||||
<li key="hiddenIndicesCount">
|
||||
<EuiTitle size="xs">
|
||||
{isShowingFullIndicesList ? (
|
||||
<EuiLink onClick={() => setIsShowingFullIndicesList(false)}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.indicesCollapseAllLink"
|
||||
defaultMessage="Hide {count, plural, one {# index} other {# indices}}"
|
||||
values={{ count: hiddenIndicesCount }}
|
||||
/>{' '}
|
||||
<EuiIcon type="arrowUp" />
|
||||
</EuiLink>
|
||||
) : (
|
||||
<EuiLink onClick={() => setIsShowingFullIndicesList(true)}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.indicesShowAllLink"
|
||||
defaultMessage="Show {count} more {count, plural, one {index} other {indices}}"
|
||||
values={{ count: hiddenIndicesCount }}
|
||||
/>{' '}
|
||||
<EuiIcon type="arrowDown" />
|
||||
</EuiLink>
|
||||
)}
|
||||
</EuiTitle>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
</EuiText>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.allIndicesValue"
|
||||
defaultMessage="All indices"
|
||||
/>
|
||||
)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.ignoreUnavailableLabel"
|
||||
defaultMessage="Ignore unavailable indices"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
{ignoreUnavailable ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.ignoreUnavailableTrueLabel"
|
||||
defaultMessage="Yes"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.ignoreUnavailableFalseLabel"
|
||||
defaultMessage="No"
|
||||
/>
|
||||
)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialLabel"
|
||||
defaultMessage="Allow partial shards"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
{partial ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialTrueLabel"
|
||||
defaultMessage="Yes"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.partialFalseLabel"
|
||||
defaultMessage="No"
|
||||
/>
|
||||
)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiDescriptionListTitle>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateLabel"
|
||||
defaultMessage="Include global state"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
{includeGlobalState === false ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateFalseLabel"
|
||||
defaultMessage="No"
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReview.summaryTab.includeGlobalStateTrueLabel"
|
||||
defaultMessage="Yes"
|
||||
/>
|
||||
)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
const renderRequestTab = () => {
|
||||
const endpoint = `PUT _slm/policy/${name}`;
|
||||
const json = JSON.stringify(serializePolicy(policy), null, 2);
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCodeBlock language="json" isCopyable>
|
||||
{`${endpoint}\n${json}`}
|
||||
</EuiCodeBlock>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiTitle>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepReviewTitle"
|
||||
defaultMessage="Review policy"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiTabbedContent
|
||||
tabs={[
|
||||
{
|
||||
id: 'summary',
|
||||
name: i18n.translate('xpack.snapshotRestore.policyForm.stepReview.summaryTabTitle', {
|
||||
defaultMessage: 'Summary',
|
||||
}),
|
||||
content: renderSummaryTab(),
|
||||
},
|
||||
{
|
||||
id: 'json',
|
||||
name: i18n.translate('xpack.snapshotRestore.policyForm.stepReview.requestTabTitle', {
|
||||
defaultMessage: 'Request',
|
||||
}),
|
||||
content: renderRequestTab(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,454 @@
|
|||
/*
|
||||
* 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, useState } from 'react';
|
||||
|
||||
import {
|
||||
EuiDescribedFormGroup,
|
||||
EuiTitle,
|
||||
EuiFormRow,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiLink,
|
||||
EuiSelectable,
|
||||
EuiPanel,
|
||||
EuiComboBox,
|
||||
} from '@elastic/eui';
|
||||
import { Option } from '@elastic/eui/src/components/selectable/types';
|
||||
import { SlmPolicyPayload, SnapshotConfig } from '../../../../../common/types';
|
||||
import { documentationLinksService } from '../../../services/documentation';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { StepProps } from './';
|
||||
|
||||
export const PolicyStepSettings: React.FunctionComponent<StepProps> = ({
|
||||
policy,
|
||||
indices,
|
||||
updatePolicy,
|
||||
errors,
|
||||
}) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
const { FormattedMessage } = i18n;
|
||||
const { config = {} } = policy;
|
||||
|
||||
const updatePolicyConfig = (updatedFields: Partial<SlmPolicyPayload['config']>): void => {
|
||||
const newConfig = { ...config, ...updatedFields };
|
||||
updatePolicy({
|
||||
config: newConfig,
|
||||
});
|
||||
};
|
||||
|
||||
// States for choosing all indices, or a subset, including caching previously chosen subset list
|
||||
const [isAllIndices, setIsAllIndices] = useState<boolean>(!Boolean(config.indices));
|
||||
const [indicesSelection, setIndicesSelection] = useState<SnapshotConfig['indices']>([...indices]);
|
||||
const [indicesOptions, setIndicesOptions] = useState<Option[]>(
|
||||
indices.map(
|
||||
(index): Option => ({
|
||||
label: index,
|
||||
checked:
|
||||
isAllIndices ||
|
||||
// If indices is a string, we default to custom input mode, so we mark individual indices
|
||||
// as selected if user goes back to list mode
|
||||
typeof config.indices === 'string' ||
|
||||
(Array.isArray(config.indices) && config.indices.includes(index))
|
||||
? 'on'
|
||||
: undefined,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// State for using selectable indices list or custom patterns
|
||||
// Users with more than 100 indices will probably want to use an index pattern to select
|
||||
// them instead, so we'll default to showing them the index pattern input.
|
||||
const [selectIndicesMode, setSelectIndicesMode] = useState<'list' | 'custom'>(
|
||||
typeof config.indices === 'string' ||
|
||||
(Array.isArray(config.indices) && config.indices.length > 100)
|
||||
? 'custom'
|
||||
: 'list'
|
||||
);
|
||||
|
||||
// State for custom patterns
|
||||
const [indexPatterns, setIndexPatterns] = useState<string[]>(
|
||||
typeof config.indices === 'string' ? config.indices.split(',') : []
|
||||
);
|
||||
|
||||
const renderIndicesField = () => (
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.indicesTitle"
|
||||
defaultMessage="Indices"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.indicesDescription"
|
||||
defaultMessage="Indices to back up."
|
||||
/>
|
||||
}
|
||||
idAria="indicesDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow hasEmptyLabelSpace fullWidth describedByIds={['indicesDescription']}>
|
||||
<Fragment>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.allIndicesLabel"
|
||||
defaultMessage="All indices, including system indices"
|
||||
/>
|
||||
}
|
||||
checked={isAllIndices}
|
||||
onChange={e => {
|
||||
const isChecked = e.target.checked;
|
||||
setIsAllIndices(isChecked);
|
||||
if (isChecked) {
|
||||
updatePolicyConfig({ indices: undefined });
|
||||
} else {
|
||||
updatePolicyConfig({
|
||||
indices:
|
||||
selectIndicesMode === 'custom'
|
||||
? indexPatterns.join(',')
|
||||
: [...(indicesSelection || [])],
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{isAllIndices ? null : (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFormRow
|
||||
className="snapshotRestore__policyForm__stepSettings__indicesFieldWrapper"
|
||||
label={
|
||||
selectIndicesMode === 'list' ? (
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.selectIndicesLabel"
|
||||
defaultMessage="Select indices"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
setSelectIndicesMode('custom');
|
||||
updatePolicyConfig({ indices: indexPatterns.join(',') });
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.indicesToggleCustomLink"
|
||||
defaultMessage="Use index patterns"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.indicesPatternLabel"
|
||||
defaultMessage="Index patterns"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
setSelectIndicesMode('list');
|
||||
updatePolicyConfig({ indices: indicesSelection });
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.indicesToggleListLink"
|
||||
defaultMessage="Select indices"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)
|
||||
}
|
||||
helpText={
|
||||
selectIndicesMode === 'list' ? (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.selectIndicesHelpText"
|
||||
defaultMessage="{count} {count, plural, one {index} other {indices}} will be backed up. {selectOrDeselectAllLink}"
|
||||
values={{
|
||||
count: config.indices && config.indices.length,
|
||||
selectOrDeselectAllLink:
|
||||
config.indices && config.indices.length > 0 ? (
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
// TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
|
||||
indicesOptions.forEach((option: Option) => {
|
||||
option.checked = undefined;
|
||||
});
|
||||
updatePolicyConfig({ indices: [] });
|
||||
setIndicesSelection([]);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.deselectAllIndicesLink"
|
||||
defaultMessage="Deselect all"
|
||||
/>
|
||||
</EuiLink>
|
||||
) : (
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
// TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
|
||||
indicesOptions.forEach((option: Option) => {
|
||||
option.checked = 'on';
|
||||
});
|
||||
updatePolicyConfig({ indices: [...indices] });
|
||||
setIndicesSelection([...indices]);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.selectAllIndicesLink"
|
||||
defaultMessage="Select all"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
isInvalid={Boolean(errors.indices)}
|
||||
error={errors.indices}
|
||||
>
|
||||
{selectIndicesMode === 'list' ? (
|
||||
<EuiSelectable
|
||||
allowExclusions={false}
|
||||
options={indicesOptions}
|
||||
onChange={options => {
|
||||
const newSelectedIndices: string[] = [];
|
||||
options.forEach(({ label, checked }) => {
|
||||
if (checked === 'on') {
|
||||
newSelectedIndices.push(label);
|
||||
}
|
||||
});
|
||||
setIndicesOptions(options);
|
||||
updatePolicyConfig({ indices: newSelectedIndices });
|
||||
setIndicesSelection(newSelectedIndices);
|
||||
}}
|
||||
searchable
|
||||
height={300}
|
||||
>
|
||||
{(list, search) => (
|
||||
<EuiPanel paddingSize="s" hasShadow={false}>
|
||||
{search}
|
||||
{list}
|
||||
</EuiPanel>
|
||||
)}
|
||||
</EuiSelectable>
|
||||
) : (
|
||||
<EuiComboBox
|
||||
options={indices.map(index => ({ label: index }))}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.snapshotRestore.policyForm.stepSettings.indicesPatternPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Enter index patterns, i.e. logstash-*',
|
||||
}
|
||||
)}
|
||||
selectedOptions={indexPatterns.map(pattern => ({ label: pattern }))}
|
||||
onCreateOption={(pattern: string) => {
|
||||
if (!pattern.trim().length) {
|
||||
return;
|
||||
}
|
||||
const newPatterns = [...indexPatterns, pattern];
|
||||
setIndexPatterns(newPatterns);
|
||||
updatePolicyConfig({
|
||||
indices: newPatterns.join(','),
|
||||
});
|
||||
}}
|
||||
onChange={(patterns: Array<{ label: string }>) => {
|
||||
const newPatterns = patterns.map(({ label }) => label);
|
||||
setIndexPatterns(newPatterns);
|
||||
updatePolicyConfig({
|
||||
indices: newPatterns.join(','),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
|
||||
const renderIgnoreUnavailableField = () => (
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableDescriptionTitle"
|
||||
defaultMessage="Ignore unavailable indices"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableDescription"
|
||||
defaultMessage="Ignores indices that are unavailable when taking the snapshot. Otherwise, the entire snapshot will fail."
|
||||
/>
|
||||
}
|
||||
idAria="policyIgnoreUnavailableDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace
|
||||
describedByIds={['policyIgnoreUnavailableDescription']}
|
||||
fullWidth
|
||||
>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.ignoreUnavailableLabel"
|
||||
defaultMessage="Ignore unavailable indices"
|
||||
/>
|
||||
}
|
||||
checked={Boolean(config.ignoreUnavailable)}
|
||||
onChange={e => {
|
||||
updatePolicyConfig({
|
||||
ignoreUnavailable: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
|
||||
const renderPartialField = () => (
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.partialDescriptionTitle"
|
||||
defaultMessage="Allow partial indices"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.partialDescription"
|
||||
defaultMessage="Allows snapshots of indices with primary shards that are unavailable. Otherwise, the entire snapshot will fail."
|
||||
/>
|
||||
}
|
||||
idAria="policyPartialDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow hasEmptyLabelSpace describedByIds={['policyPartialDescription']} fullWidth>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.partialLabel"
|
||||
defaultMessage="Allow partial indices"
|
||||
/>
|
||||
}
|
||||
checked={Boolean(config.partial)}
|
||||
onChange={e => {
|
||||
updatePolicyConfig({
|
||||
partial: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
|
||||
const renderIncludeGlobalStateField = () => (
|
||||
<EuiDescribedFormGroup
|
||||
title={
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescriptionTitle"
|
||||
defaultMessage="Include global state"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.includeGlobalStateDescription"
|
||||
defaultMessage="Stores the global state of the cluster as part of the snapshot."
|
||||
/>
|
||||
}
|
||||
idAria="policyIncludeGlobalStateDescription"
|
||||
fullWidth
|
||||
>
|
||||
<EuiFormRow
|
||||
hasEmptyLabelSpace
|
||||
describedByIds={['policyIncludeGlobalStateDescription']}
|
||||
fullWidth
|
||||
>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.policyIncludeGlobalStateLabel"
|
||||
defaultMessage="Include global state"
|
||||
/>
|
||||
}
|
||||
checked={config.includeGlobalState === undefined || config.includeGlobalState}
|
||||
onChange={e => {
|
||||
updatePolicyConfig({
|
||||
includeGlobalState: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
return (
|
||||
<div className="snapshotRestore__policyForm__stepSettings">
|
||||
{/* Step title and doc link */}
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettingsTitle"
|
||||
defaultMessage="Snapshot settings"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
flush="right"
|
||||
href={documentationLinksService.getSnapshotDocUrl()}
|
||||
target="_blank"
|
||||
iconType="help"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyForm.stepSettings.docsButtonLabel"
|
||||
defaultMessage="Snapshot settings docs"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
{renderIndicesField()}
|
||||
{renderIgnoreUnavailableField()}
|
||||
{renderPartialField()}
|
||||
{renderIncludeGlobalStateField()}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -155,7 +155,8 @@ export const RepositoryDeleteProvider: React.FunctionComponent<Props> = ({ child
|
|||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.deleteRepository.confirmModal.deleteSingleDescription"
|
||||
defaultMessage="The snapshots in this repository will still exist, but Elasticsearch won’t have access to them."
|
||||
defaultMessage="The snapshots in this repository will still exist, but Elasticsearch won’t have access to them.
|
||||
Adjust policies that use this repository to prevent scheduled snapshots from failing."
|
||||
/>
|
||||
</p>
|
||||
) : (
|
||||
|
@ -174,7 +175,8 @@ export const RepositoryDeleteProvider: React.FunctionComponent<Props> = ({ child
|
|||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.deleteRepository.confirmModal.deleteMultipleDescription"
|
||||
defaultMessage="The snapshots in these repositories will still exist, but Elasticsearch won't have access to them."
|
||||
defaultMessage="The snapshots in these repositories will still exist, but Elasticsearch won't have access to them.
|
||||
Adjust policies that use these repositories to prevent scheduled snapshots from failing."
|
||||
/>
|
||||
</p>
|
||||
</Fragment>
|
||||
|
|
|
@ -56,6 +56,8 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
|
|||
label: index,
|
||||
checked:
|
||||
isAllIndices ||
|
||||
// If indices is a string, we default to custom input mode, so we mark individual indices
|
||||
// as selected if user goes back to list mode
|
||||
typeof restoreIndices === 'string' ||
|
||||
(Array.isArray(restoreIndices) && restoreIndices.includes(index))
|
||||
? 'on'
|
||||
|
@ -97,7 +99,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
|
|||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogisticsTitle"
|
||||
defaultMessage="Logistics"
|
||||
defaultMessage="Restore details"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
|
@ -113,7 +115,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
|
|||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.restoreForm.stepLogistics.docsButtonLabel"
|
||||
defaultMessage="Logistics docs"
|
||||
defaultMessage="Snapshot and Restore docs"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
@ -234,6 +236,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
|
|||
restoreIndices && restoreIndices.length > 0 ? (
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
// TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
|
||||
indicesOptions.forEach((option: Option) => {
|
||||
option.checked = undefined;
|
||||
});
|
||||
|
@ -252,6 +255,7 @@ export const RestoreSnapshotStepLogistics: React.FunctionComponent<StepProps> =
|
|||
) : (
|
||||
<EuiLink
|
||||
onClick={() => {
|
||||
// TODO: Change this to setIndicesOptions() when https://github.com/elastic/eui/issues/2071 is fixed
|
||||
indicesOptions.forEach((option: Option) => {
|
||||
option.checked = 'on';
|
||||
});
|
||||
|
|
|
@ -16,9 +16,15 @@ interface Props {
|
|||
message?: string;
|
||||
};
|
||||
};
|
||||
actions?: JSX.Element;
|
||||
}
|
||||
|
||||
export const SectionError: React.FunctionComponent<Props> = ({ title, error, ...rest }) => {
|
||||
export const SectionError: React.FunctionComponent<Props> = ({
|
||||
title,
|
||||
error,
|
||||
actions,
|
||||
...rest
|
||||
}) => {
|
||||
const {
|
||||
error: errorString,
|
||||
cause, // wrapEsError() on the server adds a "cause" array
|
||||
|
@ -27,10 +33,10 @@ export const SectionError: React.FunctionComponent<Props> = ({ title, error, ...
|
|||
|
||||
return (
|
||||
<EuiCallOut title={title} color="danger" iconType="alert" {...rest}>
|
||||
<div>{message || errorString}</div>
|
||||
{cause ? message || errorString : <p>{message || errorString}</p>}
|
||||
{cause && (
|
||||
<Fragment>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSpacer size="s" />
|
||||
<ul>
|
||||
{cause.map((causeMsg, i) => (
|
||||
<li key={i}>{causeMsg}</li>
|
||||
|
@ -38,6 +44,7 @@ export const SectionError: React.FunctionComponent<Props> = ({ title, error, ...
|
|||
</ul>
|
||||
</Fragment>
|
||||
)}
|
||||
{actions ? actions : null}
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,13 +6,37 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { EuiEmptyPrompt, EuiLoadingSpinner, EuiText } from '@elastic/eui';
|
||||
import {
|
||||
EuiEmptyPrompt,
|
||||
EuiLoadingSpinner,
|
||||
EuiText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
inline?: boolean;
|
||||
children: React.ReactNode;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const SectionLoading: React.FunctionComponent<Props> = ({ children }) => {
|
||||
export const SectionLoading: React.FunctionComponent<Props> = ({ inline, children, ...rest }) => {
|
||||
if (inline) {
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="flexStart" alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="m" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText {...rest}>
|
||||
<EuiTextColor color="subdued">{children}</EuiTextColor>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={<EuiLoadingSpinner size="xl" />}
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { DAY } from '../../shared_imports';
|
||||
|
||||
export const BASE_PATH = '/management/elasticsearch/snapshot_restore';
|
||||
export const DEFAULT_SECTION: Section = 'snapshots';
|
||||
export type Section = 'repositories' | 'snapshots' | 'restore_status' | 'policies';
|
||||
|
@ -86,6 +88,9 @@ export const REMOVE_INDEX_SETTINGS_SUGGESTIONS: string[] = INDEX_SETTING_SUGGEST
|
|||
setting => !UNREMOVABLE_INDEX_SETTINGS.includes(setting)
|
||||
);
|
||||
|
||||
export const DEFAULT_POLICY_SCHEDULE = '0 30 1 * * ?';
|
||||
export const DEFAULT_POLICY_FREQUENCY = DAY;
|
||||
|
||||
// UI Metric constants
|
||||
export const UIM_APP_NAME = 'snapshot_restore';
|
||||
export const UIM_REPOSITORY_LIST_LOAD = 'repository_list_load';
|
||||
|
@ -112,3 +117,5 @@ export const UIM_POLICY_DETAIL_PANEL_HISTORY_TAB = 'policy_detail_panel_last_suc
|
|||
export const UIM_POLICY_EXECUTE = 'policy_execute';
|
||||
export const UIM_POLICY_DELETE = 'policy_delete';
|
||||
export const UIM_POLICY_DELETE_MANY = 'policy_delete_many';
|
||||
export const UIM_POLICY_CREATE = 'policy_create';
|
||||
export const UIM_POLICY_UPDATE = 'policy_update';
|
||||
|
|
|
@ -11,4 +11,5 @@
|
|||
// snapshotRestore__legend-isLoading
|
||||
|
||||
@import 'components/restore_snapshot_form/restore_snapshot_form';
|
||||
@import 'components/policy_form/policy_form';
|
||||
@import 'sections/home/home';
|
||||
|
|
|
@ -18,4 +18,14 @@
|
|||
background: $euiColorLightestShade;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 1. Make in progress snapshot loading indicator be centered vertically
|
||||
* when it is inside tooltip wrapper
|
||||
*/
|
||||
.snapshotRestore__policyTable {
|
||||
.euiToolTipAnchor {
|
||||
display: flex;
|
||||
}
|
||||
}
|
|
@ -22,7 +22,7 @@ import {
|
|||
|
||||
import { BASE_PATH, Section } from '../../constants';
|
||||
import { useAppDependencies } from '../../index';
|
||||
import { breadcrumbService } from '../../services/navigation';
|
||||
import { breadcrumbService, docTitleService } from '../../services/navigation';
|
||||
|
||||
import { RepositoryList } from './repository_list';
|
||||
import { SnapshotList } from './snapshot_list';
|
||||
|
@ -92,10 +92,11 @@ export const SnapshotRestoreHome: React.FunctionComponent<RouteComponentProps<Ma
|
|||
history.push(`${BASE_PATH}/${newSection}`);
|
||||
};
|
||||
|
||||
// Set breadcrumb
|
||||
// Set breadcrumb and page title
|
||||
useEffect(() => {
|
||||
breadcrumbService.setBreadcrumbs('home');
|
||||
}, []);
|
||||
breadcrumbService.setBreadcrumbs(section || 'home');
|
||||
docTitleService.setTitle(section || 'home');
|
||||
}, [section]);
|
||||
|
||||
return (
|
||||
<EuiPageBody>
|
||||
|
|
|
@ -16,6 +16,10 @@ import {
|
|||
EuiTabs,
|
||||
EuiTab,
|
||||
EuiButton,
|
||||
EuiPopover,
|
||||
EuiContextMenu,
|
||||
EuiButtonIcon,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { SlmPolicy } from '../../../../../../common/types';
|
||||
|
@ -26,6 +30,7 @@ import {
|
|||
} from '../../../../constants';
|
||||
import { useLoadPolicy } from '../../../../services/http';
|
||||
import { uiMetricService } from '../../../../services/ui_metric';
|
||||
import { linkToEditPolicy, linkToSnapshot } from '../../../../services/navigation';
|
||||
|
||||
import {
|
||||
SectionError,
|
||||
|
@ -64,6 +69,7 @@ export const PolicyDetails: React.FunctionComponent<Props> = ({
|
|||
const { trackUiMetric } = uiMetricService;
|
||||
const { error, data: policyDetails, sendRequest: reload } = useLoadPolicy(policyName);
|
||||
const [activeTab, setActiveTab] = useState<string>(TAB_SUMMARY);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
// Reset tab when we look at a different policy
|
||||
useEffect(() => {
|
||||
|
@ -183,53 +189,103 @@ export const PolicyDetails: React.FunctionComponent<Props> = ({
|
|||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
||||
{policyDetails ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<PolicyDeleteProvider>
|
||||
{deletePolicyPrompt => {
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
color="danger"
|
||||
data-test-subj="srPolicyDetailsDeleteActionButton"
|
||||
onClick={() => deletePolicyPrompt([policyName], onPolicyDeleted)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyDetails.deleteButtonLabel"
|
||||
defaultMessage="Delete"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}}
|
||||
</PolicyDeleteProvider>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<PolicyExecuteProvider>
|
||||
{executePolicyPrompt => {
|
||||
return (
|
||||
<EuiButton
|
||||
onClick={() =>
|
||||
executePolicyPrompt(policyName, () => {
|
||||
onPolicyExecuted();
|
||||
reload();
|
||||
})
|
||||
}
|
||||
fill
|
||||
color="primary"
|
||||
data-test-subj="srPolicyDetailsExecuteActionButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyDetails.executeButtonLabel"
|
||||
defaultMessage="Run policy"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
}}
|
||||
</PolicyExecuteProvider>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<PolicyExecuteProvider>
|
||||
{executePolicyPrompt => {
|
||||
return (
|
||||
<PolicyDeleteProvider>
|
||||
{deletePolicyPrompt => {
|
||||
return (
|
||||
<EuiPopover
|
||||
id="policyActionMenu"
|
||||
button={
|
||||
<EuiButton
|
||||
data-test-subj="policyActionMenuButton"
|
||||
iconSide="right"
|
||||
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
|
||||
iconType="arrowDown"
|
||||
fill
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyDetails.manageButtonLabel"
|
||||
defaultMessage="Manage policy"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setIsPopoverOpen(false)}
|
||||
panelPaddingSize="none"
|
||||
withTitle
|
||||
anchorPosition="rightUp"
|
||||
repositionOnScroll
|
||||
>
|
||||
<EuiContextMenu
|
||||
data-test-subj="policyActionContextMenu"
|
||||
initialPanelId={0}
|
||||
panels={[
|
||||
{
|
||||
id: 0,
|
||||
title: i18n.translate(
|
||||
'xpack.snapshotRestore.policyDetails.managePanelTitle',
|
||||
{
|
||||
defaultMessage: 'Policy options',
|
||||
}
|
||||
),
|
||||
items: [
|
||||
{
|
||||
name: i18n.translate(
|
||||
'xpack.snapshotRestore.policyDetails.executeButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Run now',
|
||||
}
|
||||
),
|
||||
icon: 'play',
|
||||
onClick: () => {
|
||||
executePolicyPrompt(policyName, () =>
|
||||
// Wait a little bit for policy to execute before reloading policy table
|
||||
// and policy details so that History tab information is updated with
|
||||
// results of the execution
|
||||
setTimeout(() => {
|
||||
onPolicyExecuted();
|
||||
reload();
|
||||
}, 2000)
|
||||
);
|
||||
},
|
||||
disabled: Boolean(policyDetails.policy.inProgress),
|
||||
},
|
||||
{
|
||||
name: i18n.translate(
|
||||
'xpack.snapshotRestore.policyDetails.editButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Edit',
|
||||
}
|
||||
),
|
||||
icon: 'pencil',
|
||||
href: linkToEditPolicy(policyName),
|
||||
},
|
||||
{
|
||||
name: i18n.translate(
|
||||
'xpack.snapshotRestore.policyDetails.deleteButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Delete',
|
||||
}
|
||||
),
|
||||
icon: 'trash',
|
||||
onClick: () =>
|
||||
deletePolicyPrompt([policyName], onPolicyDeleted),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}}
|
||||
</PolicyDeleteProvider>
|
||||
);
|
||||
}}
|
||||
</PolicyExecuteProvider>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
|
@ -245,11 +301,49 @@ export const PolicyDetails: React.FunctionComponent<Props> = ({
|
|||
maxWidth={550}
|
||||
>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="m">
|
||||
<h2 id="srPolicyDetailsFlyoutTitle" data-test-subj="title">
|
||||
{policyName}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="m">
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<h2 id="srPolicyDetailsFlyoutTitle" data-test-subj="title">
|
||||
{policyName}
|
||||
</h2>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType="refresh"
|
||||
color="subdued"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.snapshotRestore.policyDetails.reloadButtonAriaLabel',
|
||||
{ defaultMessage: 'Reload' }
|
||||
)}
|
||||
onClick={() => reload()}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{policyDetails && policyDetails.policy && policyDetails.policy.inProgress ? (
|
||||
<EuiFlexItem>
|
||||
<SectionLoading inline={true} size="s">
|
||||
<EuiLink
|
||||
href={linkToSnapshot(
|
||||
policyDetails.policy.repository,
|
||||
policyDetails.policy.inProgress.snapshotName
|
||||
)}
|
||||
data-test-subj="inProgressSnapshotLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyDetails.inProgressSnapshotLinkText"
|
||||
defaultMessage="'{snapshotName}' in progress"
|
||||
values={{ snapshotName: policyDetails.policy.inProgress.snapshotName }}
|
||||
/>
|
||||
</EuiLink>
|
||||
</SectionLoading>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
{renderTabs()}
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
|
|
|
@ -55,12 +55,11 @@ export const TabHistory: React.FunctionComponent<Props> = ({ policy }) => {
|
|||
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem data-test-subj="successTime">
|
||||
<EuiFlexItem data-test-subj="successDate">
|
||||
<EuiDescriptionListTitle data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyDetails.lastSuccess.timeLabel"
|
||||
defaultMessage="Succeeded on"
|
||||
description="Title for date time. Example: Succeeded on Jul 16, 2019 6:30 PM PDT"
|
||||
id="xpack.snapshotRestore.policyDetails.lastSuccess.dateLabel"
|
||||
defaultMessage="Date"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
|
@ -107,12 +106,11 @@ export const TabHistory: React.FunctionComponent<Props> = ({ policy }) => {
|
|||
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem data-test-subj="failureTime">
|
||||
<EuiFlexItem data-test-subj="failureDate">
|
||||
<EuiDescriptionListTitle data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyDetails.lastFailure.timeLabel"
|
||||
defaultMessage="Failed on"
|
||||
description="Title for date time. Example: Failed on Jul 16, 2019 6:30 PM PDT"
|
||||
id="xpack.snapshotRestore.policyDetails.lastFailure.dateLabel"
|
||||
defaultMessage="Date"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
|
@ -140,7 +138,7 @@ export const TabHistory: React.FunctionComponent<Props> = ({ policy }) => {
|
|||
<EuiDescriptionListTitle data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyDetails.lastFailure.detailsLabel"
|
||||
defaultMessage="Failure details"
|
||||
defaultMessage="Details"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
<EuiSpacer size="s" />
|
||||
|
@ -154,13 +152,13 @@ export const TabHistory: React.FunctionComponent<Props> = ({ policy }) => {
|
|||
setOptions={{
|
||||
showLineNumbers: false,
|
||||
tabSize: 2,
|
||||
maxLines: Infinity,
|
||||
}}
|
||||
editorProps={{
|
||||
$blockScrolling: Infinity,
|
||||
}}
|
||||
minLines={6}
|
||||
maxLines={6}
|
||||
maxLines={12}
|
||||
wrapEnabled={true}
|
||||
showGutter={false}
|
||||
aria-label={
|
||||
<FormattedMessage
|
||||
|
@ -191,7 +189,7 @@ export const TabHistory: React.FunctionComponent<Props> = ({ policy }) => {
|
|||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyDetails.noHistoryMessage"
|
||||
defaultMessage="This policy has not been executed yet. It will automatically run on {date} at {time}."
|
||||
defaultMessage="This policy will run on {date} at {time}."
|
||||
values={{
|
||||
date: <FormattedDateTime epochMs={nextExecutionMillis} type="date" />,
|
||||
time: <FormattedDateTime epochMs={nextExecutionMillis} type="time" />,
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* 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, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -41,36 +41,45 @@ export const TabSummary: React.FunctionComponent<Props> = ({ policy }) => {
|
|||
nextExecutionMillis,
|
||||
config,
|
||||
} = policy;
|
||||
const { includeGlobalState, ignoreUnavailable, indices, partial } = config;
|
||||
const { includeGlobalState, ignoreUnavailable, indices, partial } = config || {
|
||||
includeGlobalState: undefined,
|
||||
ignoreUnavailable: undefined,
|
||||
indices: undefined,
|
||||
partial: undefined,
|
||||
};
|
||||
|
||||
// Only show 10 indices initially
|
||||
const [isShowingFullIndicesList, setIsShowingFullIndicesList] = useState<boolean>(false);
|
||||
const hiddenIndicesCount = indices && indices.length > 10 ? indices.length - 10 : 0;
|
||||
const displayIndices = typeof indices === 'string' ? indices.split(',') : indices;
|
||||
const hiddenIndicesCount =
|
||||
displayIndices && displayIndices.length > 10 ? displayIndices.length - 10 : 0;
|
||||
const shortIndicesList =
|
||||
indices && indices.length ? (
|
||||
<ul>
|
||||
{[...indices].splice(0, 10).map((index: string) => (
|
||||
<li key={index}>
|
||||
<EuiTitle size="xs">
|
||||
<span>{index}</span>
|
||||
</EuiTitle>
|
||||
</li>
|
||||
))}
|
||||
{hiddenIndicesCount ? (
|
||||
<li key="hiddenIndicesCount">
|
||||
<EuiTitle size="xs">
|
||||
<EuiLink onClick={() => setIsShowingFullIndicesList(true)}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyDetails.indicesShowAllLink"
|
||||
defaultMessage="Show {count} more {count, plural, one {index} other {indices}}"
|
||||
values={{ count: hiddenIndicesCount }}
|
||||
/>{' '}
|
||||
<EuiIcon type="arrowDown" />
|
||||
</EuiLink>
|
||||
</EuiTitle>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
displayIndices && displayIndices.length ? (
|
||||
<EuiText size="m">
|
||||
<ul>
|
||||
{[...displayIndices].splice(0, 10).map((index: string) => (
|
||||
<li key={index}>
|
||||
<EuiTitle size="xs">
|
||||
<span>{index}</span>
|
||||
</EuiTitle>
|
||||
</li>
|
||||
))}
|
||||
{hiddenIndicesCount ? (
|
||||
<li key="hiddenIndicesCount">
|
||||
<EuiTitle size="xs">
|
||||
<EuiLink onClick={() => setIsShowingFullIndicesList(true)}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyDetails.indicesShowAllLink"
|
||||
defaultMessage="Show {count} more {count, plural, one {index} other {indices}}"
|
||||
values={{ count: hiddenIndicesCount }}
|
||||
/>{' '}
|
||||
<EuiIcon type="arrowDown" />
|
||||
</EuiLink>
|
||||
</EuiTitle>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
</EuiText>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyDetails.allIndicesLabel"
|
||||
|
@ -78,32 +87,41 @@ export const TabSummary: React.FunctionComponent<Props> = ({ policy }) => {
|
|||
/>
|
||||
);
|
||||
const fullIndicesList =
|
||||
indices && indices.length && indices.length > 10 ? (
|
||||
<ul>
|
||||
{indices.map((index: string) => (
|
||||
<li key={index}>
|
||||
<EuiTitle size="xs">
|
||||
<span>{index}</span>
|
||||
</EuiTitle>
|
||||
</li>
|
||||
))}
|
||||
{hiddenIndicesCount ? (
|
||||
<li key="hiddenIndicesCount">
|
||||
<EuiTitle size="xs">
|
||||
<EuiLink onClick={() => setIsShowingFullIndicesList(false)}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyDetails.indicesCollapseAllLink"
|
||||
defaultMessage="Hide {count, plural, one {# index} other {# indices}}"
|
||||
values={{ count: hiddenIndicesCount }}
|
||||
/>{' '}
|
||||
<EuiIcon type="arrowUp" />
|
||||
</EuiLink>
|
||||
</EuiTitle>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
displayIndices && displayIndices.length && displayIndices.length > 10 ? (
|
||||
<EuiText size="m">
|
||||
<ul>
|
||||
{displayIndices.map((index: string) => (
|
||||
<li key={index}>
|
||||
<EuiTitle size="xs">
|
||||
<span>{index}</span>
|
||||
</EuiTitle>
|
||||
</li>
|
||||
))}
|
||||
{hiddenIndicesCount ? (
|
||||
<li key="hiddenIndicesCount">
|
||||
<EuiTitle size="xs">
|
||||
<EuiLink onClick={() => setIsShowingFullIndicesList(false)}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyDetails.indicesCollapseAllLink"
|
||||
defaultMessage="Hide {count, plural, one {# index} other {# indices}}"
|
||||
values={{ count: hiddenIndicesCount }}
|
||||
/>{' '}
|
||||
<EuiIcon type="arrowUp" />
|
||||
</EuiLink>
|
||||
</EuiTitle>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
</EuiText>
|
||||
) : null;
|
||||
|
||||
// Reset indices list state when clicking through different policies
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setIsShowingFullIndicesList(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiFlexGroup>
|
||||
|
@ -180,7 +198,7 @@ export const TabSummary: React.FunctionComponent<Props> = ({ policy }) => {
|
|||
<EuiDescriptionListTitle data-test-subj="title">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyDetails.nextExecutionLabel"
|
||||
defaultMessage="Next execution"
|
||||
defaultMessage="Next snapshot"
|
||||
/>
|
||||
</EuiDescriptionListTitle>
|
||||
|
||||
|
@ -200,7 +218,7 @@ export const TabSummary: React.FunctionComponent<Props> = ({ policy }) => {
|
|||
</EuiDescriptionListTitle>
|
||||
|
||||
<EuiDescriptionListDescription className="eui-textBreakWord" data-test-subj="value">
|
||||
<EuiText>{isShowingFullIndicesList ? fullIndicesList : shortIndicesList}</EuiText>
|
||||
{isShowingFullIndicesList ? fullIndicesList : shortIndicesList}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiFlexItem>
|
||||
|
||||
|
|
|
@ -7,13 +7,16 @@
|
|||
import React, { Fragment, useEffect } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { EuiEmptyPrompt, EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui';
|
||||
import { SlmPolicy } from '../../../../../common/types';
|
||||
import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../common/constants';
|
||||
import { SectionError, SectionLoading } from '../../../components';
|
||||
import { BASE_PATH, UIM_POLICY_LIST_LOAD } from '../../../constants';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { useLoadPolicies } from '../../../services/http';
|
||||
import { uiMetricService } from '../../../services/ui_metric';
|
||||
import { linkToAddPolicy, linkToPolicy } from '../../../services/navigation';
|
||||
import { WithPrivileges, NotAuthorizedSection } from '../../../lib/authorization';
|
||||
|
||||
import { PolicyDetails } from './policy_details';
|
||||
import { PolicyTable } from './policy_table';
|
||||
|
@ -44,9 +47,7 @@ export const PolicyList: React.FunctionComponent<RouteComponentProps<MatchParams
|
|||
} = useLoadPolicies();
|
||||
|
||||
const openPolicyDetailsUrl = (newPolicyName: SlmPolicy['name']): string => {
|
||||
return history.createHref({
|
||||
pathname: `${BASE_PATH}/policies/${newPolicyName}`,
|
||||
});
|
||||
return linkToPolicy(newPolicyName);
|
||||
};
|
||||
|
||||
const closePolicyDetails = () => {
|
||||
|
@ -72,7 +73,7 @@ export const PolicyList: React.FunctionComponent<RouteComponentProps<MatchParams
|
|||
trackUiMetric(UIM_POLICY_LIST_LOAD);
|
||||
}, []);
|
||||
|
||||
let content;
|
||||
let content: JSX.Element;
|
||||
|
||||
if (isLoading) {
|
||||
content = (
|
||||
|
@ -112,37 +113,100 @@ export const PolicyList: React.FunctionComponent<RouteComponentProps<MatchParams
|
|||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyList.emptyPromptDescription"
|
||||
defaultMessage="Use policies to schedule automatic backups of your cluster."
|
||||
defaultMessage="Create a policy to automatically back up your cluster."
|
||||
/>
|
||||
</p>
|
||||
</Fragment>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
href={linkToAddPolicy()}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="createPolicyButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.createPolicyButton"
|
||||
defaultMessage="Create a policy"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
data-test-subj="emptyPrompt"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const policySchedules = policies.map((policy: SlmPolicy) => policy.schedule);
|
||||
const hasDuplicateSchedules = policySchedules.length > new Set(policySchedules).size;
|
||||
content = (
|
||||
<PolicyTable
|
||||
policies={policies || []}
|
||||
reload={reload}
|
||||
openPolicyDetailsUrl={openPolicyDetailsUrl}
|
||||
onPolicyDeleted={onPolicyDeleted}
|
||||
onPolicyExecuted={onPolicyExecuted}
|
||||
/>
|
||||
<Fragment>
|
||||
{hasDuplicateSchedules ? (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyScheduleWarningTitle"
|
||||
defaultMessage="Two or more policies have the same schedule"
|
||||
/>
|
||||
}
|
||||
color="warning"
|
||||
iconType="alert"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyScheduleWarningDescription"
|
||||
defaultMessage="Only one snapshot can be taken at a time. To avoid snapshot failures, edit or delete the policies."
|
||||
/>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer />
|
||||
</Fragment>
|
||||
) : null}
|
||||
<PolicyTable
|
||||
policies={policies || []}
|
||||
reload={reload}
|
||||
openPolicyDetailsUrl={openPolicyDetailsUrl}
|
||||
onPolicyDeleted={onPolicyDeleted}
|
||||
onPolicyExecuted={onPolicyExecuted}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section data-test-subj="policyList">
|
||||
{policyName ? (
|
||||
<PolicyDetails
|
||||
policyName={policyName}
|
||||
onClose={closePolicyDetails}
|
||||
onPolicyDeleted={onPolicyDeleted}
|
||||
onPolicyExecuted={onPolicyExecuted}
|
||||
/>
|
||||
) : null}
|
||||
{content}
|
||||
</section>
|
||||
<WithPrivileges privileges={APP_SLM_CLUSTER_PRIVILEGES.map(name => `cluster.${name}`)}>
|
||||
{({ hasPrivileges, privilegesMissing }) =>
|
||||
hasPrivileges ? (
|
||||
<section data-test-subj="policyList">
|
||||
{policyName ? (
|
||||
<PolicyDetails
|
||||
policyName={policyName}
|
||||
onClose={closePolicyDetails}
|
||||
onPolicyDeleted={onPolicyDeleted}
|
||||
onPolicyExecuted={onPolicyExecuted}
|
||||
/>
|
||||
) : null}
|
||||
{content}
|
||||
</section>
|
||||
) : (
|
||||
<NotAuthorizedSection
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyList.deniedPrivilegeTitle"
|
||||
defaultMessage="You're missing cluster privileges"
|
||||
/>
|
||||
}
|
||||
message={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyList.deniedPrivilegeDescription"
|
||||
defaultMessage="To manage Snapshot Lifecycle Policies, you must have {privilegesCount,
|
||||
plural, one {this cluster privilege} other {these cluster privileges}}: {missingPrivileges}."
|
||||
values={{
|
||||
missingPrivileges: privilegesMissing.cluster!.join(', '),
|
||||
privilegesCount: privilegesMissing.cluster!.length,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</WithPrivileges>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
EuiLink,
|
||||
EuiToolTip,
|
||||
EuiButtonIcon,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { SlmPolicy } from '../../../../../../common/types';
|
||||
|
@ -24,6 +25,7 @@ import {
|
|||
PolicyDeleteProvider,
|
||||
} from '../../../../components';
|
||||
import { uiMetricService } from '../../../../services/ui_metric';
|
||||
import { linkToAddPolicy, linkToEditPolicy } from '../../../../services/navigation';
|
||||
|
||||
interface Props {
|
||||
policies: SlmPolicy[];
|
||||
|
@ -55,16 +57,34 @@ export const PolicyTable: React.FunctionComponent<Props> = ({
|
|||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
render: (name: SlmPolicy['name']) => {
|
||||
render: (name: SlmPolicy['name'], { inProgress }: SlmPolicy) => {
|
||||
return (
|
||||
/* eslint-disable-next-line @elastic/eui/href-or-on-click */
|
||||
<EuiLink
|
||||
onClick={() => trackUiMetric(UIM_POLICY_SHOW_DETAILS_CLICK)}
|
||||
href={openPolicyDetailsUrl(name)}
|
||||
data-test-subj="policyLink"
|
||||
>
|
||||
{name}
|
||||
</EuiLink>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
|
||||
<EuiLink
|
||||
onClick={() => trackUiMetric(UIM_POLICY_SHOW_DETAILS_CLICK)}
|
||||
href={openPolicyDetailsUrl(name)}
|
||||
data-test-subj="policyLink"
|
||||
>
|
||||
{name}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
{inProgress ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.snapshotRestore.policyList.table.inProgressTooltip',
|
||||
{
|
||||
defaultMessage: 'Snapshot in progress',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiLoadingSpinner size="m" />
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
@ -95,7 +115,7 @@ export const PolicyTable: React.FunctionComponent<Props> = ({
|
|||
{
|
||||
field: 'nextExecutionMillis',
|
||||
name: i18n.translate('xpack.snapshotRestore.policyList.table.nextExecutionColumnTitle', {
|
||||
defaultMessage: 'Next execution',
|
||||
defaultMessage: 'Next snapshot',
|
||||
}),
|
||||
truncateText: true,
|
||||
sortable: true,
|
||||
|
@ -109,64 +129,96 @@ export const PolicyTable: React.FunctionComponent<Props> = ({
|
|||
}),
|
||||
actions: [
|
||||
{
|
||||
render: ({ name }: SlmPolicy) => {
|
||||
render: ({ name, inProgress }: SlmPolicy) => {
|
||||
return (
|
||||
<PolicyExecuteProvider>
|
||||
{executePolicyPrompt => {
|
||||
const label = i18n.translate(
|
||||
'xpack.snapshotRestore.policyList.table.actionExecuteTooltip',
|
||||
{ defaultMessage: 'Run policy' }
|
||||
);
|
||||
return (
|
||||
<EuiToolTip content={label}>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.snapshotRestore.policyList.table.actionExecuteAriaLabel',
|
||||
{
|
||||
defaultMessage: `Run policy '{name}'`,
|
||||
values: { name },
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<PolicyExecuteProvider>
|
||||
{executePolicyPrompt => {
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={
|
||||
Boolean(inProgress)
|
||||
? i18n.translate(
|
||||
'xpack.snapshotRestore.policyList.table.actionExecuteDisabledTooltip',
|
||||
{ defaultMessage: 'Policy is running' }
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.snapshotRestore.policyList.table.actionExecuteTooltip',
|
||||
{ defaultMessage: 'Run now' }
|
||||
)
|
||||
}
|
||||
)}
|
||||
iconType="play"
|
||||
color="primary"
|
||||
data-test-subj="executePolicyButton"
|
||||
onClick={() => executePolicyPrompt(name, onPolicyExecuted)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}}
|
||||
</PolicyExecuteProvider>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
render: ({ name }: SlmPolicy) => {
|
||||
return (
|
||||
<PolicyDeleteProvider>
|
||||
{deletePolicyPrompt => {
|
||||
const label = i18n.translate(
|
||||
'xpack.snapshotRestore.policyList.table.actionDeleteTooltip',
|
||||
{ defaultMessage: 'Delete' }
|
||||
);
|
||||
return (
|
||||
<EuiToolTip content={label}>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.snapshotRestore.policyList.table.actionDeleteAriaLabel',
|
||||
{
|
||||
defaultMessage: `Delete policy '{name}'`,
|
||||
values: { name },
|
||||
}
|
||||
)}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
data-test-subj="deletePolicyButton"
|
||||
onClick={() => deletePolicyPrompt([name], onPolicyDeleted)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}}
|
||||
</PolicyDeleteProvider>
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.snapshotRestore.policyList.table.actionExecuteAriaLabel',
|
||||
{
|
||||
defaultMessage: `Run '{name}' immediately`,
|
||||
values: { name },
|
||||
}
|
||||
)}
|
||||
iconType="play"
|
||||
color="primary"
|
||||
data-test-subj="executePolicyButton"
|
||||
onClick={() => executePolicyPrompt(name, onPolicyExecuted)}
|
||||
disabled={Boolean(inProgress)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}}
|
||||
</PolicyExecuteProvider>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.snapshotRestore.policyList.table.actionEditTooltip',
|
||||
{ defaultMessage: 'Edit' }
|
||||
)}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.snapshotRestore.policyList.table.actionEditAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Edit poicy `{name}`',
|
||||
values: { name },
|
||||
}
|
||||
)}
|
||||
iconType="pencil"
|
||||
color="primary"
|
||||
href={linkToEditPolicy(name)}
|
||||
data-test-subj="editPolicyButton"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<PolicyDeleteProvider>
|
||||
{deletePolicyPrompt => {
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.snapshotRestore.policyList.table.actionDeleteTooltip',
|
||||
{ defaultMessage: 'Delete' }
|
||||
)}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.snapshotRestore.policyList.table.actionDeleteAriaLabel',
|
||||
{
|
||||
defaultMessage: `Delete policy '{name}'`,
|
||||
values: { name },
|
||||
}
|
||||
)}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
data-test-subj="deletePolicyButton"
|
||||
onClick={() => deletePolicyPrompt([name], onPolicyDeleted)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}}
|
||||
</PolicyDeleteProvider>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
@ -237,6 +289,19 @@ export const PolicyTable: React.FunctionComponent<Props> = ({
|
|||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
href={linkToAddPolicy()}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="createPolicyButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.policyList.table.addPolicyButton"
|
||||
defaultMessage="Create a policy"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
box: {
|
||||
|
@ -268,6 +333,7 @@ export const PolicyTable: React.FunctionComponent<Props> = ({
|
|||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
className="snapshotRestore__policyTable"
|
||||
items={policies}
|
||||
itemId="name"
|
||||
columns={columns}
|
||||
|
|
|
@ -14,6 +14,7 @@ import { BASE_PATH, UIM_REPOSITORY_LIST_LOAD } from '../../../constants';
|
|||
import { useAppDependencies } from '../../../index';
|
||||
import { useLoadRepositories } from '../../../services/http';
|
||||
import { uiMetricService } from '../../../services/ui_metric';
|
||||
import { linkToAddRepository, linkToRepository } from '../../../services/navigation';
|
||||
|
||||
import { RepositoryDetails } from './repository_details';
|
||||
import { RepositoryTable } from './repository_table';
|
||||
|
@ -45,9 +46,7 @@ export const RepositoryList: React.FunctionComponent<RouteComponentProps<MatchPa
|
|||
} = useLoadRepositories();
|
||||
|
||||
const openRepositoryDetailsUrl = (newRepositoryName: Repository['name']): string => {
|
||||
return history.createHref({
|
||||
pathname: `${BASE_PATH}/repositories/${newRepositoryName}`,
|
||||
});
|
||||
return linkToRepository(newRepositoryName);
|
||||
};
|
||||
|
||||
const closeRepositoryDetails = () => {
|
||||
|
@ -116,9 +115,7 @@ export const RepositoryList: React.FunctionComponent<RouteComponentProps<MatchPa
|
|||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
href={history.createHref({
|
||||
pathname: `${BASE_PATH}/add_repository`,
|
||||
})}
|
||||
href={linkToAddRepository()}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="registerRepositoryButton"
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
EuiLoadingSpinner,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import { APP_RESTORE_INDEX_PRIVILEGES } from '../../../../../common/constants';
|
||||
import { SectionError, SectionLoading } from '../../../components';
|
||||
import { UIM_RESTORE_LIST_LOAD } from '../../../constants';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
|
@ -207,7 +208,7 @@ export const RestoreList: React.FunctionComponent = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<WithPrivileges privileges="index.*">
|
||||
<WithPrivileges privileges={APP_RESTORE_INDEX_PRIVILEGES.map(name => `index.${name}`)}>
|
||||
{({ hasPrivileges, privilegesMissing }) =>
|
||||
hasPrivileges ? (
|
||||
<section data-test-subj="restoreList">{content}</section>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import {
|
||||
EuiDescriptionList,
|
||||
|
@ -112,6 +112,13 @@ export const TabSummary: React.SFC<Props> = ({ snapshotDetails }) => {
|
|||
</ul>
|
||||
) : null;
|
||||
|
||||
// Reset indices list state when clicking through different snapshots
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setIsShowingFullIndicesList(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiFlexGroup>
|
||||
|
|
|
@ -7,15 +7,22 @@
|
|||
import React, { Fragment, useState, useEffect } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { parse } from 'querystring';
|
||||
import { EuiButton, EuiCallOut, EuiLink, EuiEmptyPrompt, EuiSpacer, EuiIcon } from '@elastic/eui';
|
||||
|
||||
import { EuiButton, EuiCallOut, EuiIcon, EuiLink, EuiEmptyPrompt, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { APP_SLM_CLUSTER_PRIVILEGES } from '../../../../../common/constants';
|
||||
import { SectionError, SectionLoading } from '../../../components';
|
||||
import { BASE_PATH, UIM_SNAPSHOT_LIST_LOAD } from '../../../constants';
|
||||
import { WithPrivileges } from '../../../lib/authorization';
|
||||
import { useAppDependencies } from '../../../index';
|
||||
import { documentationLinksService } from '../../../services/documentation';
|
||||
import { useLoadSnapshots } from '../../../services/http';
|
||||
import { linkToRepositories } from '../../../services/navigation';
|
||||
import {
|
||||
linkToRepositories,
|
||||
linkToAddRepository,
|
||||
linkToPolicies,
|
||||
linkToAddPolicy,
|
||||
linkToSnapshot,
|
||||
} from '../../../services/navigation';
|
||||
import { uiMetricService } from '../../../services/ui_metric';
|
||||
|
||||
import { SnapshotDetails } from './snapshot_details';
|
||||
|
@ -42,7 +49,7 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
const {
|
||||
error,
|
||||
isLoading,
|
||||
data: { snapshots = [], repositories = [], errors = {} },
|
||||
data: { snapshots = [], repositories = [], policies = [], errors = {} },
|
||||
sendRequest: reload,
|
||||
} = useLoadSnapshots();
|
||||
|
||||
|
@ -50,11 +57,7 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
repositoryNameToOpen: string,
|
||||
snapshotIdToOpen: string
|
||||
): string => {
|
||||
return history.createHref({
|
||||
pathname: `${BASE_PATH}/snapshots/${encodeURIComponent(
|
||||
repositoryNameToOpen
|
||||
)}/${encodeURIComponent(snapshotIdToOpen)}`,
|
||||
});
|
||||
return linkToSnapshot(repositoryNameToOpen, snapshotIdToOpen);
|
||||
};
|
||||
|
||||
const closeSnapshotDetails = () => {
|
||||
|
@ -138,37 +141,22 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
</h1>
|
||||
}
|
||||
body={
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.emptyPrompt.repositoryWarningDescription"
|
||||
defaultMessage="Go to {repositoryLink} to fix the errors."
|
||||
values={{
|
||||
repositoryLink: (
|
||||
<EuiLink href={linkToRepositories()}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryWarningLinkText"
|
||||
defaultMessage="Repositories"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<EuiLink
|
||||
href={documentationLinksService.getSnapshotDocUrl()}
|
||||
target="_blank"
|
||||
data-test-subj="srSnapshotsEmptyPromptDocLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.emptyPrompt.noSnapshotsDocLinkText"
|
||||
defaultMessage="Learn how to create a snapshot"
|
||||
/>{' '}
|
||||
<EuiIcon type="link" />
|
||||
</EuiLink>
|
||||
</p>
|
||||
</Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.emptyPrompt.repositoryWarningDescription"
|
||||
defaultMessage="Go to {repositoryLink} to fix the errors."
|
||||
values={{
|
||||
repositoryLink: (
|
||||
<EuiLink href={linkToRepositories()}>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.repositoryWarningLinkText"
|
||||
defaultMessage="Repositories"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
@ -194,9 +182,7 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
</p>
|
||||
<p>
|
||||
<EuiButton
|
||||
href={history.createHref({
|
||||
pathname: `${BASE_PATH}/add_repository`,
|
||||
})}
|
||||
href={linkToAddRepository()}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="registerRepositoryButton"
|
||||
|
@ -225,27 +211,84 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
|
|||
</h1>
|
||||
}
|
||||
body={
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.emptyPrompt.noSnapshotsDescription"
|
||||
defaultMessage="Create a snapshot using the Elasticsearch API."
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<EuiLink
|
||||
href={documentationLinksService.getSnapshotDocUrl()}
|
||||
target="_blank"
|
||||
data-test-subj="documentationLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.emptyPrompt.noSnapshotsDocLinkText"
|
||||
defaultMessage="Learn how to create a snapshot"
|
||||
/>{' '}
|
||||
<EuiIcon type="link" />
|
||||
</EuiLink>
|
||||
</p>
|
||||
</Fragment>
|
||||
<WithPrivileges privileges={APP_SLM_CLUSTER_PRIVILEGES.map(name => `cluster.${name}`)}>
|
||||
{({ hasPrivileges }) =>
|
||||
hasPrivileges ? (
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.emptyPrompt.usePolicyDescription"
|
||||
defaultMessage="Run a Snapshot Lifecycle Policy to create a snapshot.
|
||||
Snapshots can also be created by using {docLink}."
|
||||
values={{
|
||||
docLink: (
|
||||
<EuiLink
|
||||
href={documentationLinksService.getSnapshotDocUrl()}
|
||||
target="_blank"
|
||||
data-test-subj="documentationLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.emptyPrompt.usePolicyDocLinkText"
|
||||
defaultMessage="the Elasticsearch API"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
{policies.length === 0 ? (
|
||||
<EuiButton
|
||||
href={linkToAddPolicy()}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="addPolicyButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.emptyPrompt.addPolicyText"
|
||||
defaultMessage="Create a policy"
|
||||
/>
|
||||
</EuiButton>
|
||||
) : (
|
||||
<EuiButton
|
||||
href={linkToPolicies()}
|
||||
fill
|
||||
iconType="list"
|
||||
data-test-subj="goToPoliciesButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.emptyPrompt.goToPoliciesText"
|
||||
defaultMessage="View policies"
|
||||
/>
|
||||
</EuiButton>
|
||||
)}
|
||||
</p>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.snapshotList.emptyPrompt.noSnapshotsDescription"
|
||||
defaultMessage="Create a snapshot using the Elasticsearch API."
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<EuiLink
|
||||
href={documentationLinksService.getSnapshotDocUrl()}
|
||||
target="_blank"
|
||||
data-test-subj="documentationLink"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.emptyPrompt.noSnapshotsDocLinkText"
|
||||
defaultMessage="Learn how to create a snapshot"
|
||||
/>{' '}
|
||||
<EuiIcon type="link" />
|
||||
</EuiLink>
|
||||
</p>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
</WithPrivileges>
|
||||
}
|
||||
data-test-subj="emptyPrompt"
|
||||
/>
|
||||
|
|
|
@ -8,3 +8,5 @@ export { SnapshotRestoreHome } from './home';
|
|||
export { RepositoryAdd } from './repository_add';
|
||||
export { RepositoryEdit } from './repository_edit';
|
||||
export { RestoreSnapshot } from './restore_snapshot';
|
||||
export { PolicyAdd } from './policy_add';
|
||||
export { PolicyEdit } from './policy_edit';
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { CronEditor } from './cron_editor';
|
||||
export { PolicyAdd } from './policy_add';
|
|
@ -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 React, { useEffect, useState } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { SlmPolicyPayload } from '../../../../common/types';
|
||||
|
||||
import { PolicyForm, SectionError, SectionLoading } from '../../components';
|
||||
import { useAppDependencies } from '../../index';
|
||||
import { BASE_PATH, DEFAULT_POLICY_SCHEDULE } from '../../constants';
|
||||
import { breadcrumbService, docTitleService } from '../../services/navigation';
|
||||
import { addPolicy, useLoadIndicies } from '../../services/http';
|
||||
|
||||
export const PolicyAdd: React.FunctionComponent<RouteComponentProps> = ({
|
||||
history,
|
||||
location: { pathname },
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
},
|
||||
} = useAppDependencies();
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
const [saveError, setSaveError] = useState<any>(null);
|
||||
|
||||
const {
|
||||
error: errorLoadingIndices,
|
||||
isLoading: isLoadingIndices,
|
||||
data: { indices } = {
|
||||
indices: [],
|
||||
},
|
||||
} = useLoadIndicies();
|
||||
|
||||
// Set breadcrumb and page title
|
||||
useEffect(() => {
|
||||
breadcrumbService.setBreadcrumbs('policyAdd');
|
||||
docTitleService.setTitle('policyAdd');
|
||||
}, []);
|
||||
|
||||
const onSave = async (newPolicy: SlmPolicyPayload) => {
|
||||
setIsSaving(true);
|
||||
setSaveError(null);
|
||||
const { name } = newPolicy;
|
||||
const { error } = await addPolicy(newPolicy);
|
||||
setIsSaving(false);
|
||||
if (error) {
|
||||
setSaveError(error);
|
||||
} else {
|
||||
history.push(`${BASE_PATH}/policies/${name}`);
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
history.push(`${BASE_PATH}/policies`);
|
||||
};
|
||||
|
||||
const emptyPolicy: SlmPolicyPayload = {
|
||||
name: '',
|
||||
snapshotName: '',
|
||||
schedule: DEFAULT_POLICY_SCHEDULE,
|
||||
repository: '',
|
||||
config: {},
|
||||
};
|
||||
|
||||
const renderSaveError = () => {
|
||||
return saveError ? (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.addPolicy.savingPolicyErrorTitle"
|
||||
defaultMessage="Cannot create new policy"
|
||||
/>
|
||||
}
|
||||
error={saveError}
|
||||
data-test-subj="savePolicyApiError"
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const clearSaveError = () => {
|
||||
setSaveError(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<EuiTitle size="l">
|
||||
<h1 data-test-subj="pageTitle">
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.addPolicyTitle"
|
||||
defaultMessage="Create policy"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="l" />
|
||||
{isLoadingIndices ? (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.addPolicy.loadingIndicesDescription"
|
||||
defaultMessage="Loading available indices…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
) : errorLoadingIndices ? (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.addPolicy.LoadingIndicesErrorMessage"
|
||||
defaultMessage="Error loading available indices"
|
||||
/>
|
||||
}
|
||||
error={errorLoadingIndices}
|
||||
/>
|
||||
) : (
|
||||
<PolicyForm
|
||||
policy={emptyPolicy}
|
||||
indices={indices}
|
||||
currentUrl={pathname}
|
||||
isSaving={isSaving}
|
||||
saveError={renderSaveError()}
|
||||
clearSaveError={clearSaveError}
|
||||
onSave={onSave}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
)}
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
};
|
|
@ -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 { PolicyEdit } from './policy_edit';
|
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
* 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, { useEffect, useState, Fragment } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { SlmPolicyPayload } from '../../../../common/types';
|
||||
|
||||
import { SectionError, SectionLoading, PolicyForm } from '../../components';
|
||||
import { BASE_PATH } from '../../constants';
|
||||
import { useAppDependencies } from '../../index';
|
||||
import { breadcrumbService, docTitleService } from '../../services/navigation';
|
||||
import { editPolicy, useLoadPolicy, useLoadIndicies } from '../../services/http';
|
||||
|
||||
interface MatchParams {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const PolicyEdit: React.FunctionComponent<RouteComponentProps<MatchParams>> = ({
|
||||
match: {
|
||||
params: { name },
|
||||
},
|
||||
history,
|
||||
location: { pathname },
|
||||
}) => {
|
||||
const {
|
||||
core: { i18n },
|
||||
} = useAppDependencies();
|
||||
const { FormattedMessage } = i18n;
|
||||
|
||||
// Set breadcrumb and page title
|
||||
useEffect(() => {
|
||||
breadcrumbService.setBreadcrumbs('policyEdit');
|
||||
docTitleService.setTitle('policyEdit');
|
||||
}, []);
|
||||
|
||||
// Policy state with default empty policy
|
||||
const [policy, setPolicy] = useState<SlmPolicyPayload>({
|
||||
name: '',
|
||||
snapshotName: '',
|
||||
schedule: '',
|
||||
repository: '',
|
||||
config: {},
|
||||
});
|
||||
|
||||
const {
|
||||
error: errorLoadingIndices,
|
||||
isLoading: isLoadingIndices,
|
||||
data: { indices } = {
|
||||
indices: [],
|
||||
},
|
||||
} = useLoadIndicies();
|
||||
|
||||
// Load policy
|
||||
const { error: errorLoadingPolicy, isLoading: isLoadingPolicy, data: policyData } = useLoadPolicy(
|
||||
name
|
||||
);
|
||||
|
||||
// Update policy state when data is loaded
|
||||
useEffect(() => {
|
||||
if (policyData && policyData.policy) {
|
||||
setPolicy(policyData.policy);
|
||||
}
|
||||
}, [policyData]);
|
||||
|
||||
// Saving policy states
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
const [saveError, setSaveError] = useState<any>(null);
|
||||
|
||||
// Save policy
|
||||
const onSave = async (editedPolicy: SlmPolicyPayload) => {
|
||||
setIsSaving(true);
|
||||
setSaveError(null);
|
||||
const { error } = await editPolicy(editedPolicy);
|
||||
setIsSaving(false);
|
||||
if (error) {
|
||||
setSaveError(error);
|
||||
} else {
|
||||
history.push(`${BASE_PATH}/policies/${name}`);
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
history.push(`${BASE_PATH}/policies/${name}`);
|
||||
};
|
||||
|
||||
const renderLoading = () => {
|
||||
return errorLoadingPolicy ? (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.editPolicy.loadingPolicyDescription"
|
||||
defaultMessage="Loading policy details…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
) : (
|
||||
<SectionLoading>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.editPolicy.loadingIndicesDescription"
|
||||
defaultMessage="Loading available indices…"
|
||||
/>
|
||||
</SectionLoading>
|
||||
);
|
||||
};
|
||||
|
||||
const renderError = () => {
|
||||
if (errorLoadingPolicy) {
|
||||
const notFound = errorLoadingPolicy.status === 404;
|
||||
const errorObject = notFound
|
||||
? {
|
||||
data: {
|
||||
error: i18n.translate('xpack.snapshotRestore.editPolicy.policyNotFoundErrorMessage', {
|
||||
defaultMessage: `The policy '{name}' does not exist.`,
|
||||
values: {
|
||||
name,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
: errorLoadingPolicy;
|
||||
return (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.editPolicy.loadingPolicyErrorTitle"
|
||||
defaultMessage="Error loading policy details"
|
||||
/>
|
||||
}
|
||||
error={errorObject}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (errorLoadingIndices) {
|
||||
return (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.editPolicy.LoadingIndicesErrorMessage"
|
||||
defaultMessage="Error loading available indices"
|
||||
/>
|
||||
}
|
||||
error={errorLoadingIndices}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderSaveError = () => {
|
||||
return saveError ? (
|
||||
<SectionError
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.editPolicy.savingPolicyErrorTitle"
|
||||
defaultMessage="Cannot save policy"
|
||||
/>
|
||||
}
|
||||
error={saveError}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const clearSaveError = () => {
|
||||
setSaveError(null);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (isLoadingPolicy || isLoadingIndices) {
|
||||
return renderLoading();
|
||||
}
|
||||
if (errorLoadingPolicy || errorLoadingIndices) {
|
||||
return renderError();
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PolicyForm
|
||||
policy={policy}
|
||||
indices={indices}
|
||||
currentUrl={pathname}
|
||||
isEditing={true}
|
||||
isSaving={isSaving}
|
||||
saveError={renderSaveError()}
|
||||
clearSaveError={clearSaveError}
|
||||
onSave={onSave}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPageBody>
|
||||
<EuiPageContent>
|
||||
<EuiTitle size="l">
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.snapshotRestore.editPolicyTitle"
|
||||
defaultMessage="Edit policy"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="l" />
|
||||
{renderContent()}
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
};
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { parse } from 'querystring';
|
||||
|
||||
import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { Repository, EmptyRepository } from '../../../../common/types';
|
||||
|
@ -12,10 +13,13 @@ import { Repository, EmptyRepository } from '../../../../common/types';
|
|||
import { RepositoryForm, SectionError } from '../../components';
|
||||
import { BASE_PATH, Section } from '../../constants';
|
||||
import { useAppDependencies } from '../../index';
|
||||
import { breadcrumbService } from '../../services/navigation';
|
||||
import { breadcrumbService, docTitleService } from '../../services/navigation';
|
||||
import { addRepository } from '../../services/http';
|
||||
|
||||
export const RepositoryAdd: React.FunctionComponent<RouteComponentProps> = ({ history }) => {
|
||||
export const RepositoryAdd: React.FunctionComponent<RouteComponentProps> = ({
|
||||
history,
|
||||
location: { search },
|
||||
}) => {
|
||||
const {
|
||||
core: {
|
||||
i18n: { FormattedMessage },
|
||||
|
@ -25,9 +29,10 @@ export const RepositoryAdd: React.FunctionComponent<RouteComponentProps> = ({ hi
|
|||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
const [saveError, setSaveError] = useState<any>(null);
|
||||
|
||||
// Set breadcrumb
|
||||
// Set breadcrumb and page title
|
||||
useEffect(() => {
|
||||
breadcrumbService.setBreadcrumbs('repositoryAdd');
|
||||
docTitleService.setTitle('repositoryAdd');
|
||||
}, []);
|
||||
|
||||
const onSave = async (newRepository: Repository | EmptyRepository) => {
|
||||
|
@ -39,7 +44,8 @@ export const RepositoryAdd: React.FunctionComponent<RouteComponentProps> = ({ hi
|
|||
if (error) {
|
||||
setSaveError(error);
|
||||
} else {
|
||||
history.push(`${BASE_PATH}/${section}/${name}`);
|
||||
const { redirect } = parse(search.replace(/^\?/, ''));
|
||||
history.push(redirect ? (redirect as string) : `${BASE_PATH}/${section}/${name}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import { Repository, EmptyRepository } from '../../../../common/types';
|
|||
import { RepositoryForm, SectionError, SectionLoading } from '../../components';
|
||||
import { BASE_PATH, Section } from '../../constants';
|
||||
import { useAppDependencies } from '../../index';
|
||||
import { breadcrumbService } from '../../services/navigation';
|
||||
import { breadcrumbService, docTitleService } from '../../services/navigation';
|
||||
import { editRepository, useLoadRepository } from '../../services/http';
|
||||
|
||||
interface MatchParams {
|
||||
|
@ -31,9 +31,10 @@ export const RepositoryEdit: React.FunctionComponent<RouteComponentProps<MatchPa
|
|||
const { FormattedMessage } = i18n;
|
||||
const section = 'repositories' as Section;
|
||||
|
||||
// Set breadcrumb
|
||||
// Set breadcrumb and page title
|
||||
useEffect(() => {
|
||||
breadcrumbService.setBreadcrumbs('repositoryEdit');
|
||||
docTitleService.setTitle('repositoryEdit');
|
||||
}, []);
|
||||
|
||||
// Repository state with default empty repository
|
||||
|
|
|
@ -11,7 +11,7 @@ import { SnapshotDetails, RestoreSettings } from '../../../../common/types';
|
|||
import { BASE_PATH } from '../../constants';
|
||||
import { SectionError, SectionLoading, RestoreSnapshotForm } from '../../components';
|
||||
import { useAppDependencies } from '../../index';
|
||||
import { breadcrumbService } from '../../services/navigation';
|
||||
import { breadcrumbService, docTitleService } from '../../services/navigation';
|
||||
import { useLoadSnapshot, executeRestore } from '../../services/http';
|
||||
|
||||
interface MatchParams {
|
||||
|
@ -30,9 +30,10 @@ export const RestoreSnapshot: React.FunctionComponent<RouteComponentProps<MatchP
|
|||
} = useAppDependencies();
|
||||
const { FormattedMessage } = i18n;
|
||||
|
||||
// Set breadcrumb
|
||||
// Set breadcrumb and page title
|
||||
useEffect(() => {
|
||||
breadcrumbService.setBreadcrumbs('restoreSnapshot');
|
||||
docTitleService.setTitle('restoreSnapshot');
|
||||
}, []);
|
||||
|
||||
// Snapshot details state with default empty snapshot
|
||||
|
|
|
@ -10,10 +10,16 @@ import { REPOSITORY_DOC_PATHS } from '../../constants';
|
|||
class DocumentationLinksService {
|
||||
private esDocBasePath: string = '';
|
||||
private esPluginDocBasePath: string = '';
|
||||
private esStackOverviewDocBasePath: string = '';
|
||||
|
||||
public init(esDocBasePath: string, esPluginDocBasePath: string): void {
|
||||
public init(
|
||||
esDocBasePath: string,
|
||||
esPluginDocBasePath: string,
|
||||
esStackOverviewDocBasePath: string
|
||||
): void {
|
||||
this.esDocBasePath = esDocBasePath;
|
||||
this.esPluginDocBasePath = esPluginDocBasePath;
|
||||
this.esStackOverviewDocBasePath = esStackOverviewDocBasePath;
|
||||
}
|
||||
|
||||
public getRepositoryPluginDocUrl() {
|
||||
|
@ -42,7 +48,7 @@ class DocumentationLinksService {
|
|||
}
|
||||
|
||||
public getSnapshotDocUrl() {
|
||||
return `${this.esDocBasePath}/modules-snapshots.html#_snapshot`;
|
||||
return `${this.esDocBasePath}/modules-snapshots.html#snapshots-take-snapshot`;
|
||||
}
|
||||
|
||||
public getRestoreDocUrl() {
|
||||
|
@ -56,6 +62,18 @@ class DocumentationLinksService {
|
|||
public getIndexSettingsUrl() {
|
||||
return `${this.esDocBasePath}/index-modules.html`;
|
||||
}
|
||||
|
||||
public getDateMathIndexNamesUrl() {
|
||||
return `${this.esDocBasePath}/date-math-index-names.html`;
|
||||
}
|
||||
|
||||
public getSlmUrl() {
|
||||
return `${this.esDocBasePath}/slm-api-put.html`;
|
||||
}
|
||||
|
||||
public getCronUrl() {
|
||||
return `${this.esStackOverviewDocBasePath}/trigger-schedule.html#schedule-cron`;
|
||||
}
|
||||
}
|
||||
|
||||
export const documentationLinksService = new DocumentationLinksService();
|
||||
|
|
|
@ -4,8 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { API_BASE_PATH } from '../../../../common/constants';
|
||||
import { SlmPolicy } from '../../../../common/types';
|
||||
import { UIM_POLICY_EXECUTE, UIM_POLICY_DELETE, UIM_POLICY_DELETE_MANY } from '../../constants';
|
||||
import { SlmPolicy, SlmPolicyPayload } from '../../../../common/types';
|
||||
import {
|
||||
UIM_POLICY_EXECUTE,
|
||||
UIM_POLICY_DELETE,
|
||||
UIM_POLICY_DELETE_MANY,
|
||||
UIM_POLICY_CREATE,
|
||||
UIM_POLICY_UPDATE,
|
||||
} from '../../constants';
|
||||
import { uiMetricService } from '../ui_metric';
|
||||
import { httpService } from './http';
|
||||
import { useRequest, sendRequest } from './use_request';
|
||||
|
@ -24,6 +30,13 @@ export const useLoadPolicy = (name: SlmPolicy['name']) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const useLoadIndicies = () => {
|
||||
return useRequest({
|
||||
path: httpService.addBasePath(`${API_BASE_PATH}policies/indices`),
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
|
||||
export const executePolicy = async (name: SlmPolicy['name']) => {
|
||||
const result = sendRequest({
|
||||
path: httpService.addBasePath(`${API_BASE_PATH}policy/${encodeURIComponent(name)}/run`),
|
||||
|
@ -47,3 +60,29 @@ export const deletePolicies = async (names: Array<SlmPolicy['name']>) => {
|
|||
trackUiMetric(names.length > 1 ? UIM_POLICY_DELETE_MANY : UIM_POLICY_DELETE);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const addPolicy = async (newPolicy: SlmPolicyPayload) => {
|
||||
const result = sendRequest({
|
||||
path: httpService.addBasePath(`${API_BASE_PATH}policies`),
|
||||
method: 'put',
|
||||
body: newPolicy,
|
||||
});
|
||||
|
||||
const { trackUiMetric } = uiMetricService;
|
||||
trackUiMetric(UIM_POLICY_CREATE);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const editPolicy = async (editedPolicy: SlmPolicyPayload) => {
|
||||
const result = await sendRequest({
|
||||
path: httpService.addBasePath(
|
||||
`${API_BASE_PATH}policies/${encodeURIComponent(editedPolicy.name)}`
|
||||
),
|
||||
method: 'put',
|
||||
body: editedPolicy,
|
||||
});
|
||||
|
||||
const { trackUiMetric } = uiMetricService;
|
||||
trackUiMetric(UIM_POLICY_UPDATE);
|
||||
return result;
|
||||
};
|
||||
|
|
|
@ -4,50 +4,128 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { BASE_PATH } from '../../constants';
|
||||
import { textService } from '../text';
|
||||
import {
|
||||
linkToHome,
|
||||
linkToSnapshots,
|
||||
linkToRepositories,
|
||||
linkToPolicies,
|
||||
linkToRestoreStatus,
|
||||
} from './';
|
||||
|
||||
class BreadcrumbService {
|
||||
private chrome: any;
|
||||
private breadcrumbs: any = {
|
||||
management: {},
|
||||
home: {},
|
||||
repositoryAdd: {},
|
||||
repositoryEdit: {},
|
||||
restoreSnapshot: {},
|
||||
private breadcrumbs: {
|
||||
[key: string]: Array<{
|
||||
text: string;
|
||||
href?: string;
|
||||
}>;
|
||||
} = {
|
||||
management: [],
|
||||
home: [],
|
||||
snapshots: [],
|
||||
repositories: [],
|
||||
policies: [],
|
||||
restore_status: [],
|
||||
repositoryAdd: [],
|
||||
repositoryEdit: [],
|
||||
restoreSnapshot: [],
|
||||
policyAdd: [],
|
||||
policyEdit: [],
|
||||
};
|
||||
|
||||
public init(chrome: any, managementBreadcrumb: any): void {
|
||||
this.chrome = chrome;
|
||||
this.breadcrumbs.management = managementBreadcrumb;
|
||||
this.breadcrumbs.home = {
|
||||
text: textService.breadcrumbs.home,
|
||||
href: `#${BASE_PATH}`,
|
||||
};
|
||||
this.breadcrumbs.repositoryAdd = {
|
||||
text: textService.breadcrumbs.repositoryAdd,
|
||||
};
|
||||
this.breadcrumbs.repositoryEdit = {
|
||||
text: textService.breadcrumbs.repositoryEdit,
|
||||
};
|
||||
this.breadcrumbs.restoreSnapshot = {
|
||||
text: textService.breadcrumbs.restoreSnapshot,
|
||||
};
|
||||
this.breadcrumbs.management = [managementBreadcrumb];
|
||||
|
||||
// Home and sections
|
||||
this.breadcrumbs.home = [
|
||||
...this.breadcrumbs.management,
|
||||
{
|
||||
text: textService.breadcrumbs.home,
|
||||
href: linkToHome(),
|
||||
},
|
||||
];
|
||||
this.breadcrumbs.snapshots = [
|
||||
...this.breadcrumbs.home,
|
||||
{
|
||||
text: textService.breadcrumbs.snapshots,
|
||||
href: linkToSnapshots(),
|
||||
},
|
||||
];
|
||||
this.breadcrumbs.repositories = [
|
||||
...this.breadcrumbs.home,
|
||||
{
|
||||
text: textService.breadcrumbs.repositories,
|
||||
href: linkToRepositories(),
|
||||
},
|
||||
];
|
||||
this.breadcrumbs.policies = [
|
||||
...this.breadcrumbs.home,
|
||||
{
|
||||
text: textService.breadcrumbs.policies,
|
||||
href: linkToPolicies(),
|
||||
},
|
||||
];
|
||||
this.breadcrumbs.restore_status = [
|
||||
...this.breadcrumbs.home,
|
||||
{
|
||||
text: textService.breadcrumbs.restore_status,
|
||||
href: linkToRestoreStatus(),
|
||||
},
|
||||
];
|
||||
|
||||
// Inner pages
|
||||
this.breadcrumbs.repositoryAdd = [
|
||||
...this.breadcrumbs.repositories,
|
||||
{
|
||||
text: textService.breadcrumbs.repositoryAdd,
|
||||
},
|
||||
];
|
||||
this.breadcrumbs.repositoryEdit = [
|
||||
...this.breadcrumbs.repositories,
|
||||
{
|
||||
text: textService.breadcrumbs.repositoryEdit,
|
||||
},
|
||||
];
|
||||
this.breadcrumbs.restoreSnapshot = [
|
||||
...this.breadcrumbs.snapshots,
|
||||
{
|
||||
text: textService.breadcrumbs.restoreSnapshot,
|
||||
},
|
||||
];
|
||||
this.breadcrumbs.policyAdd = [
|
||||
...this.breadcrumbs.policies,
|
||||
{
|
||||
text: textService.breadcrumbs.policyAdd,
|
||||
},
|
||||
];
|
||||
this.breadcrumbs.policyEdit = [
|
||||
...this.breadcrumbs.policies,
|
||||
{
|
||||
text: textService.breadcrumbs.policyEdit,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
public setBreadcrumbs(type: string): void {
|
||||
if (!this.breadcrumbs[type]) {
|
||||
return;
|
||||
}
|
||||
if (type === 'home') {
|
||||
this.chrome.breadcrumbs.set([this.breadcrumbs.management, this.breadcrumbs.home]);
|
||||
} else {
|
||||
this.chrome.breadcrumbs.set([
|
||||
this.breadcrumbs.management,
|
||||
this.breadcrumbs.home,
|
||||
this.breadcrumbs[type],
|
||||
]);
|
||||
}
|
||||
const newBreadcrumbs = this.breadcrumbs[type]
|
||||
? [...this.breadcrumbs[type]]
|
||||
: [...this.breadcrumbs.home];
|
||||
|
||||
// Pop off last breadcrumb
|
||||
const lastBreadcrumb = newBreadcrumbs.pop() as {
|
||||
text: string;
|
||||
href?: string;
|
||||
};
|
||||
|
||||
// Put last breadcrumb back without href
|
||||
newBreadcrumbs.push({
|
||||
...lastBreadcrumb,
|
||||
href: undefined,
|
||||
});
|
||||
|
||||
this.chrome.breadcrumbs.set(newBreadcrumbs);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { textService } from '../text';
|
||||
|
||||
class DocTitleService {
|
||||
private changeDocTitle: any = () => {};
|
||||
|
||||
public init(changeDocTitle: any): void {
|
||||
this.changeDocTitle = changeDocTitle;
|
||||
}
|
||||
|
||||
public setTitle(page?: string): void {
|
||||
if (!page || page === 'home') {
|
||||
this.changeDocTitle(`${textService.breadcrumbs.home}`);
|
||||
} else if (textService.breadcrumbs[page]) {
|
||||
this.changeDocTitle(`${textService.breadcrumbs[page]} - ${textService.breadcrumbs.home}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const docTitleService = new DocTitleService();
|
|
@ -5,4 +5,5 @@
|
|||
*/
|
||||
|
||||
export { breadcrumbService } from './breadcrumb';
|
||||
export { docTitleService } from './doc_title';
|
||||
export * from './links';
|
||||
|
|
|
@ -6,6 +6,10 @@
|
|||
|
||||
import { BASE_PATH } from '../../constants';
|
||||
|
||||
export function linkToHome() {
|
||||
return `#${BASE_PATH}`;
|
||||
}
|
||||
|
||||
export function linkToRepositories() {
|
||||
return `#${BASE_PATH}/repositories`;
|
||||
}
|
||||
|
@ -18,8 +22,10 @@ export function linkToEditRepository(repositoryName: string) {
|
|||
return `#${BASE_PATH}/edit_repository/${encodeURIComponent(repositoryName)}`;
|
||||
}
|
||||
|
||||
export function linkToAddRepository() {
|
||||
return `#${BASE_PATH}/add_repository`;
|
||||
export function linkToAddRepository(redirect?: string) {
|
||||
return `#${BASE_PATH}/add_repository${
|
||||
redirect ? `?redirect=${encodeURIComponent(redirect)}` : ''
|
||||
}`;
|
||||
}
|
||||
|
||||
export function linkToSnapshots(repositoryName?: string, policyName?: string) {
|
||||
|
@ -44,6 +50,22 @@ export function linkToRestoreSnapshot(repositoryName: string, snapshotName: stri
|
|||
)}`;
|
||||
}
|
||||
|
||||
export function linkToPolicies() {
|
||||
return `#${BASE_PATH}/policies`;
|
||||
}
|
||||
|
||||
export function linkToPolicy(policyName: string) {
|
||||
return `#${BASE_PATH}/policies/${encodeURIComponent(policyName)}`;
|
||||
}
|
||||
|
||||
export function linkToEditPolicy(policyName: string) {
|
||||
return `#${BASE_PATH}/edit_policy/${encodeURIComponent(policyName)}`;
|
||||
}
|
||||
|
||||
export function linkToAddPolicy() {
|
||||
return `#${BASE_PATH}/add_policy`;
|
||||
}
|
||||
|
||||
export function linkToRestoreStatus() {
|
||||
return `#${BASE_PATH}/restore_status`;
|
||||
}
|
||||
|
|
|
@ -51,6 +51,18 @@ class TextService {
|
|||
home: i18n.translate('xpack.snapshotRestore.home.breadcrumbTitle', {
|
||||
defaultMessage: 'Snapshot and Restore',
|
||||
}),
|
||||
snapshots: i18n.translate('xpack.snapshotRestore.snapshots.breadcrumbTitle', {
|
||||
defaultMessage: 'Snapshots',
|
||||
}),
|
||||
repositories: i18n.translate('xpack.snapshotRestore.repositories.breadcrumbTitle', {
|
||||
defaultMessage: 'Repositories',
|
||||
}),
|
||||
policies: i18n.translate('xpack.snapshotRestore.policies.breadcrumbTitle', {
|
||||
defaultMessage: 'Policies',
|
||||
}),
|
||||
restore_status: i18n.translate('xpack.snapshotRestore.restoreStatus.breadcrumbTitle', {
|
||||
defaultMessage: 'Restore Status',
|
||||
}),
|
||||
repositoryAdd: i18n.translate('xpack.snapshotRestore.addRepository.breadcrumbTitle', {
|
||||
defaultMessage: 'Add repository',
|
||||
}),
|
||||
|
@ -60,6 +72,12 @@ class TextService {
|
|||
restoreSnapshot: i18n.translate('xpack.snapshotRestore.restoreSnapshot.breadcrumbTitle', {
|
||||
defaultMessage: 'Restore snapshot',
|
||||
}),
|
||||
policyAdd: i18n.translate('xpack.snapshotRestore.addPolicy.breadcrumbTitle', {
|
||||
defaultMessage: 'Add policy',
|
||||
}),
|
||||
policyEdit: i18n.translate('xpack.snapshotRestore.editPolicy.breadcrumbTitle', {
|
||||
defaultMessage: 'Edit policy',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,3 +11,5 @@ export {
|
|||
} from './validate_repository';
|
||||
|
||||
export { RestoreValidation, validateRestore } from './validate_restore';
|
||||
|
||||
export { PolicyValidation, validatePolicy } from './validate_policy';
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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 { SlmPolicyPayload } from '../../../../common/types';
|
||||
import { textService } from '../text';
|
||||
|
||||
export interface PolicyValidation {
|
||||
isValid: boolean;
|
||||
errors: { [key: string]: React.ReactNode[] };
|
||||
}
|
||||
|
||||
const isStringEmpty = (str: string | null): boolean => {
|
||||
return str ? !Boolean(str.trim()) : true;
|
||||
};
|
||||
|
||||
export const validatePolicy = (policy: SlmPolicyPayload): PolicyValidation => {
|
||||
const i18n = textService.i18n;
|
||||
|
||||
const { name, snapshotName, schedule, repository, config } = policy;
|
||||
|
||||
const validation: PolicyValidation = {
|
||||
isValid: true,
|
||||
errors: {
|
||||
name: [],
|
||||
snapshotName: [],
|
||||
schedule: [],
|
||||
repository: [],
|
||||
indices: [],
|
||||
},
|
||||
};
|
||||
|
||||
if (isStringEmpty(name)) {
|
||||
validation.errors.name.push(
|
||||
i18n.translate('xpack.snapshotRestore.policyValidation.nameRequiredError', {
|
||||
defaultMessage: 'Policy name is required.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isStringEmpty(snapshotName)) {
|
||||
validation.errors.snapshotName.push(
|
||||
i18n.translate('xpack.snapshotRestore.policyValidation.snapshotNameRequiredError', {
|
||||
defaultMessage: 'Snapshot name is required.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isStringEmpty(schedule)) {
|
||||
validation.errors.schedule.push(
|
||||
i18n.translate('xpack.snapshotRestore.policyValidation.scheduleRequiredError', {
|
||||
defaultMessage: 'Schedule is required.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isStringEmpty(repository)) {
|
||||
validation.errors.repository.push(
|
||||
i18n.translate('xpack.snapshotRestore.policyValidation.repositoryRequiredError', {
|
||||
defaultMessage: 'Repository is required.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (config && typeof config.indices === 'string' && config.indices.trim().length === 0) {
|
||||
validation.errors.indices.push(
|
||||
i18n.translate('xpack.snapshotRestore.policyValidation.indexPatternRequiredError', {
|
||||
defaultMessage: 'At least one index pattern is required.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (config && Array.isArray(config.indices) && config.indices.length === 0) {
|
||||
validation.errors.indices.push(
|
||||
i18n.translate('xpack.snapshotRestore.policyValidation.indicesRequiredError', {
|
||||
defaultMessage: 'You must select at least one index.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Remove fields with no errors
|
||||
validation.errors = Object.entries(validation.errors)
|
||||
.filter(([key, value]) => value.length > 0)
|
||||
.reduce((errs: PolicyValidation['errors'], [key, value]) => {
|
||||
errs[key] = value;
|
||||
return errs;
|
||||
}, {});
|
||||
|
||||
// Set overall validations status
|
||||
if (Object.keys(validation.errors).length > 0) {
|
||||
validation.isValid = false;
|
||||
}
|
||||
|
||||
return validation;
|
||||
};
|
|
@ -11,7 +11,7 @@ import { AppCore, AppPlugins } from './app/types';
|
|||
import template from './index.html';
|
||||
import { Core, Plugins } from './shim';
|
||||
|
||||
import { breadcrumbService } from './app/services/navigation';
|
||||
import { breadcrumbService, docTitleService } from './app/services/navigation';
|
||||
import { documentationLinksService } from './app/services/documentation';
|
||||
import { httpService } from './app/services/http';
|
||||
import { textService } from './app/services/text';
|
||||
|
@ -21,7 +21,7 @@ const REACT_ROOT_ID = 'snapshotRestoreReactRoot';
|
|||
|
||||
export class Plugin {
|
||||
public start(core: Core, plugins: Plugins): void {
|
||||
const { i18n, routing, http, chrome, notification, documentation } = core;
|
||||
const { i18n, routing, http, chrome, notification, documentation, docTitle } = core;
|
||||
const { management, uiMetric } = plugins;
|
||||
|
||||
// Register management section
|
||||
|
@ -38,8 +38,13 @@ export class Plugin {
|
|||
// Initialize services
|
||||
textService.init(i18n);
|
||||
breadcrumbService.init(chrome, management.constants.BREADCRUMB);
|
||||
documentationLinksService.init(documentation.esDocBasePath, documentation.esPluginDocBasePath);
|
||||
uiMetricService.init(uiMetric.createUiStatsReporter);
|
||||
documentationLinksService.init(
|
||||
documentation.esDocBasePath,
|
||||
documentation.esPluginDocBasePath,
|
||||
documentation.esStackOverviewDocBasePath
|
||||
);
|
||||
docTitleService.init(docTitle.change);
|
||||
|
||||
const unmountReactApp = (): void => {
|
||||
const elem = document.getElementById(REACT_ROOT_ID);
|
||||
|
|
|
@ -11,3 +11,8 @@ export {
|
|||
sendRequest,
|
||||
useRequest,
|
||||
} from '../../../../../src/plugins/es_ui_shared/public/request';
|
||||
|
||||
export {
|
||||
CronEditor,
|
||||
DAY,
|
||||
} from '../../../../../src/plugins/es_ui_shared/public/components/cron_editor';
|
||||
|
|
|
@ -12,6 +12,7 @@ import { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } from 'ui/documentation_links';
|
|||
import { management, MANAGEMENT_BREADCRUMB } from 'ui/management';
|
||||
import { fatalError, toastNotifications } from 'ui/notify';
|
||||
import routes from 'ui/routes';
|
||||
import { docTitle } from 'ui/doc_title/doc_title';
|
||||
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
|
||||
|
@ -52,6 +53,10 @@ export interface Core extends AppCore {
|
|||
documentation: {
|
||||
esDocBasePath: string;
|
||||
esPluginDocBasePath: string;
|
||||
esStackOverviewDocBasePath: string;
|
||||
};
|
||||
docTitle: {
|
||||
change: typeof docTitle.change;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -108,6 +113,10 @@ export function createShim(): { core: Core; plugins: Plugins } {
|
|||
documentation: {
|
||||
esDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`,
|
||||
esPluginDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/plugins/${DOC_LINK_VERSION}/`,
|
||||
esStackOverviewDocBasePath: `${ELASTIC_WEBSITE_URL}guide/en/elastic-stack-overview/${DOC_LINK_VERSION}/`,
|
||||
},
|
||||
docTitle: {
|
||||
change: docTitle.change,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
|
|
|
@ -60,4 +60,18 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
|
|||
],
|
||||
method: 'PUT',
|
||||
});
|
||||
|
||||
slm.updatePolicy = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_slm/policy/<%=name%>',
|
||||
req: {
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
method: 'PUT',
|
||||
});
|
||||
};
|
||||
|
|
|
@ -9,7 +9,5 @@ export {
|
|||
serializeRepositorySettings,
|
||||
} from './repository_serialization';
|
||||
export { cleanSettings } from './clean_settings';
|
||||
export { deserializeSnapshotDetails, deserializeSnapshotConfig } from './snapshot_serialization';
|
||||
export { deserializeRestoreShard } from './restore_serialization';
|
||||
export { getManagedRepositoryName } from './get_managed_repository_name';
|
||||
export { deserializePolicy } from './policy_serialization';
|
||||
export { deserializeRestoreShard } from './restore_serialization';
|
||||
|
|
|
@ -8,6 +8,7 @@ import { wrapCustomError } from '../../../../../server/lib/create_router/error_w
|
|||
import {
|
||||
APP_REQUIRED_CLUSTER_PRIVILEGES,
|
||||
APP_RESTORE_INDEX_PRIVILEGES,
|
||||
APP_SLM_CLUSTER_PRIVILEGES,
|
||||
} from '../../../common/constants';
|
||||
// NOTE: now we import it from our "public" folder, but when the Authorisation lib
|
||||
// will move to the "es_ui_shared" plugin, it will be imported from its "static" folder
|
||||
|
@ -65,7 +66,7 @@ export const getPrivilegesHandler: RouterRouteHandler = async (
|
|||
path: '/_security/user/_has_privileges',
|
||||
method: 'POST',
|
||||
body: {
|
||||
cluster: APP_REQUIRED_CLUSTER_PRIVILEGES,
|
||||
cluster: [...APP_REQUIRED_CLUSTER_PRIVILEGES, ...APP_SLM_CLUSTER_PRIVILEGES],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -4,7 +4,15 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { Request, ResponseToolkit } from 'hapi';
|
||||
import { getAllHandler, getOneHandler, executeHandler, deleteHandler } from './policy';
|
||||
import {
|
||||
getAllHandler,
|
||||
getOneHandler,
|
||||
executeHandler,
|
||||
deleteHandler,
|
||||
createHandler,
|
||||
updateHandler,
|
||||
getIndicesHandler,
|
||||
} from './policy';
|
||||
|
||||
describe('[Snapshot and Restore API Routes] Restore', () => {
|
||||
const mockRequest = {} as Request;
|
||||
|
@ -209,4 +217,110 @@ describe('[Snapshot and Restore API Routes] Restore', () => {
|
|||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createHandler()', () => {
|
||||
const name = 'fooPolicy';
|
||||
const mockCreateRequest = ({
|
||||
payload: {
|
||||
name,
|
||||
},
|
||||
} as unknown) as Request;
|
||||
|
||||
it('should return successful ES response', async () => {
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
const callWithRequest = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce({})
|
||||
.mockReturnValueOnce(mockEsResponse);
|
||||
const expectedResponse = { ...mockEsResponse };
|
||||
await expect(
|
||||
createHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should return error if policy with the same name already exists', async () => {
|
||||
const mockEsResponse = { [name]: {} };
|
||||
const callWithRequest = jest.fn().mockReturnValue(mockEsResponse);
|
||||
await expect(
|
||||
createHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
const callWithRequest = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce({})
|
||||
.mockRejectedValueOnce(new Error());
|
||||
await expect(
|
||||
createHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateHandler()', () => {
|
||||
const name = 'fooPolicy';
|
||||
const mockCreateRequest = ({
|
||||
params: {
|
||||
name,
|
||||
},
|
||||
payload: {
|
||||
name,
|
||||
},
|
||||
} as unknown) as Request;
|
||||
|
||||
it('should return successful ES response', async () => {
|
||||
const mockEsResponse = { acknowledged: true };
|
||||
const callWithRequest = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce({ [name]: {} })
|
||||
.mockReturnValueOnce(mockEsResponse);
|
||||
const expectedResponse = { ...mockEsResponse };
|
||||
await expect(
|
||||
updateHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
const callWithRequest = jest.fn().mockRejectedValueOnce(new Error());
|
||||
await expect(
|
||||
updateHandler(mockCreateRequest, callWithRequest, mockResponseToolkit)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIndicesHandler()', () => {
|
||||
it('should arrify and sort index names returned from ES', async () => {
|
||||
const mockEsResponse = [
|
||||
{
|
||||
index: 'fooIndex',
|
||||
},
|
||||
{
|
||||
index: 'barIndex',
|
||||
},
|
||||
];
|
||||
const callWithRequest = jest.fn().mockReturnValueOnce(mockEsResponse);
|
||||
const expectedResponse = {
|
||||
indices: ['barIndex', 'fooIndex'],
|
||||
};
|
||||
await expect(
|
||||
getIndicesHandler(mockRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should return empty array if no indices returned from ES', async () => {
|
||||
const mockEsResponse: any[] = [];
|
||||
const callWithRequest = jest.fn().mockReturnValueOnce(mockEsResponse);
|
||||
const expectedResponse = { indices: [] };
|
||||
await expect(
|
||||
getIndicesHandler(mockRequest, callWithRequest, mockResponseToolkit)
|
||||
).resolves.toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('should throw if ES error', async () => {
|
||||
const callWithRequest = jest.fn().mockRejectedValueOnce(new Error());
|
||||
await expect(
|
||||
getIndicesHandler(mockRequest, callWithRequest, mockResponseToolkit)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,14 +8,17 @@ import {
|
|||
wrapCustomError,
|
||||
wrapEsError,
|
||||
} from '../../../../../server/lib/create_router/error_wrappers';
|
||||
import { SlmPolicyEs, SlmPolicy } from '../../../common/types';
|
||||
import { deserializePolicy } from '../../lib';
|
||||
import { SlmPolicyEs, SlmPolicy, SlmPolicyPayload } from '../../../common/types';
|
||||
import { deserializePolicy, serializePolicy } from '../../../common/lib';
|
||||
|
||||
export function registerPolicyRoutes(router: Router) {
|
||||
router.get('policies', getAllHandler);
|
||||
router.get('policy/{name}', getOneHandler);
|
||||
router.post('policy/{name}/run', executeHandler);
|
||||
router.delete('policies/{names}', deleteHandler);
|
||||
router.put('policies', createHandler);
|
||||
router.put('policies/{name}', updateHandler);
|
||||
router.get('policies/indices', getIndicesHandler);
|
||||
}
|
||||
|
||||
export const getAllHandler: RouterRouteHandler = async (
|
||||
|
@ -96,3 +99,65 @@ export const deleteHandler: RouterRouteHandler = async (req, callWithRequest) =>
|
|||
|
||||
return response;
|
||||
};
|
||||
|
||||
export const createHandler: RouterRouteHandler = async (req, callWithRequest) => {
|
||||
const policy = req.payload as SlmPolicyPayload;
|
||||
const { name } = policy;
|
||||
const conflictError = wrapCustomError(
|
||||
new Error('There is already a policy with that name.'),
|
||||
409
|
||||
);
|
||||
|
||||
// Check that policy with the same name doesn't already exist
|
||||
try {
|
||||
const policyByName = await callWithRequest('slm.policy', { name });
|
||||
if (policyByName[name]) {
|
||||
throw conflictError;
|
||||
}
|
||||
} catch (e) {
|
||||
// Rethrow conflict error but silently swallow all others
|
||||
if (e === conflictError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise create new policy
|
||||
return await callWithRequest('slm.updatePolicy', {
|
||||
name,
|
||||
body: serializePolicy(policy),
|
||||
});
|
||||
};
|
||||
|
||||
export const updateHandler: RouterRouteHandler = async (req, callWithRequest) => {
|
||||
const { name } = req.params;
|
||||
const policy = req.payload as SlmPolicyPayload;
|
||||
|
||||
// Check that policy with the given name exists
|
||||
// If it doesn't exist, 404 will be thrown by ES and will be returned
|
||||
await callWithRequest('slm.policy', { name });
|
||||
|
||||
// Otherwise update policy
|
||||
return await callWithRequest('slm.updatePolicy', {
|
||||
name,
|
||||
body: serializePolicy(policy),
|
||||
});
|
||||
};
|
||||
|
||||
export const getIndicesHandler: RouterRouteHandler = async (
|
||||
req,
|
||||
callWithRequest
|
||||
): Promise<{
|
||||
indices: string[];
|
||||
}> => {
|
||||
// Get indices
|
||||
const indices: Array<{
|
||||
index: string;
|
||||
}> = await callWithRequest('cat.indices', {
|
||||
format: 'json',
|
||||
h: 'index',
|
||||
});
|
||||
|
||||
return {
|
||||
indices: indices.map(({ index }) => index).sort(),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -55,6 +55,10 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
|
|||
const mockRequest = {} as Request;
|
||||
|
||||
test('combines snapshots and their repositories returned from ES', async () => {
|
||||
const mockSnapshotGetPolicyEsResponse = {
|
||||
fooPolicy: {},
|
||||
};
|
||||
|
||||
const mockSnapshotGetRepositoryEsResponse = {
|
||||
fooRepository: {},
|
||||
barRepository: {},
|
||||
|
@ -78,6 +82,7 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
|
|||
|
||||
const callWithRequest = jest
|
||||
.fn()
|
||||
.mockReturnValueOnce(mockSnapshotGetPolicyEsResponse)
|
||||
.mockReturnValueOnce(mockSnapshotGetRepositoryEsResponse)
|
||||
.mockReturnValueOnce(mockGetSnapshotsFooResponse)
|
||||
.mockReturnValueOnce(mockGetSnapshotsBarResponse);
|
||||
|
@ -85,6 +90,7 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
|
|||
const expectedResponse = {
|
||||
errors: {},
|
||||
repositories: ['fooRepository', 'barRepository'],
|
||||
policies: ['fooPolicy'],
|
||||
snapshots: [
|
||||
{
|
||||
...defaultSnapshot,
|
||||
|
@ -106,12 +112,17 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => {
|
|||
});
|
||||
|
||||
test('returns empty arrays if no snapshots returned from ES', async () => {
|
||||
const mockSnapshotGetPolicyEsResponse = {};
|
||||
const mockSnapshotGetRepositoryEsResponse = {};
|
||||
const callWithRequest = jest.fn().mockReturnValue(mockSnapshotGetRepositoryEsResponse);
|
||||
const callWithRequest = jest
|
||||
.fn()
|
||||
.mockReturnValue(mockSnapshotGetPolicyEsResponse)
|
||||
.mockReturnValue(mockSnapshotGetRepositoryEsResponse);
|
||||
const expectedResponse = {
|
||||
errors: [],
|
||||
snapshots: [],
|
||||
repositories: [],
|
||||
policies: [],
|
||||
};
|
||||
|
||||
const response = await getAllHandler(mockRequest, callWithRequest, mockResponseToolkit);
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
import { Router, RouterRouteHandler } from '../../../../../server/lib/create_router';
|
||||
import { wrapEsError } from '../../../../../server/lib/create_router/error_wrappers';
|
||||
import { SnapshotDetails, SnapshotDetailsEs } from '../../../common/types';
|
||||
import { deserializeSnapshotDetails } from '../../../common/lib';
|
||||
import { Plugins } from '../../../shim';
|
||||
import { deserializeSnapshotDetails, getManagedRepositoryName } from '../../lib';
|
||||
import { getManagedRepositoryName } from '../../lib';
|
||||
|
||||
let callWithInternalUser: any;
|
||||
|
||||
|
@ -24,10 +25,22 @@ export const getAllHandler: RouterRouteHandler = async (
|
|||
): Promise<{
|
||||
snapshots: SnapshotDetails[];
|
||||
errors: any[];
|
||||
policies: string[];
|
||||
repositories: string[];
|
||||
managedRepository?: string;
|
||||
}> => {
|
||||
const managedRepository = await getManagedRepositoryName(callWithInternalUser);
|
||||
let policies: string[] = [];
|
||||
|
||||
// Attempt to retrieve policies
|
||||
// This could fail if user doesn't have access to read SLM policies
|
||||
try {
|
||||
const policiesByName = await callWithRequest('slm.policies');
|
||||
policies = Object.keys(policiesByName);
|
||||
} catch (e) {
|
||||
// Silently swallow error as policy names aren't required in UI
|
||||
}
|
||||
|
||||
const repositoriesByName = await callWithRequest('snapshot.getRepository', {
|
||||
repository: '_all',
|
||||
});
|
||||
|
@ -35,7 +48,7 @@ export const getAllHandler: RouterRouteHandler = async (
|
|||
const repositoryNames = Object.keys(repositoriesByName);
|
||||
|
||||
if (repositoryNames.length === 0) {
|
||||
return { snapshots: [], errors: [], repositories: [] };
|
||||
return { snapshots: [], errors: [], repositories: [], policies };
|
||||
}
|
||||
|
||||
const snapshots: SnapshotDetails[] = [];
|
||||
|
@ -70,6 +83,7 @@ export const getAllHandler: RouterRouteHandler = async (
|
|||
|
||||
return {
|
||||
snapshots,
|
||||
policies,
|
||||
repositories,
|
||||
errors,
|
||||
};
|
||||
|
|
|
@ -8712,26 +8712,6 @@
|
|||
"xpack.rollupJobs.createAction.jobIdAlreadyExistsErrorMessage": "ID「{jobConfigId}」のジョブが既に存在します。",
|
||||
"xpack.rollupJobs.createBreadcrumbTitle": "作成",
|
||||
"xpack.rollupJobs.createTitle": "ロールアップジョブを作成",
|
||||
"xpack.rollupJobs.cronEditor.cronDaily.fieldHour.textAtLabel": "時点で",
|
||||
"xpack.rollupJobs.cronEditor.cronDaily.fieldTimeLabel": "時間",
|
||||
"xpack.rollupJobs.cronEditor.cronHourly.fieldMinute.textAtLabel": "時点で",
|
||||
"xpack.rollupJobs.cronEditor.cronHourly.fieldTimeLabel": "分",
|
||||
"xpack.rollupJobs.cronEditor.cronMonthly.fieldDateLabel": "日付",
|
||||
"xpack.rollupJobs.cronEditor.cronMonthly.fieldHour.textAtLabel": "時点で",
|
||||
"xpack.rollupJobs.cronEditor.cronMonthly.fieldTimeLabel": "時間",
|
||||
"xpack.rollupJobs.cronEditor.cronMonthly.textOnTheLabel": "On the",
|
||||
"xpack.rollupJobs.cronEditor.cronWeekly.fieldDateLabel": "日",
|
||||
"xpack.rollupJobs.cronEditor.cronWeekly.fieldHour.textAtLabel": "時点で",
|
||||
"xpack.rollupJobs.cronEditor.cronWeekly.fieldTimeLabel": "時間",
|
||||
"xpack.rollupJobs.cronEditor.cronWeekly.textOnLabel": "オン",
|
||||
"xpack.rollupJobs.cronEditor.cronYearly.fieldDate.textOnTheLabel": "On the",
|
||||
"xpack.rollupJobs.cronEditor.cronYearly.fieldDateLabel": "日付",
|
||||
"xpack.rollupJobs.cronEditor.cronYearly.fieldHour.textAtLabel": "時点で",
|
||||
"xpack.rollupJobs.cronEditor.cronYearly.fieldMonth.textInLabel": "In",
|
||||
"xpack.rollupJobs.cronEditor.cronYearly.fieldMonthLabel": "月",
|
||||
"xpack.rollupJobs.cronEditor.cronYearly.fieldTimeLabel": "時間",
|
||||
"xpack.rollupJobs.cronEditor.fieldFrequencyLabel": "頻度",
|
||||
"xpack.rollupJobs.cronEditor.textEveryLabel": "毎",
|
||||
"xpack.rollupJobs.deleteAction.errorTitle": "ロールアップジョブの削除中にエラーが発生",
|
||||
"xpack.rollupJobs.deleteAction.successMultipleNotificationTitle": "{count} 件のロールアップジョブが削除されました",
|
||||
"xpack.rollupJobs.deleteAction.successSingleNotificationTitle": "ロールアップジョブ「{jobId}」が削除されました",
|
||||
|
@ -8815,25 +8795,6 @@
|
|||
"xpack.rollupJobs.rollupIndexPatternsTitle": "ロールアップインデックスパターンを有効にする",
|
||||
"xpack.rollupJobs.startJobsAction.errorTitle": "ロールアップジョブの開始中にエラーが発生",
|
||||
"xpack.rollupJobs.stopJobsAction.errorTitle": "ロールアップジョブの停止中にエラーが発生",
|
||||
"xpack.rollupJobs.util.day.friday": "金曜日",
|
||||
"xpack.rollupJobs.util.day.monday": "月曜日",
|
||||
"xpack.rollupJobs.util.day.saturday": "土曜日",
|
||||
"xpack.rollupJobs.util.day.sunday": "日曜日",
|
||||
"xpack.rollupJobs.util.day.thursday": "木曜日",
|
||||
"xpack.rollupJobs.util.day.tuesday": "火曜日",
|
||||
"xpack.rollupJobs.util.day.wednesday": "水曜日",
|
||||
"xpack.rollupJobs.util.month.april": "4 月",
|
||||
"xpack.rollupJobs.util.month.august": "8 月",
|
||||
"xpack.rollupJobs.util.month.december": "12 月",
|
||||
"xpack.rollupJobs.util.month.february": "2 月",
|
||||
"xpack.rollupJobs.util.month.january": "1 月",
|
||||
"xpack.rollupJobs.util.month.july": "7 月",
|
||||
"xpack.rollupJobs.util.month.june": "6 月",
|
||||
"xpack.rollupJobs.util.month.march": "3 月",
|
||||
"xpack.rollupJobs.util.month.may": "5 月",
|
||||
"xpack.rollupJobs.util.month.november": "11 月",
|
||||
"xpack.rollupJobs.util.month.october": "10 月",
|
||||
"xpack.rollupJobs.util.month.september": "9 月",
|
||||
"xpack.searchProfiler.aggregationProfileTabTitle": "集約プロフィール",
|
||||
"xpack.searchProfiler.basicLicenseTitle": "ベーシック",
|
||||
"xpack.searchProfiler.formIndexLabel": "インデックス",
|
||||
|
|
|
@ -8855,26 +8855,6 @@
|
|||
"xpack.rollupJobs.createAction.jobIdAlreadyExistsErrorMessage": "ID 为 “{jobConfigId}” 的作业已存在。",
|
||||
"xpack.rollupJobs.createBreadcrumbTitle": "创建",
|
||||
"xpack.rollupJobs.createTitle": "创建汇总/打包作业",
|
||||
"xpack.rollupJobs.cronEditor.cronDaily.fieldHour.textAtLabel": "在",
|
||||
"xpack.rollupJobs.cronEditor.cronDaily.fieldTimeLabel": "时间",
|
||||
"xpack.rollupJobs.cronEditor.cronHourly.fieldMinute.textAtLabel": "在",
|
||||
"xpack.rollupJobs.cronEditor.cronHourly.fieldTimeLabel": "分钟",
|
||||
"xpack.rollupJobs.cronEditor.cronMonthly.fieldDateLabel": "日期",
|
||||
"xpack.rollupJobs.cronEditor.cronMonthly.fieldHour.textAtLabel": "在",
|
||||
"xpack.rollupJobs.cronEditor.cronMonthly.fieldTimeLabel": "时间",
|
||||
"xpack.rollupJobs.cronEditor.cronMonthly.textOnTheLabel": "处于",
|
||||
"xpack.rollupJobs.cronEditor.cronWeekly.fieldDateLabel": "天",
|
||||
"xpack.rollupJobs.cronEditor.cronWeekly.fieldHour.textAtLabel": "在",
|
||||
"xpack.rollupJobs.cronEditor.cronWeekly.fieldTimeLabel": "时间",
|
||||
"xpack.rollupJobs.cronEditor.cronWeekly.textOnLabel": "开启",
|
||||
"xpack.rollupJobs.cronEditor.cronYearly.fieldDate.textOnTheLabel": "处于",
|
||||
"xpack.rollupJobs.cronEditor.cronYearly.fieldDateLabel": "日期",
|
||||
"xpack.rollupJobs.cronEditor.cronYearly.fieldHour.textAtLabel": "在",
|
||||
"xpack.rollupJobs.cronEditor.cronYearly.fieldMonth.textInLabel": "于",
|
||||
"xpack.rollupJobs.cronEditor.cronYearly.fieldMonthLabel": "月",
|
||||
"xpack.rollupJobs.cronEditor.cronYearly.fieldTimeLabel": "时间",
|
||||
"xpack.rollupJobs.cronEditor.fieldFrequencyLabel": "频率",
|
||||
"xpack.rollupJobs.cronEditor.textEveryLabel": "所有",
|
||||
"xpack.rollupJobs.deleteAction.errorTitle": "删除汇总/打包作业时出错",
|
||||
"xpack.rollupJobs.deleteAction.successMultipleNotificationTitle": "已删除 {count} 个汇总/打包作业",
|
||||
"xpack.rollupJobs.deleteAction.successSingleNotificationTitle": "已删除汇总/打包作业“{jobId}”",
|
||||
|
@ -8958,25 +8938,6 @@
|
|||
"xpack.rollupJobs.rollupIndexPatternsTitle": "启用汇总索引模式",
|
||||
"xpack.rollupJobs.startJobsAction.errorTitle": "启动汇总/打包作业时出错",
|
||||
"xpack.rollupJobs.stopJobsAction.errorTitle": "停止汇总/打包作业时出错",
|
||||
"xpack.rollupJobs.util.day.friday": "星期五",
|
||||
"xpack.rollupJobs.util.day.monday": "星期一",
|
||||
"xpack.rollupJobs.util.day.saturday": "星期六",
|
||||
"xpack.rollupJobs.util.day.sunday": "星期日",
|
||||
"xpack.rollupJobs.util.day.thursday": "星期四",
|
||||
"xpack.rollupJobs.util.day.tuesday": "星期二",
|
||||
"xpack.rollupJobs.util.day.wednesday": "星期三",
|
||||
"xpack.rollupJobs.util.month.april": "四月",
|
||||
"xpack.rollupJobs.util.month.august": "八月",
|
||||
"xpack.rollupJobs.util.month.december": "十二月",
|
||||
"xpack.rollupJobs.util.month.february": "二月",
|
||||
"xpack.rollupJobs.util.month.january": "一月",
|
||||
"xpack.rollupJobs.util.month.july": "七月",
|
||||
"xpack.rollupJobs.util.month.june": "六月",
|
||||
"xpack.rollupJobs.util.month.march": "三月",
|
||||
"xpack.rollupJobs.util.month.may": "五月",
|
||||
"xpack.rollupJobs.util.month.november": "十一月",
|
||||
"xpack.rollupJobs.util.month.october": "十月",
|
||||
"xpack.rollupJobs.util.month.september": "九月",
|
||||
"xpack.searchProfiler.aggregationProfileTabTitle": "聚合配置文件",
|
||||
"xpack.searchProfiler.basicLicenseTitle": "基础级",
|
||||
"xpack.searchProfiler.formIndexLabel": "索引",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue