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

Big scary buttons #1471

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 118 additions & 12 deletions photon-client/src/components/cameras/CameraSettingsCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import PvSelect from "@/components/common/pv-select.vue";
import PvNumberInput from "@/components/common/pv-number-input.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { computed, ref, watchEffect } from "vue";
import { computed, getCurrentInstance, inject, ref, watchEffect } from "vue";
import { type CameraSettingsChangeRequest, ValidQuirks } from "@/types/SettingTypes";
import axios from "axios";

const tempSettingsStruct = ref<CameraSettingsChangeRequest>({
fov: useCameraSettingsStore().currentCameraSettings.fov.value,
Expand Down Expand Up @@ -108,6 +109,53 @@ watchEffect(() => {
// Reset temp settings on remote camera settings change
resetTempSettingsStruct();
});

const interactiveCols = computed(() => {
return getCurrentInstance()?.proxy.$vuetify.breakpoint.smAndDown || false ? 12 : 8;
});

const showDeleteCamera = ref(false);

const address = inject<string>("backendHost");
const exportSettings = ref();
const openExportSettingsPrompt = () => {
exportSettings.value.click();
};

const yesDeleteMySettingsText = ref("");
const deleteThisCamera = () => {
const payload = {
cameraUniqueName: useCameraSettingsStore().cameraUniqueNames[useStateStore().currentCameraIndex]
};

axios
.post("/utils/nukeOneCamera", payload)
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully dispatched the delete command. Waiting for backend to start back up",
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfil the request to delete this camera.",
color: "error"
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
});
showDeleteCamera.value = false;
};
</script>

<template>
Expand Down Expand Up @@ -144,17 +192,75 @@ watchEffect(() => {
:select-cols="8"
/>
<br />
<v-btn
class="mt-2 mb-3"
style="width: 100%"
small
color="secondary"
:disabled="!settingsHaveChanged()"
@click="saveCameraSettings"
>
<v-icon left> mdi-content-save </v-icon>
Save Changes
</v-btn>
<v-row>
<v-col cols="6">
<v-btn
class="mt-2 mb-3"
style="width: 100%"
small
color="secondary"
:disabled="!settingsHaveChanged()"
@click="saveCameraSettings"
>
<v-icon left> mdi-content-save </v-icon>
Save Changes
</v-btn>
</v-col>
<v-col cols="6">
<v-btn class="mt-2 mb-3" style="width: 100%" small color="red" @click="() => (showDeleteCamera = true)">
<v-icon left> mdi-bomb </v-icon>
{{ $vuetify.breakpoint.mdAndUp ? "Delete Config And Unmatch Camera" : "Delete Camera" }}
</v-btn>
</v-col>
</v-row>
</div>

<v-dialog v-model="showDeleteCamera" dark>
<v-card dark class="dialog-container pa-6" color="primary" flat>
<v-card-title
>Delete camera "{{ useCameraSettingsStore().cameraNames[useStateStore().currentCameraIndex] }}"</v-card-title
>
<v-row class="pl-3">
<span class="mt-3"
>This will delete ALL OF YOUR SETTINGS for camera "{{
useCameraSettingsStore().cameraNames[useStateStore().currentCameraIndex]
}}" and restart PhotonVision.</span
>
</v-row>
<v-row class="pl-3">
<v-btn color="secondary" @click="openExportSettingsPrompt" class="mt-3">
<v-icon left class="open-icon"> mdi-export </v-icon>
<span class="open-label">Your final chance to export settings</span>
<a
ref="exportSettings"
style="color: black; text-decoration: none; display: none"
:href="`http://${address}/api/settings/photonvision_config.zip`"
download="photonvision-settings.zip"
target="_blank"
/>
</v-btn>
</v-row>
<v-divider class="mt-8 mb-4" />
<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + useCameraSettingsStore().currentCameraName + '&quot;:'"
:inputCols="interactiveCols"
/>
<v-btn
color="red"
@click="deleteThisCamera"
:disabled="yesDeleteMySettingsText.toLowerCase() !== useCameraSettingsStore().currentCameraName.toLowerCase()"
>
<v-icon left class="open-icon"> mdi-skull </v-icon>
<span class="open-label">Yes, delete this camera; I have backed up what I need</span>
</v-btn>
</v-card>
</v-dialog>
</v-card>
</template>

