Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(controller): add remote unit update #370

Merged
merged 10 commits into from
Oct 8, 2024
32 changes: 28 additions & 4 deletions public/i18n/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"upload": "Upload",
"no_data_available": "No data available",
"type": "Type",
"unknown": "Unknown"
"unknown": "Unknown",
"confirm": "Confirm"
},
"error": {
"generic_error": "Something went wrong",
Expand Down Expand Up @@ -676,14 +677,15 @@
"keep_scheduled_update": "Keep scheduled update",
"cancel_update_description": "The scheduled update to version '{version}' will be canceled",
"system_update_in_progress_message": "Updating and rebooting unit, please wait...",
"update_system": "Update system",
"update_system": "Update system | Update systems",
"edit_scheduled_date": "Edit scheduled date",
"update": "Update",
"automatic_updates": "Automatic updates",
"automatic_updates_tooltip": "Enable to activate automatic updates. Available only with subscription.",
"go_to_subscription": "Go to subscription",
"automatic_updates_enabled_message": "Automatic updates successfully enabled",
"automatic_updates_disabled_message": "Automatic updates successfully disabled"
"automatic_updates_disabled_message": "Automatic updates successfully disabled",
"schedule_update": "Schedule update"
},
"storage": {
"title": "Storage",
Expand Down Expand Up @@ -2182,7 +2184,29 @@
"cannot_open_unit_description": "The unit {name} must be updated before opening it. Access the unit UI directly and go to System > Updates page.",
"open_anyway": "Open anyway",
"warning_open_unit": "Controller is not up-to-date",
"warning_open_unit_description": "It appears that the controller is not up-to-date. It's recommended to update the controller instance on your NethServer 8 cluster before opening this unit to avoid compatibility issues."
"warning_open_unit_description": "It appears that the controller is not up-to-date. It's recommended to update the controller instance on your NethServer 8 cluster before opening this unit to avoid compatibility issues.",
"scheduled_image_update": "Image update scheduled",
"scheduled_image_update_tooltip": "The unit has been scheduled to install \"{version}\" on the {date}.",
"image_update_available": "Image update available",
"image_update_available_tooltip": "Version \"{version}\" is available for download.",
"edit_scheduled_image_update": "Edit scheduled image update",
"error_fetching_unit_image_version": "Error fetching unit image version",
"error_setting_image_update": "Error sending image update command to unit",
"scheduled_image_update_success": "Image update scheduled successfully",
"image_update_success": "Image update sent successfully",
"image_update_description": "Unit may be not available for a few minutes while the update is being installed.",
"scheduled_image_update_aborted": "Scheduled image update for '{name}' aborted successfully",
"cancel_scheduled_image_update": "Cancel scheduled image update",
"cancel_scheduled_image_update_description": "You are about to cancel the scheduled update to '{version}' for unit '{name}'.",
"check_packages_updates": "Check packages updates",
"packages_upgrade_in_progress": "Packages update in progress for unit '{name}'",
"error_upgrading_unit_packages": "Error updating unit packages",
"error_removing_scheduled_update": "Error removing scheduled update",
"packages_to_update": "The following packages are available to be updated:",
"batch_update_system": "You are about to update the system for multiple units. The units that are available for an update are shown inside the combobox below.",
"batch_updated_systems": "System update sent to the unit | System update sent to {n} units",
"error_batch_updating_systems": "Some systems failed to update.",
"error_batch_updating_systems_description": "Some units failed to get the update from the controller. Please check below the ones that failed and try again."
},
"unit_terminal": {
"name_unit_terminal": "{name} unit terminal",
Expand Down
1 change: 1 addition & 0 deletions src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

@import '@xterm/xterm/css/xterm.css';

Tbaile marked this conversation as resolved.
Show resolved Hide resolved
@tailwind base;
@tailwind components;
@tailwind utilities;
Expand Down
256 changes: 256 additions & 0 deletions src/components/controller/units/BatchUnitImageUpdate.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
<script lang="ts" setup>
import {
focusElement,
NeButton,
NeCombobox,
type NeComboboxOption,
type NeDropdownItem,
NeFormItemLabel,
NeInlineNotification,
NeRadioSelection,
NeSideDrawer,
NeTooltip
} from '@nethesis/vue-components'
import { useI18n } from 'vue-i18n'
import { useUnitsStore } from '@/stores/controller/units'
import { computed, ref } from 'vue'
import { MessageBag } from '@/lib/validation'
import '@vuepic/vue-datepicker/dist/main.css'
import VueDatePicker from '@vuepic/vue-datepicker'
import { useThemeStore } from '@/stores/theme'
import { useUpdates } from '@/composables/useUpdates'
import { useNotificationsStore } from '@/stores/notifications'

const { t } = useI18n()
const unitsStore = useUnitsStore()
const theme = useThemeStore()
const { upgradeUnitImage, scheduleUpgradeUnitImage } = useUpdates()
const notificationsStore = useNotificationsStore()

defineProps<{
show: boolean
}>()

const emit = defineEmits<{
close: []
}>()

function close() {
if (!loading.value) {
emit('close')
}
}

function validate(): boolean {
errorBag.value.clear()

if (selectedUnits.value.length < 1) {
errorBag.value.set('units', 'error.required_option')
focusElement(selectedUnitsRef.value)
}

return errorBag.value.size > 0
}

function updateUnits() {
if (validate()) {
return
}
loading.value = true
someUnitsFailed.value = false
Promise.allSettled(
selectedUnits.value.map((unit) => {
if (updateMode.value === 'now') {
return upgradeUnitImage(unit)
} else {
return scheduleUpgradeUnitImage(scheduledUpdate.value, unit)
}
})
)
.then(async (results) => {
// load again the info of the units
for (const unit of selectedUnits.value) {
await unitsStore.getUnitInfo(unit.id)
}
await unitsStore.getUnits()
// clean selection
selectedUnits.value = []
// if some units failed to update, prompt the user to check again
// the selection is cleaned every choice
if (results.some((result) => result.status === 'rejected')) {
// count the failed units
someUnitsFailed.value = true
} else {
// all good, close the drawer and show a success notification
emit('close')
updateMode.value = 'now'
notificationsStore.createNotification({
title: t(
'controller.units.batch_updated_systems',
results.filter((result) => result.status === 'fulfilled').length
),
kind: 'success'
})
}
})
.finally(() => {
loading.value = false
})
}

const unitsAvailableForUpdate = computed((): NeDropdownItem[] => {
return unitsStore.units
.filter(
// if a unit is connected, and does not have a scheduled update and have a new image available
(unit) => unit.connected && unit.info?.scheduled_update < 0 && unit.info?.version_update != ''
)
.map((unit) => {
return {
id: unit.id,
label: unit.info?.unit_name || unit.id,
description: unit.info?.unit_name ? unit.id : ''
}
})
})

function selectAllUnits() {
selectedUnits.value = unitsAvailableForUpdate.value
}

function deselectAllUnits() {
selectedUnits.value = []
}

const updateModeOptions = [
{
id: 'now',
label: t('standalone.update.now')
},
{
id: 'scheduled',
label: t('standalone.update.schedule_time_and_date')
}
]

const allUnitsSelected = computed(() => {
return selectedUnits.value.length === unitsAvailableForUpdate.value.length
})

const selectedUnits = ref<NeComboboxOption[]>([])
const loading = ref(false)
const scheduledUpdate = ref<Date>(new Date())
const updateMode = ref<'scheduled' | 'now'>('now')
const errorBag = ref(new MessageBag())
const selectedUnitsRef = ref()
const someUnitsFailed = ref(false)
</script>

<template>
<NeSideDrawer
:closeAriaLabel="t('common.shell.close_side_drawer')"
:isShown="show"
:title="t('standalone.update.update_system', 2)"
@close="close"
>
<div class="space-y-4">
<NeInlineNotification
:closeAriaLabel="t('common.close')"
:description="t('controller.units.batch_update_system')"
:showDetailsLabel="t('notifications.show_details')"
:title="t('standalone.update.update_system', 2)"
kind="info"
/>
<NeInlineNotification
v-if="someUnitsFailed"
:description="t('controller.units.error_batch_updating_systems_description')"
:title="t('controller.units.error_batch_updating_systems')"
kind="warning"
/>
<NeButton v-if="allUnitsSelected" class="-mx-2" kind="tertiary" @click="deselectAllUnits">
{{ t('controller.units.deselect_all_units') }}
</NeButton>
<NeButton v-else class="-mx-2" kind="tertiary" @click="selectAllUnits">
{{ t('controller.units.select_all_units') }}
</NeButton>
<NeCombobox
ref="selectedUnitsRef"
v-model="selectedUnits"
:disabled="loading"
:invalidMessage="t(errorBag.getFirstI18nKeyFor('units'))"
:label="t('controller.units.units')"
:limitedOptionsLabel="t('ne_combobox.limited_options_label')"
:noOptionsLabel="t('controller.units.no_unit_is_currently_connected')"
:noResultsLabel="t('ne_combobox.no_results')"
:optionalLabel="t('common.optional')"
:options="unitsAvailableForUpdate"
:placeholder="t('ne_combobox.choose_multiple')"
:selected-label="t('ne_combobox.selected')"
:user-input-label="t('ne_combobox.user_input_label')"
multiple
/>
<NeRadioSelection
v-model="updateMode"
:disabled="loading"
:label="t('standalone.update.choose_when_to_update')"
:options="updateModeOptions"
>
<template #tooltip>
<NeTooltip>
<template #content>
{{ t('standalone.update.schedule_mode_tooltip') }}
</template>
</NeTooltip>
</template>
</NeRadioSelection>
<template v-if="updateMode == 'scheduled'">
<div>
<NeFormItemLabel class="mb-2">{{ t('standalone.update.time_and_date') }}</NeFormItemLabel>
<VueDatePicker
v-model="scheduledUpdate"
:clearable="false"
:dark="!theme.isLight"
:disabled="loading"
:disabled-dates="(date: Date) => date < new Date()"
:readonly="loading"
:time-picker-inline="true"
/>
</div>
</template>
<hr />
<div class="flex flex-wrap justify-end gap-4">
<NeButton :disabled="loading" :readonly="loading" kind="tertiary" @click="emit('close')">
{{ t('common.cancel') }}
</NeButton>
<NeButton
:disabled="loading"
:loading="loading"
:readonly="loading"
kind="primary"
@click="updateUnits"
>
<template v-if="updateMode == 'now'">
{{ t('standalone.update.update_and_reboot') }}
</template>
<template v-else>
{{ t('standalone.update.schedule') }}
</template>
</NeButton>
</div>
</div>
</NeSideDrawer>
</template>

<style>
/* tailwind theme for vue-datepicker */
.dp__theme_dark {
--dp-background-color: rgb(3 7 18 / var(--tw-bg-opacity));
--dp-primary-color: rgb(6 182 212 / var(--tw-bg-opacity));
--dp-primary-text-color: rgb(3 7 18 / var(--tw-text-opacity));
--dp-border-color-hover: var(--dp-primary-color);
}

.dp__theme_light {
--dp-primary-color: rgb(14 116 144 / var(--tw-bg-opacity));
--dp-border-color-hover: var(--dp-primary-color);
}
</style>
Loading