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 6 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
115 changes: 104 additions & 11 deletions photon-client/src/components/cameras/CameraSettingsCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { useStateStore } from "@/stores/StateStore";
import { computed, 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,48 @@
// Reset temp settings on remote camera settings change
resetTempSettingsStruct();
});

const showDeleteCamera = ref(false);
const expected = computed<string>({
mcm001 marked this conversation as resolved.
Show resolved Hide resolved
get() {
return useCameraSettingsStore().cameraNames[useStateStore().currentCameraIndex];
},
set(thing) {}

Check warning on line 118 in photon-client/src/components/cameras/CameraSettingsCard.vue

View workflow job for this annotation

GitHub Actions / PhotonClient Lint and Formatting

'thing' is defined but never used
mcm001 marked this conversation as resolved.
Show resolved Hide resolved
});
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 +187,67 @@
: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-content-save </v-icon>
Delete Config And Unmatch Camera
</v-btn>
</v-col>
</v-row>
</div>

<v-dialog v-model="showDeleteCamera" width="1500" height="900" 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>
<span>This will delete ALL OF YOUR SETTINGS for this camera</span>
</v-row>
<v-row>
<v-btn color="secondary" @click="() => (showDeleteCamera = true)">
<v-icon left class="open-icon"> mdi-export </v-icon>
<span class="open-label">Your final chance to export settings</span>
</v-btn>
</v-row>

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

<v-row>
<v-btn
color="red"
@click="deleteThisCamera"
:disabled="yesDeleteMySettingsText.toLowerCase() !== expected.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-row>
</v-card>
</v-dialog>
</v-card>
</template>

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

.v-divider {
border-color: white !important;
}
</style>
74 changes: 74 additions & 0 deletions photon-client/src/components/settings/DeviceControlCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,39 @@ const handleSettingsImport = () => {
importType.value = -1;
importFile.value = null;
};

const showFactoryReset = ref(false);
const expected = "I solumnly swear that I am up to no good";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just inline this with the text

mcm001 marked this conversation as resolved.
Show resolved Hide resolved
const yesDeleteMySettingsText = ref(expected);
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;
};
</script>

<template>
Expand Down Expand Up @@ -322,11 +355,52 @@ 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-restart-alert </v-icon>
<span class="open-label">Factory Reset PhotonVision and delete everything (the big scary red button)</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>Factory Reset PhotonVision</v-card-title>
<v-row>
<span>This will delete ALL OF YOUR SETTINGS</span>
</v-row>
<v-row>
<v-btn color="secondary" @click="openExportSettingsPrompt">
<v-icon left class="open-icon"> mdi-export </v-icon>
<span class="open-label">Your final chance to export settings</span>
</v-btn>
</v-row>

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

<v-row>
<v-btn
color="red"
@click="nukePhotonConfigDirectory"
:disabled="yesDeleteMySettingsText.toLowerCase() !== expected.toLowerCase()"
>
<v-icon left class="open-icon"> mdi-skull </v-icon>
<span class="open-label">Delete everything; I have backed up what I need</span>
</v-btn>
</v-row>
</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
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ public static void addFileAppender(Path logFilePath) {
currentAppenders.add(new FileLogAppender(logFilePath));
}

public static void closeAllLoggers() {
currentAppenders.forEach(LogAppender::shutdown);
currentAppenders.clear();
}

public static void cleanLogs(Path folderToClean) {
File[] logs = folderToClean.toFile().listFiles();
if (logs == null) return;
Expand Down Expand Up @@ -284,6 +289,9 @@ private static String convertStackTraceToString(Throwable throwable) {

private interface LogAppender {
void log(String message, LogLevel level);

/** Release any file or other resources currently held by the Logger */
default void shutdown() {}
}

private static class ConsoleLogAppender implements LogAppender {
Expand Down Expand Up @@ -343,5 +351,16 @@ public void log(String message, LogLevel level) {
// Nothing to do - no stream available for writing
}
}

@Override
public void shutdown() {
try {
out.close();
out = null;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ private FileUtils() {}
private static final Set<PosixFilePermission> allReadWriteExecutePerms =
new HashSet<>(Arrays.asList(PosixFilePermission.values()));

public static void deleteDirectory(Path path) {
public static boolean deleteDirectory(Path path) {
try {
var files = Files.walk(path);

Expand All @@ -53,8 +53,11 @@ public static void deleteDirectory(Path path) {

// close the stream
files.close();

return true;
} catch (IOException e) {
logger.error("Exception deleting files in " + path + "!", e);
return false;
}
}

Expand Down
Loading
Loading