Skip to content

Commit 1985492

Browse files
wilfred-asomaniiStorj Robot
authored andcommitted
satellite/admin-ui: add update bucket UI
Issue: #7661 Change-Id: I845a9929d0927f81d1d365abf754759a02f5640c
1 parent c7ace71 commit 1985492

File tree

6 files changed

+210
-121
lines changed

6 files changed

+210
-121
lines changed

satellite/admin/back-office/ui/src/components/BucketActionsMenu.vue

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,19 @@
44
<template>
55
<v-menu activator="parent">
66
<v-list class="pa-2">
7-
<v-list-item v-if="featureFlags.bucket.view" density="comfortable" link rounded="lg" base-color="info" router-link to="/bucket-details">
7+
<!--<v-list-item v-if="featureFlags.bucket.view" density="comfortable" link rounded="lg" base-color="info" router-link to="/bucket-details">
88
<v-list-item-title class="text-body-2 font-weight-medium">
99
View Bucket
1010
</v-list-item-title>
11-
</v-list-item>
12-
13-
<v-divider v-if="featureFlags.bucket.updateInfo || featureFlags.bucket.updateValueAttribution || featureFlags.bucket.updatePlacement" class="my-2" />
11+
</v-list-item>-->
1412

15-
<v-list-item v-if="featureFlags.bucket.updateInfo" density="comfortable" link rounded="lg">
13+
<v-list-item v-if="hasUpdatePerm" density="comfortable" link rounded="lg" @click="emit('update', bucket)">
1614
<v-list-item-title class="text-body-2 font-weight-medium">
1715
Edit Bucket
18-
<BucketInformationDialog />
19-
</v-list-item-title>
20-
</v-list-item>
21-
22-
<v-list-item v-if="featureFlags.bucket.updateValueAttribution" density="comfortable" link rounded="lg">
23-
<v-list-item-title class="text-body-2 font-weight-medium">
24-
Set Value
25-
<BucketUserAgentsDialog />
26-
</v-list-item-title>
27-
</v-list-item>
28-
29-
<v-list-item v-if="featureFlags.bucket.updatePlacement" density="comfortable" link rounded="lg">
30-
<v-list-item-title class="text-body-2 font-weight-medium">
31-
Set Placement
32-
<BucketGeofenceDialog />
3316
</v-list-item-title>
3417
</v-list-item>
3518

36-
<v-divider v-if="featureFlags.bucket.delete" class="my-2" />
37-
38-
<v-list-item v-if="featureFlags.bucket.delete" density="comfortable" link rounded="lg" base-color="error">
19+
<v-list-item v-if="featureFlags.delete" density="comfortable" link rounded="lg" base-color="error">
3920
<v-list-item-title class="text-body-2 font-weight-medium">
4021
Delete Bucket
4122
<BucketDeleteDialog />
@@ -46,15 +27,26 @@
4627
</template>
4728

4829
<script setup lang="ts">
49-
import { VMenu, VList, VListItem, VListItemTitle, VDivider } from 'vuetify/components';
30+
import { VList, VListItem, VListItemTitle, VMenu } from 'vuetify/components';
31+
import { computed } from 'vue';
5032
51-
import { FeatureFlags } from '@/api/client.gen';
33+
import { BucketFlags, BucketInfo } from '@/api/client.gen';
5234
import { useAppStore } from '@/store/app';
5335
54-
import BucketInformationDialog from '@/components/BucketInformationDialog.vue';
5536
import BucketDeleteDialog from '@/components/BucketDeleteDialog.vue';
56-
import BucketGeofenceDialog from '@/components/BucketGeofenceDialog.vue';
57-
import BucketUserAgentsDialog from '@/components/BucketUserAgentsDialog.vue';
5837
59-
const featureFlags = useAppStore().state.settings.admin.features as FeatureFlags;
38+
defineProps<{
39+
bucket: BucketInfo;
40+
}>();
41+
42+
const emit = defineEmits<{
43+
(e: 'update', bucket: BucketInfo): void;
44+
}>();
45+
46+
const featureFlags = computed(() => useAppStore().state.settings.admin.features.bucket as BucketFlags);
47+
48+
const hasUpdatePerm = computed(() => {
49+
return featureFlags.value.updatePlacement ||
50+
featureFlags.value.updateValueAttribution;
51+
});
6052
</script>