<style scoped>
.v-divider {
border-color: white !important;
}
</style>
93 changes: 92 additions & 1 deletion photon-client/src/components/settings/DeviceControlCard.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { inject, ref } from "vue";
import { computed, getCurrentInstance, inject, ref } from "vue";
import { useStateStore } from "@/stores/StateStore";
import PvSelect from "@/components/common/pv-select.vue";
import axios from "axios";
Expand Down Expand Up @@ -201,6 +201,45 @@ const handleSettingsImport = () => {
importType.value = -1;
importFile.value = null;
};

const showFactoryReset = ref(false);
const expected = "I have backed up my settings, now rid my coprocessor of them";
const yesDeleteMySettingsText = ref("");
const nukePhotonConfigDirectory = () => {
axios
.post("/utils/nukeConfigDirectory")
.then(() => {
useStateStore().showSnackbarMessage({
message: "Successfully dispatched the reset command. Waiting for backend to start back up",
color: "success"
});
})
.catch((error) => {
if (error.response) {
useStateStore().showSnackbarMessage({
message: "The backend is unable to fulfil the request to reset the device.",
color: "error"
});
} else if (error.request) {
useStateStore().showSnackbarMessage({
message: "Error while trying to process the request! The backend didn't respond.",
color: "error"
});
} else {
useStateStore().showSnackbarMessage({
message: "An error occurred while trying to process the request.",
color: "error"
});
}
});
showFactoryReset.value = false;
};

const interactiveCols = computed(() => {
const ret = getCurrentInstance()?.proxy.$vuetify.breakpoint.smAndDown || false ? 12 : 8;
console.log(ret);
return ret;
});
</script>

