pcp-tgu: редактор — расчёт плана съёмки+сброса одной операцией, очистка плана, удаление включений
Вкладка «Редактор»:
- Кнопка «Рассчитать план» — запускает surveys/calculate → drops/calculate
одной операцией на черновике (ensureDraft), затем перечитывает включения.
Источник комплексного плана (snapshot этапа 4) выбирает бэкенд по интервалу.
Вызовы идут напрямую в pcp-mission-planing-service (/api/pcp-mission).
Ручные маршруты съёмки (source=MANUAL) сохраняются при пересчёте.
- Кнопка «Очистить план» — новый эндпоинт DELETE /api/missions/{id}/modes
(clearPlan) сносит ВСЕ включения, включая ручные, с бронями и связями.
- Удаление маршрута съёмки / сброса в правой панели с подтверждением:
WorkInspector.canDelete показывает кнопку удаления и для рассчитанных
(read-only) включений, когда есть черновик; удаление через deleteMode.
This commit is contained in:
+21
@@ -252,6 +252,27 @@ class MissionController(
|
|||||||
return ResponseEntity.noContent().build()
|
return ResponseEntity.noContent().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Полностью очистить план",
|
||||||
|
description = "Удаляет ВСЕ включения плана миссии: съёмки (включая построенные вручную, source=MANUAL) " +
|
||||||
|
"и сбросы, вместе с бронями слотов и связями съёмка↔сброс. В отличие от пересчёта плана, " +
|
||||||
|
"ручные маршруты тоже удаляются. Используется кнопкой «Очистить план» в редакторе."
|
||||||
|
)
|
||||||
|
@ApiResponses(
|
||||||
|
value = [
|
||||||
|
ApiResponse(responseCode = "204", description = "План очищен"),
|
||||||
|
ApiResponse(responseCode = "404", description = "Миссия не найдена")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@DeleteMapping("/{mission_id}/modes")
|
||||||
|
fun clearPlan(
|
||||||
|
@Parameter(description = "Идентификатор миссии")
|
||||||
|
@PathVariable("mission_id") id: UUID
|
||||||
|
): ResponseEntity<Void> {
|
||||||
|
missionPlaningService.clearPlan(id)
|
||||||
|
return ResponseEntity.noContent().build()
|
||||||
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Проверить миссию-черновик на конфликты",
|
summary = "Проверить миссию-черновик на конфликты",
|
||||||
description = "Считает конфликты/наложения включений плана-черновика и возвращает дифф по " +
|
description = "Считает конфликты/наложения включений плана-черновика и возвращает дифф по " +
|
||||||
|
|||||||
+15
@@ -936,6 +936,21 @@ class MissionPlaningService(
|
|||||||
logger.info("Удалён сброс (drop_mode id={}), снято связей со съёмками={}", dropId, links.size)
|
logger.info("Удалён сброс (drop_mode id={}), снято связей со съёмками={}", dropId, links.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Полная очистка плана: удаляет ВСЕ включения миссии — съёмки (включая ручные, source=MANUAL)
|
||||||
|
* и сбросы — вместе с бронями слотов и связями съёмка↔сброс. В отличие от пересчёта плана
|
||||||
|
* ([loadSlots]/[calculateDrops]), ручные маршруты тоже удаляются. Сбросы удаляются первыми,
|
||||||
|
* чтобы снять связи до удаления съёмок и не плодить проверку осиротевших сбросов.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
fun clearPlan(missionId: UUID) {
|
||||||
|
missionRepository.findById(missionId).orElseThrow { notFound(missionId) }
|
||||||
|
val modes = modeRepository.findAllByPlan_Mission_MissionIdOrderByTimeStartAsc(missionId)
|
||||||
|
modes.filterIsInstance<DropModeEntity>().forEach { deleteDropMode(it) }
|
||||||
|
modes.filterIsInstance<SurveyModeEntity>().forEach { deleteSurveyMode(it) }
|
||||||
|
logger.info("План миссии {} полностью очищен: удалено включений={}", missionId, modes.size)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Валидация миссии-черновика на конфликты/наложения включений.
|
* Валидация миссии-черновика на конфликты/наложения включений.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -138,6 +138,76 @@ export async function saveDropRoute(planId: string, params: SaveDropRouteParams)
|
|||||||
return JSON.parse(text) as DropMode;
|
return JSON.parse(text) as DropMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Итог расчёта плана съёмки (ответ /surveys/calculate). Времена — локальные. */
|
||||||
|
export type SurveyCalculationResult = {
|
||||||
|
missionId: string;
|
||||||
|
completed: boolean;
|
||||||
|
comPlanSnapshotId: number | null;
|
||||||
|
surveyModesCount: number;
|
||||||
|
manualModesCount: number;
|
||||||
|
complanModesCount: number;
|
||||||
|
slotsModesCount: number;
|
||||||
|
mixedModesCount: number;
|
||||||
|
bookedSlotIdsCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запуск расчёта плана съёмки для плана-черновика
|
||||||
|
* (pcp-mission-planing-service, POST /api/missions/{planId}/surveys/calculate).
|
||||||
|
* Источник комплексного плана (snapshot этапа 4) бэкенд выбирает сам по интервалу плана.
|
||||||
|
* Пересчёт сохраняет ручные маршруты (source=MANUAL), затирая только пересчитываемые.
|
||||||
|
*/
|
||||||
|
export async function calculateSurveys(planId: string): Promise<SurveyCalculationResult> {
|
||||||
|
const url = `${BASE}/api/missions/${encodeURIComponent(planId)}/surveys/calculate`;
|
||||||
|
let response: Response;
|
||||||
|
try {
|
||||||
|
response = await fetch(url, { method: "POST", headers: { Accept: "application/json" } });
|
||||||
|
} catch {
|
||||||
|
throw new TguApiError("Не удалось подключиться к сервису планирования миссий.", "network");
|
||||||
|
}
|
||||||
|
const text = await response.text();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new TguApiError(extractErrorText(text) || "Не удалось рассчитать съёмку.", "http", response.status);
|
||||||
|
}
|
||||||
|
return JSON.parse(text) as SurveyCalculationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запуск расчёта плана сбросов для плана-черновика по уже рассчитанным/добавленным съёмкам
|
||||||
|
* (pcp-mission-planing-service, POST /api/missions/{planId}/drops/calculate). Возвращает 204.
|
||||||
|
*/
|
||||||
|
export async function calculateDrops(planId: string): Promise<void> {
|
||||||
|
const url = `${BASE}/api/missions/${encodeURIComponent(planId)}/drops/calculate`;
|
||||||
|
let response: Response;
|
||||||
|
try {
|
||||||
|
response = await fetch(url, { method: "POST", headers: { Accept: "application/json" } });
|
||||||
|
} catch {
|
||||||
|
throw new TguApiError("Не удалось подключиться к сервису планирования миссий.", "network");
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
throw new TguApiError(extractErrorText(text) || "Не удалось рассчитать сбросы.", "http", response.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Полная очистка плана: удаляет все включения (съёмки, включая ручные, и сбросы) плана-черновика
|
||||||
|
* (pcp-mission-planing-service, DELETE /api/missions/{planId}/modes). Возвращает 204.
|
||||||
|
*/
|
||||||
|
export async function clearPlanModes(planId: string): Promise<void> {
|
||||||
|
const url = `${BASE}/api/missions/${encodeURIComponent(planId)}/modes`;
|
||||||
|
let response: Response;
|
||||||
|
try {
|
||||||
|
response = await fetch(url, { method: "DELETE", headers: { Accept: "application/json" } });
|
||||||
|
} catch {
|
||||||
|
throw new TguApiError("Не удалось подключиться к сервису планирования миссий.", "network");
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
const text = await response.text();
|
||||||
|
throw new TguApiError(extractErrorText(text) || "Не удалось очистить план.", "http", response.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Перестроение контура существующего режима съёмки по новым параметрам окна/крена
|
* Перестроение контура существующего режима съёмки по новым параметрам окна/крена
|
||||||
* (pcp-mission-planing-service, PATCH /api/missions/survey-modes/{modeId}). Правка включения
|
* (pcp-mission-planing-service, PATCH /api/missions/survey-modes/{modeId}). Правка включения
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ type WorkInspectorProps = {
|
|||||||
conflicts: string[];
|
conflicts: string[];
|
||||||
/** Включение существующего плана — показываем только информацию, без правок. */
|
/** Включение существующего плана — показываем только информацию, без правок. */
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
/** Разрешить удаление даже у read-only включения плана (есть черновик — режим живёт в нём). */
|
||||||
|
canDelete?: boolean;
|
||||||
onUpdate: (workId: string, patch: EditorWorkPatch) => void;
|
onUpdate: (workId: string, patch: EditorWorkPatch) => void;
|
||||||
onDelete: (workId: string) => void;
|
onDelete: (workId: string) => void;
|
||||||
onDuplicate: (workId: string) => void;
|
onDuplicate: (workId: string) => void;
|
||||||
@@ -131,6 +133,7 @@ export function WorkInspector({
|
|||||||
surveyModeLabel,
|
surveyModeLabel,
|
||||||
conflicts,
|
conflicts,
|
||||||
readOnly,
|
readOnly,
|
||||||
|
canDelete,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onDelete,
|
onDelete,
|
||||||
onDuplicate,
|
onDuplicate,
|
||||||
@@ -270,6 +273,14 @@ export function WorkInspector({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{readOnly && canDelete && (
|
||||||
|
<div className="tgu-editor-panel__actions">
|
||||||
|
<button className="tgu-button tgu-button--reject" type="button" onClick={() => onDelete(work.id)}>
|
||||||
|
{work.kind === "downlink" ? "Удалить сброс" : "Удалить маршрут съёмки"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<span className="tgu-editor-panel__color" style={{ background: color }} aria-hidden="true" />
|
<span className="tgu-editor-panel__color" style={{ background: color }} aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,11 +6,19 @@ type EditorToolbarProps = {
|
|||||||
selectedPlan?: TguPlanUi;
|
selectedPlan?: TguPlanUi;
|
||||||
platforms: TguPlatformUi[];
|
platforms: TguPlatformUi[];
|
||||||
checking: boolean;
|
checking: boolean;
|
||||||
|
/** Идёт расчёт/очистка всего плана — блокирует кнопки операций над планом. */
|
||||||
|
planCalcRunning: boolean;
|
||||||
|
/** Почему нельзя рассчитывать/чистить план (заложен/стартовал) — гасит кнопки. */
|
||||||
|
calcBlockedReason?: string;
|
||||||
conflictCount: number;
|
conflictCount: number;
|
||||||
windowLabel: string;
|
windowLabel: string;
|
||||||
rightMode: "none" | "inspect" | "shoot" | "downlink" | "mapInfo";
|
rightMode: "none" | "inspect" | "shoot" | "downlink" | "mapInfo" | "mapPick";
|
||||||
onSetRightMode: (mode: "shoot" | "downlink") => void;
|
onSetRightMode: (mode: "shoot" | "downlink") => void;
|
||||||
onCheck: () => void;
|
onCheck: () => void;
|
||||||
|
/** Рассчитать план съёмки и сброса одной операцией. */
|
||||||
|
onCalculatePlan: () => void;
|
||||||
|
/** Полностью очистить план (с подтверждением внутри обработчика). */
|
||||||
|
onClearPlan: () => void;
|
||||||
onZoom: (factor: number) => void;
|
onZoom: (factor: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -19,11 +27,15 @@ export function EditorToolbar({
|
|||||||
selectedPlan,
|
selectedPlan,
|
||||||
platforms,
|
platforms,
|
||||||
checking,
|
checking,
|
||||||
|
planCalcRunning,
|
||||||
|
calcBlockedReason,
|
||||||
conflictCount,
|
conflictCount,
|
||||||
windowLabel,
|
windowLabel,
|
||||||
rightMode,
|
rightMode,
|
||||||
onSetRightMode,
|
onSetRightMode,
|
||||||
onCheck,
|
onCheck,
|
||||||
|
onCalculatePlan,
|
||||||
|
onClearPlan,
|
||||||
onZoom
|
onZoom
|
||||||
}: EditorToolbarProps) {
|
}: EditorToolbarProps) {
|
||||||
const platformLabel = selectedSpacecraftId
|
const platformLabel = selectedSpacecraftId
|
||||||
@@ -61,6 +73,27 @@ export function EditorToolbar({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="tgu-editor-toolbar__group">
|
||||||
|
<button
|
||||||
|
className="tgu-button tgu-button--primary"
|
||||||
|
type="button"
|
||||||
|
onClick={onCalculatePlan}
|
||||||
|
disabled={!selectedPlan || planCalcRunning || Boolean(calcBlockedReason)}
|
||||||
|
title={calcBlockedReason ?? "Рассчитать план съёмки и сброса по комплексному плану (ручные маршруты сохраняются)"}
|
||||||
|
>
|
||||||
|
{planCalcRunning ? "Расчёт..." : "Рассчитать план"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="tgu-button tgu-button--reject"
|
||||||
|
type="button"
|
||||||
|
onClick={onClearPlan}
|
||||||
|
disabled={!selectedPlan || planCalcRunning || Boolean(calcBlockedReason)}
|
||||||
|
title={calcBlockedReason ?? "Удалить все включения плана, включая построенные вручную"}
|
||||||
|
>
|
||||||
|
Очистить план
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="tgu-editor-toolbar__spacer" />
|
<div className="tgu-editor-toolbar__spacer" />
|
||||||
|
|
||||||
<div className="tgu-editor-window-control" aria-label="Масштаб окна" title="Колесо мыши над таймлайном — тоже масштаб">
|
<div className="tgu-editor-window-control" aria-label="Масштаб окна" title="Колесо мыши над таймлайном — тоже масштаб">
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import { stationPasses, type OrbitPassInfo, type PassRange } from "../tgu-map-2d
|
|||||||
import { fetchPointVisibility, fetchSatelliteRva, type PointVisibilityParam, type RvaZone } from "../../api/ballisticsApi";
|
import { fetchPointVisibility, fetchSatelliteRva, type PointVisibilityParam, type RvaZone } from "../../api/ballisticsApi";
|
||||||
import {
|
import {
|
||||||
buildSurveyRoute,
|
buildSurveyRoute,
|
||||||
|
calculateDrops,
|
||||||
|
calculateSurveys,
|
||||||
|
clearPlanModes,
|
||||||
deleteMode,
|
deleteMode,
|
||||||
saveDropRoute,
|
saveDropRoute,
|
||||||
saveSurveyRoute,
|
saveSurveyRoute,
|
||||||
@@ -127,6 +130,9 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
const [stationDtos, setStationDtos] = useState<StationDto[]>([]);
|
const [stationDtos, setStationDtos] = useState<StationDto[]>([]);
|
||||||
const [serviceConflicts, setServiceConflicts] = useState<EditorConflictMap>({});
|
const [serviceConflicts, setServiceConflicts] = useState<EditorConflictMap>({});
|
||||||
const [checking, setChecking] = useState(false);
|
const [checking, setChecking] = useState(false);
|
||||||
|
// Идёт серверная операция над всем планом (расчёт съёмки+сброса или полная очистка) —
|
||||||
|
// блокирует повторный запуск и кнопки правок, пока не перечитаем включения.
|
||||||
|
const [planCalcRunning, setPlanCalcRunning] = useState(false);
|
||||||
const [notice, setNotice] = useState<string>();
|
const [notice, setNotice] = useState<string>();
|
||||||
const [applyError, setApplyError] = useState<string>();
|
const [applyError, setApplyError] = useState<string>();
|
||||||
const [contextSpacecraftIds, setContextSpacecraftIds] = useState<Set<string>>(new Set());
|
const [contextSpacecraftIds, setContextSpacecraftIds] = useState<Set<string>>(new Set());
|
||||||
@@ -712,12 +718,71 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Удаление включения: персистнутые режимы (с planModeId) удаляются в БД черновика (Ф3,
|
// Запуск расчёта плана съёмки и сброса как одной операции (вкладка «Редактор»). Адресуем
|
||||||
// с каскадами drop↔survey), затем убираются из локального представления. Несохранённые
|
// в DRAFT (ленивое создание на первую правку), затем перечитываем включения. Пересчёт на бэке
|
||||||
// (planModeId нет) — только локально.
|
// сохраняет ручные маршруты съёмки (source=MANUAL) — они не затираются. Источник комплексного
|
||||||
|
// плана (snapshot этапа 4) бэкенд выбирает сам по интервалу плана.
|
||||||
|
const runPlanCalculation = async () => {
|
||||||
|
if (!selectedPlan || planCalcRunning) return;
|
||||||
|
setPlanCalcRunning(true);
|
||||||
|
setApplyError(undefined);
|
||||||
|
setNotice(undefined);
|
||||||
|
try {
|
||||||
|
const draftId = await ensureDraft();
|
||||||
|
const surveyResult = await calculateSurveys(draftId);
|
||||||
|
await calculateDrops(draftId);
|
||||||
|
await refreshDraftModes(draftId);
|
||||||
|
setNotice(
|
||||||
|
`Расчёт выполнен: съёмок ${surveyResult.surveyModesCount} (ручных сохранено ${surveyResult.manualModesCount}); сбросы пересчитаны.`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
setApplyError(error instanceof Error ? error.message : "Не удалось рассчитать план.");
|
||||||
|
} finally {
|
||||||
|
setPlanCalcRunning(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Полная очистка плана: сносит ВСЕ включения (съёмки, включая ручные, и сбросы) в БД черновика
|
||||||
|
// и в локальном представлении. Единственный способ убрать ручные маршруты массово (помимо
|
||||||
|
// ручного удаления по одному). Подтверждение — действие необратимо.
|
||||||
|
const clearPlan = async () => {
|
||||||
|
if (!selectedPlan || planCalcRunning) return;
|
||||||
|
// window здесь затенён состоянием окна таймлайна — берём confirm из globalThis.
|
||||||
|
if (!globalThis.confirm("Очистить план полностью? Будут удалены все включения, включая построенные вручную. Действие необратимо.")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPlanCalcRunning(true);
|
||||||
|
setApplyError(undefined);
|
||||||
|
setNotice(undefined);
|
||||||
|
try {
|
||||||
|
const draftId = await ensureDraft();
|
||||||
|
await clearPlanModes(draftId);
|
||||||
|
await refreshDraftModes(draftId);
|
||||||
|
changeDraft((current) => ({ ...current, works: [] }));
|
||||||
|
setSelectedWorkId(undefined);
|
||||||
|
setRightMode("none");
|
||||||
|
setNotice("План полностью очищен.");
|
||||||
|
} catch (error) {
|
||||||
|
setApplyError(error instanceof Error ? error.message : "Не удалось очистить план.");
|
||||||
|
} finally {
|
||||||
|
setPlanCalcRunning(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Удаление включения (правая панель, с подтверждением): персистнутые режимы (с planModeId)
|
||||||
|
// удаляются в БД черновика (Ф3, с каскадами drop↔survey), затем убираются из локального
|
||||||
|
// представления. Plan-origin включения адресуются по planModeId только когда черновик уже
|
||||||
|
// существует (draftPlanId): тогда включения принадлежат именно ему. Несохранённые (planModeId
|
||||||
|
// нет) — только локально.
|
||||||
const handleDeleteWork = async (workId: string) => {
|
const handleDeleteWork = async (workId: string) => {
|
||||||
const work = draft.works.find((item) => item.id === workId);
|
const work = displayWorks.find((item) => item.id === workId);
|
||||||
if (work?.planModeId != null && draftPlanId) {
|
if (!work) return;
|
||||||
|
const confirmMessage =
|
||||||
|
work.kind === "downlink"
|
||||||
|
? "Удалить включение сброса? Действие необратимо."
|
||||||
|
: "Удалить маршрут съёмки? Действие необратимо.";
|
||||||
|
if (!globalThis.confirm(confirmMessage)) return;
|
||||||
|
if (work.planModeId != null && draftPlanId) {
|
||||||
try {
|
try {
|
||||||
await deleteMode(work.planModeId);
|
await deleteMode(work.planModeId);
|
||||||
await refreshDraftModes(draftPlanId);
|
await refreshDraftModes(draftPlanId);
|
||||||
@@ -900,6 +965,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
surveyModeLabel={surveyModeLabel}
|
surveyModeLabel={surveyModeLabel}
|
||||||
conflicts={conflicts[work.id] ?? []}
|
conflicts={conflicts[work.id] ?? []}
|
||||||
readOnly={work.origin === "plan"}
|
readOnly={work.origin === "plan"}
|
||||||
|
canDelete={draftPlanId != null}
|
||||||
onUpdate={onChangeWork}
|
onUpdate={onChangeWork}
|
||||||
onDelete={(workId) => { void handleDeleteWork(workId); }}
|
onDelete={(workId) => { void handleDeleteWork(workId); }}
|
||||||
onDuplicate={(workId) => {
|
onDuplicate={(workId) => {
|
||||||
@@ -943,6 +1009,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
selectedPlan={selectedPlan}
|
selectedPlan={selectedPlan}
|
||||||
platforms={platforms}
|
platforms={platforms}
|
||||||
checking={checking}
|
checking={checking}
|
||||||
|
planCalcRunning={planCalcRunning}
|
||||||
|
calcBlockedReason={inPlaceEditBlockedReason}
|
||||||
conflictCount={conflictCount(conflicts)}
|
conflictCount={conflictCount(conflicts)}
|
||||||
windowLabel={formatDuration(window.toMs - window.fromMs)}
|
windowLabel={formatDuration(window.toMs - window.fromMs)}
|
||||||
rightMode={rightMode}
|
rightMode={rightMode}
|
||||||
@@ -954,6 +1022,8 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
|||||||
setNotice(undefined);
|
setNotice(undefined);
|
||||||
}}
|
}}
|
||||||
onCheck={runCheck}
|
onCheck={runCheck}
|
||||||
|
onCalculatePlan={() => { void runPlanCalculation(); }}
|
||||||
|
onClearPlan={() => { void clearPlan(); }}
|
||||||
onZoom={zoom}
|
onZoom={zoom}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user