satellite/admin/back-office/ui/src/components/BucketInformationDialog.vue

Lines changed: 0 additions & 86 deletions
This file was deleted.
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright (C) 2025 Storj Labs, Inc.
2+
// See LICENSE for copying information.
3+
4+
<template>
5+
<RequireReasonFormDialog
6+
v-model="model"
7+
:loading="isLoading"
8+
:initial-form-data="initialFormData"
9+
:form-config="formConfig"
10+
title="Update Project"
11+
width="550"
12+
@submit="update"
13+
/>
14+
</template>
15+
16+
<script setup lang="ts">
17+
import { computed, ref, watch } from 'vue';
18+
19+
import { useAppStore } from '@/store/app';
20+
import { useBucketsStore } from '@/store/buckets';
21+
import { useNotify } from '@/composables/useNotify';
22+
import { useLoading } from '@/composables/useLoading';
23+
import { BucketFlags, BucketInfo, BucketState, Project, UpdateBucketRequest } from '@/api/client.gen';
24+
import { FieldType, FormConfig, FormField } from '@/types/forms';
25+
import { RequiredRule } from '@/types/common';
26+
27+
import RequireReasonFormDialog from '@/components/RequireReasonFormDialog.vue';
28+
29+
const appStore = useAppStore();
30+
const bucketsStore = useBucketsStore();
31+
32+
const notify = useNotify();
33+
const { isLoading, withLoading } = useLoading();
34+
35+
const model = defineModel<boolean>({ required: true });
36+
37+
const props = defineProps<{
38+
project: Project;
39+
bucket: BucketInfo;
40+
}>();
41+
42+
const emit = defineEmits<{
43+
(e: 'updated', bucket: BucketInfo): void;
44+
}>();
45+
46+
const bucketState = ref<BucketState>();
47+
48+
const featureFlags = computed<BucketFlags>(() => appStore.state.settings.admin.features.bucket);
49+
50+
const placements = computed(() => appStore.state.placements.filter(p => !!p.location));
51+
52+
const initialFormData = computed(() => ({
53+
userAgent: props.bucket.userAgent ?? '',
54+
placement: props.bucket.placement ?? 0,
55+
}));
56+
57+
const formConfig = computed((): FormConfig => {
58+
const config: FormConfig = {
59+
sections: [{ rows: [] }],
60+
};
61+
62+
const firstRowFields: FormField[] = [];
63+
if (featureFlags.value.updateValueAttribution)
64+
firstRowFields.push({
65+
key: 'userAgent',
66+
type: FieldType.Text,
67+
label: 'Useragent',
68+
clearable: true,
69+
transform: {
70+
back: (value) => value ?? '',
71+
},
72+
});
73+
if (firstRowFields.length > 0) config.sections[0].rows.push({ fields: firstRowFields });
74+
75+
const secondRowFields: FormField[] = [];
76+
if (featureFlags.value.updatePlacement)
77+
secondRowFields.push({
78+
key: 'placement',
79+
type: FieldType.Select,
80+
label: 'Bucket Placement',
81+
placeholder: 'Select bucket placement',
82+
items: placements.value,
83+
disabled: !bucketState.value?.empty,
84+
messages: (_) => {
85+
if (!bucketState.value?.empty) {
86+
return ['Placement can only be changed for empty buckets.'];
87+
}
88+
return [];
89+
},
90+
itemTitle: 'location',
91+
itemValue: 'id',
92+
rules: [RequiredRule],
93+
required: true,
94+
});
95+
96+
if (secondRowFields.length > 0) config.sections[0].rows.push({ fields: secondRowFields });
97+
98+
return config;
99+
});
100+
101+
function update(formData: Record<string, unknown>) {
102+
withLoading(async () => {
103+
const request = new UpdateBucketRequest();
104+
for (const key in request) {
105+
if (!Object.hasOwn(formData, key)) continue;
106+
if (formData[key] === initialFormData.value[key]) continue;
107+
// set only changed fields
108+
request[key] = formData[key];
109+
}
110+
111+
try {
112+
await bucketsStore.updateBucket(props.project.id, props.bucket.name, request);
113+
114+
const updated = { ...props.bucket };
115+
if (request.userAgent !== undefined)
116+
updated.userAgent = request.userAgent as string;
117+
if (request.placement !== undefined) {
118+
const placement = placements.value.find(p => p.id === request.placement);
119+
updated.placement = placement ? placement.location : 'Unknown';
120+
}
121+
model.value = false;
122+
emit('updated', updated);
123+
notify.success('Bucket updated successfully!');
124+
} catch (e) {
125+
notify.error(`Failed to update bucket. ${e.message}`);
126+
}
127+
});
128+
}
129+
130+
watch(() => props.bucket, () => {
131+
withLoading(async () => {
132+
try {
133+
bucketState.value = await bucketsStore.getBucketState(props.project.id, props.bucket.name);
134+
} catch (error) {
135+
notify.error('Failed to load bucket state', error);
136+
}
137+
});
138+
}, { immediate: true });
139+
</script>

satellite/admin/back-office/ui/src/components/BucketsTableComponent.vue

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@
3030
variant="outlined" color="default" size="small" class="mr-1 text-caption" density="comfortable" icon
3131
width="24" height="24"
3232
>
33-
<BucketActionsMenu />
33+
<BucketActionsMenu
34+
:bucket="item"
35+
@update="bucket => {
36+
bucketToUpdate = bucket;
37+
updateBucketDialog = true;
38+
}"
39+
/>
3440
<v-icon :icon="MoreHorizontal" />
3541
</v-btn>
3642
<v-chip variant="text" size="small" class="font-weight-bold pl-1 ml-1">
@@ -55,6 +61,14 @@
5561
</template>
5662
</v-data-table-server>
5763
</v-card>
64+
65+
<BucketUpdateDialog
66+
v-if="bucketToUpdate"
67+
v-model="updateBucketDialog"
68+
:bucket="bucketToUpdate"
69+
:project="props.project"
70+
@updated="onUpdated"
71+
/>
5872
</template>
5973

6074
<script setup lang="ts">
@@ -64,13 +78,14 @@ import { MoreHorizontal, Search } from 'lucide-vue-next';
6478
import { useDate } from 'vuetify';
6579
6680
import { DataTableHeader } from '@/types/common';
67-
import { BucketInfoPage, Project } from '@/api/client.gen';
81+
import { BucketInfo, BucketInfoPage, Project } from '@/api/client.gen';
6882
import { useLoading } from '@/composables/useLoading';
6983
import { useNotify } from '@/composables/useNotify';
7084
import { Memory, Size } from '@/utils/bytesSize';
7185
import { useBucketsStore } from '@/store/buckets';
7286
7387
import BucketActionsMenu from '@/components/BucketActionsMenu.vue';
88+
import BucketUpdateDialog from '@/components/BucketUpdateDialog.vue';
7489
7590
const bucketsStore = useBucketsStore();
7691
@@ -86,6 +101,8 @@ const search = ref<string>('');
86101
const pageSize = ref<number>(10);
87102
const searchTimer = ref<NodeJS.Timeout>();
88103
const bucketPage = ref<BucketInfoPage>();
104+
const updateBucketDialog = ref<boolean>(false);
105+
const bucketToUpdate = ref<BucketInfo>();
89106
90107
const headers: DataTableHeader[] = [
91108
{ title: 'Bucket', key: 'name' },
@@ -106,6 +123,15 @@ const headers: DataTableHeader[] = [
106123
},
107124
];
108125
126+
function onUpdated(bucket: BucketInfo) {
127+
if (!bucketPage.value?.items) return;
128+
const index = bucketPage.value?.items?.findIndex(b => b.name === bucket.name) ?? -1;
129+
if (index === -1) return;
130+
const items = bucketPage.value.items;
131+
items[index] = bucket;
132+
bucketPage.value = { ...bucketPage.value, items };
133+
}
134+
109135
/**
110136
* Handles update table rows limit event.
111137
*/
@@ -145,6 +171,14 @@ watch(search, () => {
145171
}, 500); // 500ms delay for every new call.
146172
});
147173
174+
watch(updateBucketDialog, async (newVal) => {
175+
if (newVal) return;
176+
177+
// wait for dialog to close
178+
await new Promise(resolve => setTimeout(resolve, 300));
179+
bucketToUpdate.value = undefined;
180+
});
181+
148182
onMounted(() => {
149183
fetchBuckets();
150184
});

0 commit comments

Comments
 (0)