<template>
Expand Down Expand Up @@ -322,11 +361,63 @@ const handleSettingsImport = () => {
</v-btn>
</v-col>
</v-row>
<v-divider style="margin: 12px 0" />
<v-row>
<v-col cols="12">
<v-btn color="red" @click="() => (showFactoryReset = true)">
<v-icon left class="open-icon"> mdi-skull-crossbones </v-icon>
<span class="open-icon">
{{
$vuetify.breakpoint.mdAndUp
? "Factory Reset PhotonVision and delete EVERYTHING (big scary button)"
: "Factory Reset PhotonVision"
}}
</span>
</v-btn>
</v-col>
</v-row>
</div>

<v-dialog v-model="showFactoryReset" width="1500" height="900" dark>
<v-card dark class="dialog-container pa-6" color="primary" flat>
<v-card-title>
<v-icon right color="red" class="open-icon"> mdi-nuke </v-icon>
Factory Reset PhotonVision
<v-icon right color="red" class="open-icon"> mdi-nuke </v-icon>
</v-card-title>
<span>This will delete ALL OF YOUR SETTINGS!</span>
<v-btn color="secondary" @click="openExportSettingsPrompt" class="mt-2">
<v-icon left class="open-icon"> mdi-export </v-icon>
<span class="open-label">Your final chance to export settings</span>
</v-btn>

<v-divider class="mt-4 mb-4" />

<pv-input
v-model="yesDeleteMySettingsText"
:label="'Type &quot;' + expected + '&quot;:'"
:inputCols="interactiveCols"
/>

<v-btn
color="red"
@click="nukePhotonConfigDirectory"
:disabled="yesDeleteMySettingsText.toLowerCase() !== expected.toLowerCase()"
>
<v-icon left class="open-icon"> mdi-skull-crossbones </v-icon>
<span class="open-label">Delete everything, I have backed up what I need</span>
<v-icon left class="open-icon ml-1"> mdi-skull-crossbones </v-icon>
</v-btn>
</v-card>
</v-dialog>
</v-card>
</template>

<style scoped>
.dialog-container {
min-height: 300px !important;
}

.v-divider {
border-color: white !important;
}
Expand Down
3 changes: 3 additions & 0 deletions photon-client/src/stores/settings/CameraSettingsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
cameraNames(): string[] {
return this.cameras.map((c) => c.nickname);
},
cameraUniqueNames(): string[] {
return this.cameras.map((c) => c.nickname);
},
currentCameraName(): string {
return this.cameraNames[useStateStore().currentCameraIndex];
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public class ConfigManager {
// special case flag to disable flushing settings to disk at shutdown. Avoids the jvm shutdown
// hook overwriting the settings we just uploaded
private boolean flushOnShutdown = true;
private boolean allowWriteTask = false;

enum ConfigSaveStrategy {
SQL,
Expand Down Expand Up @@ -135,14 +136,20 @@ private void translateLegacyIfPresent(Path folderPath) {
}
}

public static boolean nukeConfigDirectory() {
return FileUtils.deleteDirectory(getRootFolder());
}

public static boolean saveUploadedSettingsZip(File uploadPath) {
// Unpack to /tmp/something/photonvision
var folderPath = Path.of(System.getProperty("java.io.tmpdir"), "photonvision").toFile();
folderPath.mkdirs();
ZipUtil.unpack(uploadPath, folderPath);

// Nuke the current settings directory
FileUtils.deleteDirectory(getRootFolder());
if (!nukeConfigDirectory()) {
return false;
}

// If there's a cameras folder in the upload, we know we need to import from the
// old style
Expand Down Expand Up @@ -301,7 +308,9 @@ public void saveToDisk() {
private void saveAndWriteTask() {
// Only save if 1 second has past since the request was made
while (!Thread.currentThread().isInterrupted()) {
if (saveRequestTimestamp > 0 && (System.currentTimeMillis() - saveRequestTimestamp) > 1000L) {
if (saveRequestTimestamp > 0
&& (System.currentTimeMillis() - saveRequestTimestamp) > 1000L
&& allowWriteTask) {
saveRequestTimestamp = -1;
logger.debug("Saving to disk...");
saveToDisk();
Expand Down Expand Up @@ -330,6 +339,11 @@ public void disableFlushOnShutdown() {
this.flushOnShutdown = false;
}

/** Prevent pending automatic saves */
public void setWriteTaskEnabled(boolean enabled) {
this.allowWriteTask = enabled;
}

public void onJvmExit() {
if (flushOnShutdown) {
logger.info("Force-flushing settings...");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ public void addCameraConfig(String name, CameraConfiguration config) {
cameraConfigurations.put(name, config);
}

/**
* Delete a camera by its unique name
*
* @param name The camera name (usually unique name)
* @return True if the camera configuration was removed
*/
public boolean removeCameraConfig(String name) {
return cameraConfigurations.remove(name) != null;
}

public Map<String, Object> toHashMap() {
Map<String, Object> map = new HashMap<>();
var settingsSubmap = new HashMap<String, Object>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,19 @@ private String getOneConfigFile(Connection conn, String filename) {

private void saveCameras(Connection conn) {
try {
// Delete all cameras we don't need anymore
String deleteExtraCamsString =
String.format(
"DELETE FROM %s WHERE %s not in (%s)",
Tables.CAMERAS,
Columns.CAM_UNIQUE_NAME,
config.getCameraConfigurations().keySet().stream()
.map(it -> "\"" + it + "\"")
.collect(Collectors.joining(", ")));

var stmt = conn.createStatement();
stmt.executeUpdate(deleteExtraCamsString);

// Replace this camera's row with the new settings
var sqlString =
String.format(
Expand Down Expand Up @@ -388,6 +401,7 @@ private void saveCameras(Connection conn) {

statement.executeUpdate();
}

} catch (SQLException | IOException e) {
logger.error("Err saving cameras", e);
try {
Expand Down
Loading
Loading