feat(vikon): merge update buttons into single flow with all parts selected by default
This commit is contained in:
@@ -1 +1 @@
|
|||||||
{"version":2,"defects":{"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_atomic_swap_replaces_existing_entry":7,"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_atomic_swap_creates_new_entry_when_not_exists":7,"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_atomic_swap_returns_false_on_path_traversal":8,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_completed_when_status_is_completed":8,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_failed_when_status_is_failed":8,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_timeout_after_max_attempts":8,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_requests_generation_and_polls_status":7,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_throws_on_generation_failure":7,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_throws_on_poll_failure":7,"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_restore_after_fail_removes_new_and_restores_old":8,"App\\Containers\\VikonIntegration\\Tests\\Feature\\UpdatePartTest::test_update_part_validates_module_id":8,"App\\Containers\\VikonIntegration\\Tests\\Feature\\UpdatePartTest::test_update_part_validates_part":8,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_rejects_invalid_part":7,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_rejects_invalid_module_id":7},"times":{"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_atomic_swap_replaces_existing_entry":0.005,"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_atomic_swap_creates_new_entry_when_not_exists":0.001,"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_atomic_swap_returns_false_on_path_traversal":0.001,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_completed_when_status_is_completed":0.027,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_failed_when_status_is_failed":0.001,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_timeout_after_max_attempts":0.002,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_rejects_invalid_module_id":0.003,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_rejects_invalid_part":0.011,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_requests_generation_and_polls_status":0.003,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_throws_on_generation_failure":0.001,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_throws_on_poll_failure":0.001,"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_restore_after_fail_removes_new_and_restores_old":0.004,"App\\Containers\\VikonIntegration\\Tests\\Feature\\UpdatePartTest::test_update_part_requires_authentication":0.073,"App\\Containers\\VikonIntegration\\Tests\\Feature\\UpdatePartTest::test_update_part_validates_module_id":0.025,"App\\Containers\\VikonIntegration\\Tests\\Feature\\UpdatePartTest::test_update_part_validates_part":0.009,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_completed_when_status_is_1":0.026,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_failed_when_status_is_negative_1":0}}
|
{"version":2,"defects":{"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_atomic_swap_replaces_existing_entry":7,"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_atomic_swap_creates_new_entry_when_not_exists":7,"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_atomic_swap_returns_false_on_path_traversal":8,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_completed_when_status_is_completed":8,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_failed_when_status_is_failed":8,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_timeout_after_max_attempts":8,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_requests_generation_and_polls_status":7,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_throws_on_generation_failure":7,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_throws_on_poll_failure":7,"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_restore_after_fail_removes_new_and_restores_old":8,"App\\Containers\\VikonIntegration\\Tests\\Feature\\UpdatePartTest::test_update_part_validates_module_id":8,"App\\Containers\\VikonIntegration\\Tests\\Feature\\UpdatePartTest::test_update_part_validates_part":8,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_rejects_invalid_part":7,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_rejects_invalid_module_id":7},"times":{"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_atomic_swap_replaces_existing_entry":0.018,"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_atomic_swap_creates_new_entry_when_not_exists":0.002,"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_atomic_swap_returns_false_on_path_traversal":0.002,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_completed_when_status_is_completed":0.027,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_failed_when_status_is_failed":0.001,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_timeout_after_max_attempts":0.002,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_rejects_invalid_module_id":0.003,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_rejects_invalid_part":0.011,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_requests_generation_and_polls_status":0.003,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_throws_on_generation_failure":0.001,"App\\Containers\\VikonIntegration\\Tests\\Unit\\UpdatePartActionTest::test_throws_on_poll_failure":0.001,"App\\Containers\\VikonIntegration\\Tests\\Unit\\FilesystemTaskTest::test_restore_after_fail_removes_new_and_restores_old":0.001,"App\\Containers\\VikonIntegration\\Tests\\Feature\\UpdatePartTest::test_update_part_requires_authentication":0.069,"App\\Containers\\VikonIntegration\\Tests\\Feature\\UpdatePartTest::test_update_part_validates_module_id":0.022,"App\\Containers\\VikonIntegration\\Tests\\Feature\\UpdatePartTest::test_update_part_validates_part":0.01,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_completed_when_status_is_1":0.023,"App\\Containers\\VikonIntegration\\Tests\\Unit\\PollPartStatusTaskTest::test_returns_failed_when_status_is_negative_1":0}}
|
||||||
@@ -76,24 +76,25 @@
|
|||||||
<p class="text-xs text-muted-foreground-1">ID: {{ id }}</p>
|
<p class="text-xs text-muted-foreground-1">ID: {{ id }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button @click="updateModule(id)" :disabled="updating || !accessInfo.has_access || parts[id]?.disabled"
|
<button @click="updateModule(id)" :disabled="updating === id || !accessInfo.has_access || parts[id]?.disabled"
|
||||||
class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover disabled:opacity-50">
|
class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover disabled:opacity-50">
|
||||||
{{ updating ? 'Обновление...' : 'Обновить' }}
|
<span v-if="updating === id">Обновление...</span>
|
||||||
|
<span v-else>Обновить</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="parts && parts[id]?.disabled" class="mt-3 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
<div v-if="parts && parts[id]?.disabled" class="mt-3 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||||
<p class="text-sm text-yellow-800">Обновление модуля запрещено сервером VIKON</p>
|
<p class="text-sm text-yellow-800">Обновление модуля запрещено сервером VIKON</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="parts && parts[id]?.parts?.length" class="mt-3 space-y-2">
|
<div v-else-if="parts && parts[id]?.parts?.length" class="mt-3">
|
||||||
<label class="flex items-center gap-2 text-xs font-medium text-muted-foreground-1 cursor-pointer">
|
<div v-if="updating === id" class="flex items-center gap-2 text-sm text-muted-foreground-1 mb-2">
|
||||||
<input type="checkbox" :checked="isAllPartsSelected(id)" @change="toggleAllParts(id)" class="rounded" />
|
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
|
||||||
Выбрать все
|
<span>{{ updatePhase }}</span>
|
||||||
</label>
|
</div>
|
||||||
<div class="pl-5 space-y-1">
|
<div class="pl-5 space-y-1">
|
||||||
<div v-for="p in parts[id]?.parts || []" :key="p.id"
|
<div v-for="p in parts[id]?.parts || []" :key="p.id"
|
||||||
class="flex items-center gap-2 text-sm"
|
class="flex items-center gap-2 text-sm"
|
||||||
:class="p.access ? '' : 'opacity-50'">
|
:class="p.access ? '' : 'opacity-50'">
|
||||||
<input type="checkbox" :value="p.id" v-model="selectedParts[id]" :disabled="!p.access || !!partStatus[`${id}-${p.id}`]" class="rounded" />
|
<input type="checkbox" :value="p.id" v-model="selectedParts[id]" :disabled="!p.access || updating === id" class="rounded" />
|
||||||
<span class="flex-1" :class="p.access ? '' : 'cursor-not-allowed'">{{ p.name }}</span>
|
<span class="flex-1" :class="p.access ? '' : 'cursor-not-allowed'">{{ p.name }}</span>
|
||||||
<span v-if="partStatus[`${id}-${p.id}`] === 'loading'" class="text-primary">
|
<span v-if="partStatus[`${id}-${p.id}`] === 'loading'" class="text-primary">
|
||||||
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
|
<svg class="animate-spin h-4 w-4" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
|
||||||
@@ -102,31 +103,11 @@
|
|||||||
<span v-else-if="partStatus[`${id}-${p.id}`] === 'error'" class="text-red-600 text-xs" :title="partErrors[`${id}-${p.id}`]">✗</span>
|
<span v-else-if="partStatus[`${id}-${p.id}`] === 'error'" class="text-red-600 text-xs" :title="partErrors[`${id}-${p.id}`]">✗</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
@click="updateSelectedParts(id)"
|
|
||||||
:disabled="!selectedParts[id]?.length || updatingPart === id"
|
|
||||||
class="text-sm bg-surface border border-layer-line px-3 py-1.5 rounded-md hover:bg-muted-hover disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<span v-if="updatingPart === id">Обновление...</span>
|
|
||||||
<span v-else>Обновить выбранные ({{ selectedParts[id]?.length || 0 }})</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Прогресс -->
|
|
||||||
<div v-if="updating" class="bg-layer border border-layer-line rounded-lg p-6">
|
|
||||||
<div class="flex items-center gap-3 mb-3">
|
|
||||||
<DashboardIcon name="arrow-path" size="5" class="text-primary animate-spin" />
|
|
||||||
<p class="text-sm font-medium">Обновление...</p>
|
|
||||||
</div>
|
|
||||||
<div class="w-full bg-muted rounded-full h-2.5">
|
|
||||||
<div class="bg-primary h-2.5 rounded-full transition-all" :style="{ width: progress + '%' }"></div>
|
|
||||||
</div>
|
|
||||||
<p class="text-xs text-muted-foreground-1 mt-2">{{ progress }}%</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ошибка -->
|
<!-- Ошибка -->
|
||||||
<div v-if="updateError" class="bg-layer border border-red-200 rounded-lg p-6">
|
<div v-if="updateError" class="bg-layer border border-red-200 rounded-lg p-6">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
@@ -167,20 +148,19 @@ const isAuthenticated = ref(props.is_authenticated);
|
|||||||
const currentVersion = ref(props.current_version);
|
const currentVersion = ref(props.current_version);
|
||||||
const checkingVersion = ref(false);
|
const checkingVersion = ref(false);
|
||||||
const checkingAccess = ref(false);
|
const checkingAccess = ref(false);
|
||||||
const updating = ref(false);
|
const updating = ref(null);
|
||||||
const progress = ref(0);
|
const updatePhase = ref('');
|
||||||
const updateError = ref(null);
|
const updateError = ref(null);
|
||||||
const versionInfo = ref({ current_version: props.current_version, has_update: false, latest_version: null });
|
const versionInfo = ref({ current_version: props.current_version, has_update: false, latest_version: null });
|
||||||
const accessInfo = ref({ has_access: false, error: null });
|
const accessInfo = ref({ has_access: false, error: null });
|
||||||
const selectedParts = ref({});
|
const selectedParts = ref({});
|
||||||
if (props.parts) {
|
if (props.parts) {
|
||||||
for (const [id, moduleData] of Object.entries(props.parts)) {
|
for (const [id, moduleData] of Object.entries(props.parts)) {
|
||||||
if (!moduleData.disabled) {
|
if (!moduleData.disabled && moduleData.parts) {
|
||||||
selectedParts.value[id] = [];
|
selectedParts.value[id] = moduleData.parts.filter(p => p.access).map(p => p.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const updatingPart = ref(null);
|
|
||||||
const partStatus = ref({});
|
const partStatus = ref({});
|
||||||
const partErrors = ref({});
|
const partErrors = ref({});
|
||||||
|
|
||||||
@@ -246,35 +226,59 @@ async function checkVersion() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateModule(moduleId) {
|
async function updateModule(moduleId) {
|
||||||
if (!confirm('Обновить модуль?')) return;
|
const partsToUpdate = selectedParts.value[moduleId] || [];
|
||||||
|
const msg = partsToUpdate.length
|
||||||
|
? `Обновить модуль и ${partsToUpdate.length} раздел(ов)?`
|
||||||
|
: 'Обновить модуль (только ядро и файлы)?';
|
||||||
|
if (!confirm(msg)) return;
|
||||||
|
|
||||||
updating.value = true;
|
updating.value = moduleId;
|
||||||
progress.value = 0;
|
updatePhase.value = 'Обновление ядра модуля...';
|
||||||
updateError.value = null;
|
updateError.value = null;
|
||||||
|
// Clear previous part statuses
|
||||||
const timer = setInterval(() => {
|
for (const p of partsToUpdate) {
|
||||||
if (progress.value < 90) progress.value += 10;
|
delete partStatus.value[`${moduleId}-${p}`];
|
||||||
}, 500);
|
delete partErrors.value[`${moduleId}-${p}`];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Phase 1: Core + FM sync
|
||||||
const res = await axios.post(route('dashboard.vikon-updates.update-module'), { module_id: moduleId });
|
const res = await axios.post(route('dashboard.vikon-updates.update-module'), { module_id: moduleId });
|
||||||
progress.value = 100;
|
if (!res.data.success) {
|
||||||
clearInterval(timer);
|
|
||||||
|
|
||||||
if (res.data.success) {
|
|
||||||
updateError.value = null;
|
|
||||||
updating.value = false;
|
|
||||||
alert(res.data.message);
|
|
||||||
} else {
|
|
||||||
updateError.value = res.data.message;
|
updateError.value = res.data.message;
|
||||||
updating.value = false;
|
updating.value = null;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 2: Update selected parts
|
||||||
|
if (partsToUpdate.length) {
|
||||||
|
updatePhase.value = 'Обновление разделов...';
|
||||||
|
for (const part of partsToUpdate) {
|
||||||
|
const key = `${moduleId}-${part}`;
|
||||||
|
partStatus.value[key] = 'loading';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(route('dashboard.vikon-updates.update-part'), {
|
||||||
|
module_id: moduleId,
|
||||||
|
part,
|
||||||
|
});
|
||||||
|
partStatus.value[key] = 'success';
|
||||||
|
} catch (e) {
|
||||||
|
partStatus.value[key] = 'error';
|
||||||
|
partErrors.value[key] = e.response?.data?.message || e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateError.value = null;
|
||||||
|
alert('Модуль обновлён.');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
clearInterval(timer);
|
|
||||||
updating.value = false;
|
|
||||||
const msg = e.response?.data?.message || e.message || 'Неизвестная ошибка';
|
const msg = e.response?.data?.message || e.message || 'Неизвестная ошибка';
|
||||||
const code = e.response?.status || '';
|
const code = e.response?.status || '';
|
||||||
updateError.value = code ? `[${code}] ${msg}` : msg;
|
updateError.value = code ? `[${code}] ${msg}` : msg;
|
||||||
|
} finally {
|
||||||
|
updating.value = null;
|
||||||
|
updatePhase.value = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,37 +297,6 @@ function toggleAllParts(moduleId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSelectedParts(moduleId) {
|
|
||||||
const partsToUpdate = selectedParts.value[moduleId] || [];
|
|
||||||
if (!partsToUpdate.length) return;
|
|
||||||
|
|
||||||
updatingPart.value = moduleId;
|
|
||||||
// Clear previous statuses for this module
|
|
||||||
for (const part of partsToUpdate) {
|
|
||||||
delete partStatus.value[`${moduleId}-${part}`];
|
|
||||||
delete partErrors.value[`${moduleId}-${part}`];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const part of partsToUpdate) {
|
|
||||||
const key = `${moduleId}-${part}`;
|
|
||||||
partStatus.value[key] = 'loading';
|
|
||||||
partErrors.value[key] = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.post(route('dashboard.vikon-updates.update-part'), {
|
|
||||||
module_id: moduleId,
|
|
||||||
part,
|
|
||||||
});
|
|
||||||
partStatus.value[key] = 'success';
|
|
||||||
} catch (e) {
|
|
||||||
partStatus.value[key] = 'error';
|
|
||||||
partErrors.value[key] = e.response?.data?.message || e.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatingPart.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
try {
|
try {
|
||||||
await axios.post(route('dashboard.vikon-updates.logout'));
|
await axios.post(route('dashboard.vikon-updates.logout'));
|
||||||
|
|||||||
Reference in New Issue
Block a user