Skip to content
Snippets Groups Projects
Commit 892f4b1a authored by Daniel Göbel's avatar Daniel Göbel
Browse files

Merge branch 'feature/16-manipulate-s3-keys' into 'development'

Manipulate S3 keys

Closes #16

See merge request denbi/object-storage-access-ui!15
parents 4ecec278 fe76ce70
No related branches found
No related tags found
2 merge requests!22Version 1.0.0,!15Manipulate S3 keys
Source diff could not be displayed: it is too large. Options to address this: view the blob.
{ {
"name": "proxyapi-ui", "name": "proxyapi-ui",
"version": "0.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "run-p type-check build-only", "build": "run-p type-check build-only",
...@@ -11,16 +11,16 @@ ...@@ -11,16 +11,16 @@
"generate-client": "openapi --input http://localhost:9999/api/openapi.json --output src/client --client axios" "generate-client": "openapi --input http://localhost:9999/api/openapi.json --output src/client --client axios"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.165.0", "@aws-sdk/client-s3": "^3.186.0",
"@aws-sdk/s3-request-presigner": "^3.165.0", "@aws-sdk/s3-request-presigner": "^3.186.0",
"@aws-sdk/lib-storage": "^3.165.0", "@aws-sdk/lib-storage": "^3.186.0",
"@popperjs/core": "^2.11.6", "@popperjs/core": "^2.11.6",
"bootstrap": "^5.2.1", "bootstrap": "^5.2.2",
"bootstrap-icons": "^1.9.1", "bootstrap-icons": "^1.9.1",
"dayjs": "^1.11.5", "dayjs": "^1.11.5",
"filesize": "^9.0.11", "filesize": "^10.0.5",
"pinia": "^2.0.22", "pinia": "^2.0.23",
"vue": "3.2.37", "vue": "^3.2.40",
"vue-router": "^4.1.5", "vue-router": "^4.1.5",
"vue3-cookies": "^1.0.6" "vue3-cookies": "^1.0.6"
}, },
...@@ -28,13 +28,13 @@ ...@@ -28,13 +28,13 @@
"@esbuild-plugins/node-globals-polyfill": "^0.1.1", "@esbuild-plugins/node-globals-polyfill": "^0.1.1",
"@esbuild-plugins/node-modules-polyfill": "^0.1.4", "@esbuild-plugins/node-modules-polyfill": "^0.1.4",
"@rushstack/eslint-patch": "^1.1.0", "@rushstack/eslint-patch": "^1.1.0",
"@types/bootstrap": "^5.2.0", "@types/bootstrap": "^5.2.5",
"@types/node": "^16.11.45", "@types/node": "^16.11.45",
"@vitejs/plugin-vue": "^3.1.0", "@vitejs/plugin-vue": "^3.1.2",
"@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.1", "@vue/eslint-config-typescript": "^11.0.1",
"@vue/tsconfig": "^0.1.3", "@vue/tsconfig": "^0.1.3",
"axios": "^0.27.2", "axios": "^1.1.2",
"eslint": "^8.23.0", "eslint": "^8.23.0",
"eslint-plugin-vue": "^9.4.0", "eslint-plugin-vue": "^9.4.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
"rollup-plugin-node-polyfills": "^0.2.1", "rollup-plugin-node-polyfills": "^0.2.1",
"sass": "^1.54.9", "sass": "^1.54.9",
"typescript": "~4.7.4", "typescript": "~4.7.4",
"vite": "^3.1.0", "vite": "^3.1.7",
"vue-tsc": "^0.40.13" "vue-tsc": "^0.40.13"
} }
} }
...@@ -4,3 +4,7 @@ body { ...@@ -4,3 +4,7 @@ body {
max-height: 100vh; max-height: 100vh;
background: #181818; background: #181818;
} }
.top-toast {
top: 4rem;
}
...@@ -3,7 +3,7 @@ import type { BucketOut, BucketPermission } from "@/client"; ...@@ -3,7 +3,7 @@ import type { BucketOut, BucketPermission } from "@/client";
import BootstrapIcon from "@/components/BootstrapIcon.vue"; import BootstrapIcon from "@/components/BootstrapIcon.vue";
import PermissionModal from "@/components/Modals/PermissionModal.vue"; import PermissionModal from "@/components/Modals/PermissionModal.vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import fileSize from "filesize"; import { filesize } from "filesize";
import { onMounted } from "vue"; import { onMounted } from "vue";
import { Tooltip } from "bootstrap"; import { Tooltip } from "bootstrap";
...@@ -114,7 +114,7 @@ onMounted(() => { ...@@ -114,7 +114,7 @@ onMounted(() => {
</tr> </tr>
<tr> <tr>
<th scope="row" class="fw-bold">Size:</th> <th scope="row" class="fw-bold">Size:</th>
<td>{{ fileSize(0) }}</td> <td>{{ filesize(0) }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
......
...@@ -8,7 +8,7 @@ import type { ...@@ -8,7 +8,7 @@ import type {
} from "@/client"; } from "@/client";
import { ObjectService } from "@/client"; import { ObjectService } from "@/client";
import BootstrapIcon from "@/components/BootstrapIcon.vue"; import BootstrapIcon from "@/components/BootstrapIcon.vue";
import fileSize from "filesize"; import { filesize } from "filesize";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { Toast, Tooltip } from "bootstrap"; import { Toast, Tooltip } from "bootstrap";
import PermissionListModal from "@/components/Modals/PermissionListModal.vue"; import PermissionListModal from "@/components/Modals/PermissionListModal.vue";
...@@ -515,7 +515,7 @@ watch( ...@@ -515,7 +515,7 @@ watch(
</script> </script>
<template> <template>
<div class="toast-container position-fixed top-0 end-0 p-3"> <div class="toast-container position-fixed top-toast end-0 p-3">
<div <div
role="alert" role="alert"
aria-live="assertive" aria-live="assertive"
...@@ -782,7 +782,7 @@ watch( ...@@ -782,7 +782,7 @@ watch(
>{{ dayjs(obj.last_modified).fromNow() }}</span >{{ dayjs(obj.last_modified).fromNow() }}</span
> >
</td> </td>
<td>{{ fileSize(obj.size) }}</td> <td>{{ filesize(obj.size) }}</td>
<!-- Show buttons with dropdown menu if row is an object --> <!-- Show buttons with dropdown menu if row is an object -->
<td class="text-end"> <td class="text-end">
<div <div
......
...@@ -90,7 +90,7 @@ onMounted(() => { ...@@ -90,7 +90,7 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="toast-container position-fixed top-0 end-0 p-3"> <div class="toast-container position-fixed top-toast end-0 p-3">
<div <div
role="alert" role="alert"
aria-live="assertive" aria-live="assertive"
...@@ -110,7 +110,7 @@ onMounted(() => { ...@@ -110,7 +110,7 @@ onMounted(() => {
</div> </div>
</div> </div>
</div> </div>
<div class="toast-container position-fixed top-0 end-0 p-3"> <div class="toast-container position-fixed top-toast end-0 p-3">
<div <div
role="alert" role="alert"
aria-live="assertive" aria-live="assertive"
......
...@@ -79,7 +79,7 @@ onMounted(() => { ...@@ -79,7 +79,7 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="toast-container position-fixed top-0 end-0 p-3"> <div class="toast-container position-fixed top-toast end-0 p-3">
<div <div
role="alert" role="alert"
aria-live="assertive" aria-live="assertive"
...@@ -99,7 +99,7 @@ onMounted(() => { ...@@ -99,7 +99,7 @@ onMounted(() => {
</div> </div>
</div> </div>
</div> </div>
<div class="toast-container position-fixed top-0 end-0 p-3"> <div class="toast-container position-fixed top-toast end-0 p-3">
<div <div
role="alert" role="alert"
aria-live="assertive" aria-live="assertive"
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import BootstrapModal from "@/components/Modals/BootstrapModal.vue"; import BootstrapModal from "@/components/Modals/BootstrapModal.vue";
import type { S3ObjectMetaInformation } from "@/client"; import type { S3ObjectMetaInformation } from "@/client";
import dayjs from "dayjs"; import dayjs from "dayjs";
import fileSize from "filesize"; import { filesize } from "filesize";
const props = defineProps<{ const props = defineProps<{
modalID: string; modalID: string;
...@@ -48,7 +48,7 @@ const props = defineProps<{ ...@@ -48,7 +48,7 @@ const props = defineProps<{
</tr> </tr>
<tr> <tr>
<th scope="row">Size</th> <th scope="row">Size</th>
<td>{{ fileSize(props.s3Object.size) }}</td> <td>{{ filesize(props.s3Object.size) }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
......
...@@ -290,7 +290,7 @@ onMounted(() => { ...@@ -290,7 +290,7 @@ onMounted(() => {
confirmedDeletePermission(permission.bucket_name, permission.uid) confirmedDeletePermission(permission.bucket_name, permission.uid)
" "
/> />
<div class="toast-container position-fixed top-0 end-0 p-3"> <div class="toast-container position-fixed top-toast end-0 p-3">
<div <div
role="alert" role="alert"
aria-live="assertive" aria-live="assertive"
......
...@@ -6,7 +6,7 @@ import { computed, onMounted, reactive, watch } from "vue"; ...@@ -6,7 +6,7 @@ import { computed, onMounted, reactive, watch } from "vue";
import type { ComputedRef } from "vue"; import type { ComputedRef } from "vue";
import type { S3ObjectMetaInformation } from "@/client"; import type { S3ObjectMetaInformation } from "@/client";
import dayjs from "dayjs"; import dayjs from "dayjs";
import fileSize from "filesize"; import { filesize } from "filesize";
import { Modal, Toast } from "bootstrap"; import { Modal, Toast } from "bootstrap";
const props = defineProps<{ const props = defineProps<{
...@@ -124,7 +124,7 @@ onMounted(() => { ...@@ -124,7 +124,7 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="toast-container position-fixed top-0 end-0 p-3"> <div class="toast-container position-fixed top-toast end-0 p-3">
<div <div
role="alert" role="alert"
aria-live="assertive" aria-live="assertive"
...@@ -144,7 +144,7 @@ onMounted(() => { ...@@ -144,7 +144,7 @@ onMounted(() => {
</div> </div>
</div> </div>
</div> </div>
<div class="toast-container position-fixed top-0 end-0 p-3"> <div class="toast-container position-fixed top-toast end-0 p-3">
<div <div
role="alert" role="alert"
aria-live="assertive" aria-live="assertive"
...@@ -257,8 +257,8 @@ onMounted(() => { ...@@ -257,8 +257,8 @@ onMounted(() => {
</div> </div>
</div> </div>
<span v-if="formState.uploadDone > 0"> <span v-if="formState.uploadDone > 0">
{{ fileSize(formState.uploadDone) }} / {{ filesize(formState.uploadDone) }} /
{{ fileSize(formState.uploadTotal) }} {{ filesize(formState.uploadTotal) }}
</span> </span>
</div> </div>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
......
<script setup lang="ts">
import type { S3Key } from "@/client";
import type { Ref } from "vue";
import { ref, watch } from "vue";
import BootstrapIcon from "@/components/BootstrapIcon.vue";
import DeleteModal from "@/components/Modals/DeleteModal.vue";
const props = defineProps<{
s3key: S3Key;
deletable: boolean;
loading: boolean;
}>();
const emit = defineEmits<{
(e: "delete-key", accessKey: string): void;
}>();
watch(
() => props.s3key.access_key,
() => {
visibleSecret.value = false;
}
);
const visibleSecret: Ref<boolean> = ref(false);
function deleteKeyTrigger() {
if (props.deletable) {
emit("delete-key", props.s3key.access_key);
}
}
</script>
<template>
<DeleteModal
modalID="delete-key-modal"
modal-label="Delete S3 Key"
:object-name-delete="props.s3key.access_key"
:back-modal-id="undefined"
@confirm-delete="deleteKeyTrigger"
/>
<h3>Access Key:</h3>
<div v-if="props.loading" class="placeholder-glow">
<span class="placeholder col-5 mt-3 mb-2 fs-4"></span><br />
</div>
<input
v-else
class="form-control-plaintext text-white fs-4"
type="text"
:value="props.s3key.access_key"
aria-label="S3 Access Key"
readonly
/>
<div class="d-flex align-items-center">
<span class="fs-3">Secret Key:</span>
<button
class="btn btn-outline-secondary ms-3"
:class="{ active: visibleSecret }"
data-bs-toggle="button"
:disabled="props.loading"
@click="visibleSecret = !visibleSecret"
>
<bootstrap-icon
:width="18"
:height="18"
fill="white"
:icon="visibleSecret ? 'eye' : 'eye-slash'"
/>
</button>
</div>
<div v-if="props.loading" class="placeholder-glow">
<span class="placeholder col-7 mt-3 mb-4 fs-4"></span><br />
</div>
<input
v-else
id="s3-secret-key"
class="form-control-plaintext text-white fs-4 mb-3"
:type="visibleSecret ? 'text' : 'password'"
:value="props.s3key.secret_key"
aria-label="S3 Access Key"
aria-describedby="s3-secret-key"
readonly
/>
<button
type="button"
class="btn btn-danger fs-5"
:disabled="!props.deletable || props.loading"
data-bs-toggle="modal"
data-bs-target="#delete-key-modal"
>
Delete
</button>
</template>
<style scoped></style>
...@@ -26,9 +26,6 @@ const router = createRouter({ ...@@ -26,9 +26,6 @@ const router = createRouter({
{ {
path: "object-storage/s3-keys", path: "object-storage/s3-keys",
name: "s3_keys", name: "s3_keys",
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import("../views/object-storage/S3KeysView.vue"), component: () => import("../views/object-storage/S3KeysView.vue"),
}, },
], ],
......
<script setup lang="ts">
import S3KeyView from "@/components/S3KeyView.vue";
import BootstrapIcon from "@/components/BootstrapIcon.vue";
import { reactive, onMounted, computed } from "vue";
import type { ComputedRef } from "vue";
import type { S3Key } from "@/client";
import { KeyService } from "@/client";
import { useAuthStore } from "@/stores/auth";
import { Toast } from "bootstrap";
const authStore = useAuthStore();
authStore.$onAction(({ name, args }) => {
if (name === "updateUser") {
refreshKeys(args[0].uid);
}
});
let successToast: Toast | null = null;
const keyState = reactive({
keys: [],
activeKey: 0,
initialLoading: true,
deletedKey: "",
} as {
keys: S3Key[];
activeKey: number;
initialLoading: boolean;
deletedKey: string;
});
const allowKeyDeletion: ComputedRef<boolean> = computed(
() => keyState.keys.length > 1
);
function refreshKeys(uid: string) {
KeyService.keyGetUserKeys(uid)
.then((keys) => {
if (keyState.activeKey >= keys.length) {
keyState.activeKey = keys.length - 1;
}
keyState.keys = keys;
})
.catch((err) => console.error(err))
.finally(() => (keyState.initialLoading = false));
}
function deleteKey(accessKey: string) {
if (allowKeyDeletion.value && authStore.user != null) {
KeyService.keyDeleteUserKey(accessKey, authStore.user.uid)
.then(() => {
keyState.deletedKey = accessKey;
keyState.activeKey = 0;
keyState.keys = keyState.keys.filter(
(s3key) => s3key.access_key !== accessKey
);
authStore.setS3Key(keyState.keys[0]);
successToast?.show();
})
.catch((err) => console.error(err));
}
}
function createKey() {
if (authStore.user != null) {
KeyService.keyCreateUserKey(authStore.user.uid)
.then((s3key) => {
keyState.keys.push(s3key);
keyState.keys = [...keyState.keys].sort((keyA, keyB) =>
keyA.access_key > keyB.access_key ? 1 : -1
);
})
.catch((err) => console.error(err));
}
}
onMounted(() => {
successToast = new Toast("#successKeyToast");
if (authStore.user != null) {
refreshKeys(authStore.user.uid);
}
});
</script>
<template> <template>
<div class="about"> <div class="toast-container position-fixed top-toast end-0 p-3">
<h1>This is the S3 Key Page</h1> <div
role="alert"
aria-live="assertive"
aria-atomic="true"
class="toast text-bg-success align-items-center border-0"
data-bs-autohide="true"
:id="'successKeyToast'"
>
<div class="d-flex">
<div class="toast-body">
Successfully deleted S3 Key {{ keyState.deletedKey }}
</div>
<button
type="button"
class="btn-close btn-close-white me-2 m-auto"
data-bs-dismiss="toast"
aria-label="Close"
></button>
</div>
</div>
</div>
<div class="row m-2 border-bottom border-light mt-4">
<div class="col-12"></div>
<h1 class="mb-2 text-light">S3 Keys</h1>
</div>
<div class="row m-2 mt-4">
<div class="col-4">
<div class="d-flex justify-content-between mb-4">
<button
type="button"
class="btn btn-light"
@click="refreshKeys(authStore.user?.uid ?? 'impossible')"
>
<bootstrap-icon icon="arrow-clockwise" />
<span class="visually-hidden">Refresh S3 Keys</span>
</button>
<button type="button" class="btn btn-light" @click="createKey">
<bootstrap-icon icon="plus-lg" />
<span class="visually-hidden">Create S3 Key</span>
</button>
</div>
<div v-if="keyState.initialLoading" class="placeholder-glow">
<a
v-for="n in 3"
:key="n"
type="button"
aria-hidden="true"
class="btn w-100 fs-5 mb-3 btn-secondary disabled placeholder"
/>
</div>
<div v-else>
<button
v-for="(s3key, index) in keyState.keys"
:key="s3key.access_key"
class="btn w-100 fs-5 mb-3"
type="button"
@click="keyState.activeKey = index"
:class="{
'btn-light': keyState.activeKey !== index,
'btn-primary': keyState.activeKey === index,
}"
>
{{ s3key.access_key }}
</button>
</div>
</div>
<div class="col-7 offset-md-1">
<s3-key-view
v-if="keyState.keys.length > 0 || keyState.initialLoading"
:s3key="
keyState.initialLoading
? { user: '', access_key: '', secret_key: '' }
: keyState.keys[keyState.activeKey]
"
:deletable="allowKeyDeletion"
:loading="keyState.initialLoading"
@delete-key="deleteKey"
/>
<div v-else>
No keys here. <br />
Create a new Key to interact with your Buckets again.
</div>
</div>
</div> </div>
</template> </template>
<style> <style scoped></style>
@media (min-width: 1024px) {
.about {
display: flex;
align-items: center;
}
}
</style>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment