feat(backup-ng/new): group saved & new schedules (#3093)

See #2711
This commit is contained in:
badrAZ
2018-06-22 16:23:12 +02:00
committed by Julien Fontanet
parent 915052d5f6
commit 3b244c24d7
4 changed files with 135 additions and 275 deletions

View File

@@ -1255,8 +1255,6 @@ const messages = {
// ---- Backup views ---
backupSchedules: 'Schedules',
backupSavedSchedules: 'Saved schedules',
backupNewSchedules: 'New schedules',
scheduleCron: 'Cron pattern',
scheduleName: 'Name',
scheduleTimezone: 'Timezone',

View File

@@ -2,7 +2,7 @@ import addSubscriptions from 'add-subscriptions'
import React from 'react'
import { injectState, provideState } from '@julien-f/freactal'
import { subscribeBackupNgJobs, subscribeSchedules } from 'xo'
import { find, groupBy } from 'lodash'
import { find, groupBy, keyBy } from 'lodash'
import New from './new'
@@ -18,7 +18,7 @@ export default [
computed: {
job: (_, { jobs, routeParams: { id } }) => find(jobs, { id }),
schedules: (_, { schedulesByJob, routeParams: { id } }) =>
schedulesByJob && schedulesByJob[id],
schedulesByJob && keyBy(schedulesByJob[id], 'id'),
},
}),
injectState,

View File

@@ -19,14 +19,14 @@ import { injectState, provideState } from '@julien-f/freactal'
import { SelectRemote, SelectSr, SelectVm } from 'select-objects'
import { Toggle } from 'form'
import {
find,
findKey,
cloneDeep,
flatten,
forEach,
includes,
isEmpty,
keyBy,
map,
mapValues,
some,
} from 'lodash'
import {
@@ -45,9 +45,9 @@ import { FormFeedback, FormGroup, Input, Number, Ul, Li } from './utils'
// ===================================================================
const normaliseTagValues = values => resolveIds(values).map(value => [value])
const normalizeTagValues = values => resolveIds(values).map(value => [value])
const normaliseCopyRentention = settings => {
const normalizeCopyRetention = settings => {
forEach(settings, schedule => {
if (schedule.copyRetention === undefined) {
schedule.copyRetention = schedule.exportRetention
@@ -55,7 +55,7 @@ const normaliseCopyRentention = settings => {
})
}
const normaliseSettings = ({
const normalizeSettings = ({
settings,
exportMode,
copyMode,
@@ -101,35 +101,6 @@ const destructVmsPattern = pattern =>
vms: destructPattern(pattern),
}
const getNewSettings = ({ schedules, exportMode, copyMode, snapshotMode }) => {
const newSettings = {}
for (const id in schedules) {
newSettings[id] = {
exportRetention: exportMode ? schedules[id].exportRetention : undefined,
copyRetention: copyMode ? schedules[id].copyRetention : undefined,
snapshotRetention: snapshotMode
? schedules[id].snapshotRetention
: undefined,
}
}
return newSettings
}
const getNewSchedules = schedules => {
const newSchedules = {}
for (const id in schedules) {
newSchedules[id] = {
cron: schedules[id].cron,
timezone: schedules[id].timezone,
}
}
return newSchedules
}
const REPORT_WHEN_FILTER_OPTIONS = [
{
label: 'reportWhenAlways',
@@ -158,13 +129,12 @@ const getInitialState = () => ({
editionMode: undefined,
formId: generateRandomId(),
name: '',
newSchedules: {},
offlineSnapshot: false,
paramsUpdated: false,
powerState: 'All',
remotes: [],
reportWhen: 'failure',
schedules: [],
schedules: {},
settings: {},
showErrors: false,
smartMode: false,
@@ -202,10 +172,13 @@ export default [
name: state.name,
mode: state.isDelta ? 'delta' : 'full',
compression: state.compression ? 'native' : '',
schedules: getNewSchedules(state.newSchedules),
schedules: mapValues(
state.schedules,
({ id, ...schedule }) => schedule
),
settings: {
...getNewSettings({
schedules: state.newSchedules,
...normalizeSettings({
settings: cloneDeep(state.settings),
exportMode: state.exportMode,
copyMode: state.copyMode,
snapshotMode: state.snapshotMode,
@@ -237,75 +210,50 @@ export default [
}
}
const newSettings = {}
if (!isEmpty(state.newSchedules)) {
await Promise.all(
map(state.newSchedules, async schedule => {
const scheduleId = (await createSchedule(props.job.id, {
cron: schedule.cron,
timezone: schedule.timezone,
})).id
newSettings[scheduleId] = {
exportRetention: schedule.exportRetention,
copyRetention: schedule.copyRetention,
snapshotRetention: schedule.snapshotRetention,
}
})
)
}
await Promise.all(
map(props.schedules, oldSchedule => {
const scheduleId = oldSchedule.id
const newSchedule = find(state.schedules, { id: scheduleId })
if (
newSchedule !== undefined &&
newSchedule.cron === oldSchedule.cron &&
newSchedule.timezone === oldSchedule.timezone
) {
return
}
const id = oldSchedule.id
const newSchedule = state.schedules[id]
if (newSchedule === undefined) {
return deleteSchedule(scheduleId)
return deleteSchedule(id)
}
return editSchedule({
id: scheduleId,
jobId: props.job.id,
cron: newSchedule.cron,
timezone: newSchedule.timezone,
})
if (
newSchedule.cron !== oldSchedule.cron ||
newSchedule.timezone !== oldSchedule.timezone
) {
return editSchedule({
id,
jobId: props.job.id,
cron: newSchedule.cron,
timezone: newSchedule.timezone,
})
}
})
)
const oldSettings = props.job.settings
const settings = state.settings
if (!('' in oldSettings)) {
oldSettings[''] = {}
}
for (const id in oldSettings) {
const oldSetting = oldSettings[id]
const newSetting = settings[id]
const settings = cloneDeep(state.settings)
await Promise.all([
...map(state.schedules, async schedule => {
const tmpId = schedule.id
if (props.schedules[tmpId] === undefined) {
const { id } = await createSchedule(props.job.id, {
cron: schedule.cron,
timezone: schedule.timezone,
})
if (id === '') {
oldSetting.reportWhen = state.reportWhen
oldSetting.concurrency = state.concurrency
oldSetting.offlineSnapshot = state.offlineSnapshot
} else if (!(id in settings)) {
delete oldSettings[id]
} else if (
oldSetting.snapshotRetention !== newSetting.snapshotRetention ||
oldSetting.exportRetention !== newSetting.exportRetention ||
oldSetting.copyRetention !== newSetting.copyRetention
) {
newSettings[id] = {
exportRetention: newSetting.exportRetention,
copyRetention: newSetting.copyRetention,
snapshotRetention: newSetting.snapshotRetention,
settings[id] = settings[tmpId]
delete settings[tmpId]
}
}
}),
])
settings[''] = {
...props.job.settings[''],
reportWhen: state.reportWhen,
concurrency: state.concurrency,
offlineSnapshot: state.offlineSnapshot,
}
await editBackupNgJob({
@@ -313,11 +261,8 @@ export default [
name: state.name,
mode: state.isDelta ? 'delta' : 'full',
compression: state.compression ? 'native' : '',
settings: normaliseSettings({
settings: {
...oldSettings,
...newSettings,
},
settings: normalizeSettings({
settings,
exportMode: state.exportMode,
copyMode: state.copyMode,
snapshotMode: state.snapshotMode,
@@ -384,13 +329,13 @@ export default [
const srs = job.srs !== undefined ? destructPattern(job.srs) : []
const { concurrency, reportWhen, offlineSnapshot } =
job.settings[''] || {}
const settings = { ...job.settings }
const settings = cloneDeep(job.settings)
delete settings['']
const drMode = job.mode === 'full' && !isEmpty(srs)
const crMode = job.mode === 'delta' && !isEmpty(srs)
if (drMode || crMode) {
normaliseCopyRentention(settings)
normalizeCopyRetention(settings)
}
return {
@@ -426,42 +371,24 @@ export default [
tmpSchedule: {},
editionMode: undefined,
}),
editSchedule: (_, schedule) => state => {
const { snapshotRetention, exportRetention, copyRetention } =
state.settings[schedule.id] || {}
return {
...state,
editionMode: 'editSchedule',
tmpSchedule: {
exportRetention,
copyRetention,
snapshotRetention,
...schedule,
},
}
},
deleteSchedule: (_, id) => async (state, props) => {
const schedules = [...state.schedules]
schedules.splice(findKey(state.schedules, { id }), 1)
return {
...state,
schedules,
}
},
editNewSchedule: (_, schedule) => state => ({
editSchedule: (_, schedule) => state => ({
...state,
editionMode: 'editNewSchedule',
editionMode: 'editSchedule',
tmpSchedule: {
...schedule,
},
}),
deleteNewSchedule: (_, id) => async (state, props) => {
const newSchedules = { ...state.newSchedules }
delete newSchedules[id]
deleteSchedule: (_, schedule) => state => {
const id = resolveId(schedule)
const schedules = { ...state.schedules }
const settings = { ...state.settings }
delete schedules[id]
delete settings[id]
return {
...state,
newSchedules,
schedules,
settings,
}
},
saveSchedule: (
@@ -469,14 +396,21 @@ export default [
{ cron, timezone, exportRetention, copyRetention, snapshotRetention }
) => async (state, props) => {
if (state.editionMode === 'creation') {
const id = generateRandomId()
return {
...state,
editionMode: undefined,
newSchedules: {
...state.newSchedules,
[generateRandomId()]: {
schedules: {
...state.schedules,
[id]: {
id,
cron,
timezone,
},
},
settings: {
...state.settings,
[id]: {
exportRetention,
copyRetention,
snapshotRetention,
@@ -486,45 +420,27 @@ export default [
}
const id = state.tmpSchedule.id
if (state.editionMode === 'editSchedule') {
const scheduleKey = findKey(state.schedules, { id })
const schedules = [...state.schedules]
schedules[scheduleKey] = {
...schedules[scheduleKey],
cron,
timezone,
}
const schedules = { ...state.schedules }
const settings = { ...state.settings }
const settings = { ...state.settings }
settings[id] = {
exportRetention,
copyRetention,
snapshotRetention,
}
return {
...state,
editionMode: undefined,
schedules,
settings,
tmpSchedule: {},
}
schedules[id] = {
...schedules[id],
cron,
timezone,
}
settings[id] = {
...settings[id],
exportRetention,
copyRetention,
snapshotRetention,
}
return {
...state,
editionMode: undefined,
schedules,
settings,
tmpSchedule: {},
newSchedules: {
...state.newSchedules,
[id]: {
cron,
timezone,
exportRetention,
copyRetention,
snapshotRetention,
},
},
}
},
setPowerState: (_, powerState) => state => ({
@@ -628,7 +544,7 @@ export default [
vmsSmartPattern: ({ $pool, powerState, tags }) => ({
$pool: constructSmartPattern($pool, resolveIds),
power_state: powerState === 'All' ? undefined : powerState,
tags: constructSmartPattern(tags, normaliseTagValues),
tags: constructSmartPattern(tags, normalizeTagValues),
type: 'VM',
}),
srPredicate: ({ srs }) => sr => isSrWritable(sr) && !includes(srs, sr.id),

View File

@@ -4,62 +4,10 @@ import React from 'react'
import SortedTable from 'sorted-table'
import { Card, CardBlock, CardHeader } from 'card'
import { injectState, provideState } from '@julien-f/freactal'
import { isEmpty, find, findKey, size } from 'lodash'
import { isEmpty, find, size } from 'lodash'
import NewSchedule from './new-schedule'
import { FormFeedback, FormGroup } from './utils'
// ===================================================================
const rowTransform = (schedule, { settings }) => {
const { exportRetention, copyRetention, snapshotRetention } =
settings[schedule.id] || {}
return {
...schedule,
exportRetention,
copyRetention,
snapshotRetention,
}
}
const SAVED_SCHEDULES_INDIVIDUAL_ACTIONS = [
{
handler: (schedule, { editSchedule }) => editSchedule(schedule),
label: _('scheduleEdit'),
icon: 'edit',
disabled: (_, { disabledEdition }) => disabledEdition,
level: 'primary',
},
{
handler: (schedule, { deleteSchedule }) => deleteSchedule(schedule.id),
label: _('scheduleDelete'),
disabled: (_, { disabledDeletion }) => disabledDeletion,
icon: 'delete',
level: 'danger',
},
]
const NEW_SCHEDULES_INDIVIDUAL_ACTIONS = [
{
handler: (schedule, { editNewSchedule, newSchedules }) =>
editNewSchedule({
id: findKey(newSchedules, schedule),
...schedule,
}),
label: _('scheduleEdit'),
disabled: (_, { disabledEdition }) => disabledEdition,
icon: 'edit',
level: 'primary',
},
{
handler: (schedule, { deleteNewSchedule, newSchedules }) =>
deleteNewSchedule(findKey(newSchedules, schedule)),
label: _('scheduleDelete'),
icon: 'delete',
level: 'danger',
},
]
import { FormFeedback } from './utils'
// ===================================================================
@@ -74,14 +22,49 @@ export default [
injectState,
provideState({
computed: {
disabledDeletion: state =>
state.schedules.length + size(state.newSchedules) <= 1,
disabledDeletion: state => size(state.schedules) <= 1,
disabledEdition: state =>
state.editionMode !== undefined ||
(!state.exportMode && !state.copyMode && !state.snapshotMode),
error: state => find(FEEDBACK_ERRORS, error => state[error]),
individualActions: (
{ disabledDeletion, disabledEdition },
{ effects: { deleteSchedule, editSchedule } }
) => [
{
disabled: disabledEdition,
handler: editSchedule,
icon: 'edit',
label: _('scheduleEdit'),
level: 'primary',
},
{
disabled: disabledDeletion,
handler: deleteSchedule,
icon: 'delete',
label: _('scheduleDelete'),
level: 'danger',
},
],
rowTransform: ({ settings }) => schedule => {
const { exportRetention, copyRetention, snapshotRetention } =
settings[schedule.id] || {}
return {
...schedule,
exportRetention,
copyRetention,
snapshotRetention,
}
},
schedulesColumns: state => {
const columns = [
{
itemRenderer: _ => _.name,
sortCriteria: 'name',
name: _('scheduleName'),
default: true,
},
{
itemRenderer: _ => _.cron,
sortCriteria: 'cron',
@@ -119,15 +102,6 @@ export default [
}
return columns
},
savedSchedulesColumns: state => [
{
itemRenderer: _ => _.name,
sortCriteria: 'name',
name: _('scheduleName'),
default: true,
},
...state.schedulesColumns,
],
},
}),
injectState,
@@ -150,43 +124,15 @@ export default [
/>
</CardHeader>
<CardBlock>
{isEmpty(state.schedules) &&
isEmpty(state.newSchedules) && (
<p className='text-md-center'>{_('noSchedules')}</p>
)}
{!isEmpty(state.schedules) && (
<FormGroup>
<label>
<strong>{_('backupSavedSchedules')}</strong>
</label>
<SortedTable
collection={state.schedules}
columns={state.savedSchedulesColumns}
data-deleteSchedule={effects.deleteSchedule}
data-disabledDeletion={state.disabledDeletion}
data-disabledEdition={state.disabledEdition}
data-editSchedule={effects.editSchedule}
data-settings={state.settings}
individualActions={SAVED_SCHEDULES_INDIVIDUAL_ACTIONS}
rowTransform={rowTransform}
/>
</FormGroup>
)}
{!isEmpty(state.newSchedules) && (
<FormGroup>
<label>
<strong>{_('backupNewSchedules')}</strong>
</label>
<SortedTable
collection={state.newSchedules}
columns={state.schedulesColumns}
data-deleteNewSchedule={effects.deleteNewSchedule}
data-disabledEdition={state.disabledEdition}
data-editNewSchedule={effects.editNewSchedule}
data-newSchedules={state.newSchedules}
individualActions={NEW_SCHEDULES_INDIVIDUAL_ACTIONS}
/>
</FormGroup>
{isEmpty(state.schedules) ? (
<p className='text-md-center'>{_('noSchedules')}</p>
) : (
<SortedTable
collection={state.schedules}
columns={state.schedulesColumns}
individualActions={state.individualActions}
rowTransform={state.rowTransform}
/>
)}
</CardBlock>
</FormFeedback>