web/satellite: paginate ListObjects request to show more than 1000 objects

With this change, we are able to fetch all objects to show in the object browser.
AWS SDK V3 provides paginator functionality to automatically make additional requests for every MaxKeys value (we use 500 objects at a time).
By initial request we fetch first 500 objects and save continuation tokens for the rest of the object batches.
Also, we save currently active (fetched) object range.
If user tries to open a page with objects which are out of currently active range then we look for needed continuation token and fetch needed objects batch.
Added a feature flag for this funtionality.

Issue:
https://github.com/storj/storj/issues/5595

Change-Id: If63e3c2ddaac3ea9f2bc1dc63cb49007f897e3e2
This commit is contained in:
Vitalii 2023-08-31 14:33:39 +03:00 committed by Vitalii Shpital
parent f40baf8629
commit 6e1fd12930
11 changed files with 286 additions and 57 deletions

View File

@ -49,6 +49,7 @@ type FrontendConfig struct {
NewUploadModalEnabled bool `json:"newUploadModalEnabled"`
GalleryViewEnabled bool `json:"galleryViewEnabled"`
NeededTransactionConfirmations int `json:"neededTransactionConfirmations"`
ObjectBrowserPaginationEnabled bool `json:"objectBrowserPaginationEnabled"`
}
// Satellites is a configuration value that contains a list of satellite names and addresses.

View File

@ -107,6 +107,7 @@ type Config struct {
GalleryViewEnabled bool `help:"whether to show new gallery view" default:"true"`
UseVuetifyProject bool `help:"whether to use vuetify POC project" default:"false"`
VuetifyHost string `help:"the subdomain the vuetify POC project should be hosted on" default:""`
ObjectBrowserPaginationEnabled bool `help:"whether to use object browser pagination" default:"false"`
OauthCodeExpiry time.Duration `help:"how long oauth authorization codes are issued for" default:"10m"`
OauthAccessTokenExpiry time.Duration `help:"how long oauth access tokens are issued for" default:"24h"`
@ -727,6 +728,7 @@ func (server *Server) frontendConfigHandler(w http.ResponseWriter, r *http.Reque
NewUploadModalEnabled: server.config.NewUploadModalEnabled,
GalleryViewEnabled: server.config.GalleryViewEnabled,
NeededTransactionConfirmations: server.neededTokenPaymentConfirmations,
ObjectBrowserPaginationEnabled: server.config.ObjectBrowserPaginationEnabled,
}
err := json.NewEncoder(w).Encode(&cfg)

View File

@ -319,6 +319,9 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
# how long oauth refresh tokens are issued for
# console.oauth-refresh-token-expiry: 720h0m0s
# whether to use object browser pagination
# console.object-browser-pagination-enabled: false
# enable open registration
# console.open-registration-enabled: false

View File

@ -99,11 +99,22 @@
/>
<TooManyObjectsBanner
v-if="files.length >= NUMBER_OF_DISPLAYED_OBJECTS && isTooManyObjectsBanner"
v-if="!isPaginationEnabled && files.length >= NUMBER_OF_DISPLAYED_OBJECTS && isTooManyObjectsBanner"
:on-close="closeTooManyObjectsBanner"
/>
<v-table items-label="objects" :total-items-count="files.length" selectable :selected="allFilesSelected" show-select class="file-browser-table" @selectAllClicked="toggleSelectAllFiles">
<v-table
items-label="objects"
selectable
:selected="allFilesSelected"
:limit="isPaginationEnabled ? cursor.limit : 0"
:total-page-count="isPaginationEnabled ? pageCount : 0"
:total-items-count="isPaginationEnabled ? fetchedObjectsCount : files.length"
show-select
class="file-browser-table"
:on-page-change="isPaginationEnabled ? changePageAndLimit : null"
@selectAllClicked="toggleSelectAllFiles"
>
<template #head>
<file-browser-header />
</template>
@ -141,7 +152,8 @@
<v-button
width="60px"
font-size="14px"
label="Cancel" :is-deletion="true"
label="Cancel"
is-deletion
:on-press="() => cancelUpload(file.Key)"
/>
</th>
@ -203,7 +215,7 @@
</template>
<script setup lang="ts">
import { computed, onBeforeMount, ref } from 'vue';
import { computed, onBeforeMount, onBeforeUnmount, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import FileBrowserHeader from './FileBrowserHeader.vue';
@ -216,11 +228,17 @@ import { RouteConfig } from '@/types/router';
import { useNotify } from '@/utils/hooks';
import { Bucket } from '@/types/buckets';
import { MODALS } from '@/utils/constants/appStatePopUps';
import { BrowserObject, useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
import {
BrowserObject,
MAX_KEY_COUNT,
ObjectBrowserCursor,
useObjectBrowserStore,
} from '@/store/modules/objectBrowserStore';
import { useAppStore } from '@/store/modules/appStore';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { useConfigStore } from '@/store/modules/configStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { DEFAULT_PAGE_LIMIT } from '@/types/pagination';
import VButton from '@/components/common/VButton.vue';
import BucketSettingsNav from '@/components/objects/BucketSettingsNav.vue';
@ -259,6 +277,27 @@ const routePath = ref(calculateRoutePath());
const NUMBER_OF_DISPLAYED_OBJECTS = 1000;
/**
* Calculates page count depending on object count and page limit.
*/
const pageCount = computed((): number => {
return Math.ceil(fetchedObjectsCount.value / cursor.value.limit);
});
/**
* Returns fetched object count from store.
*/
const fetchedObjectsCount = computed((): number => {
return obStore.state.totalObjectCount;
});
/**
* Returns table cursor from store.
*/
const cursor = computed((): ObjectBrowserCursor => {
return obStore.state.cursor;
});
/**
* Check if the s3 client has been initialized in the store.
*/
@ -266,6 +305,13 @@ const isInitialized = computed((): boolean => {
return obStore.isInitialized;
});
/**
* Indicates if pagination should be used.
*/
const isPaginationEnabled = computed((): boolean => {
return configStore.state.config.objectBrowserPaginationEnabled;
});
/**
* Indicates if new objects uploading flow should be working.
*/
@ -298,9 +344,7 @@ const currentPath = computed((): string => {
* Return locked files number.
*/
const lockedFilesCount = computed((): number => {
const ownObjectsCount = obStore.state.objectsCount;
return objectsCount.value - ownObjectsCount;
return objectsCount.value - (isPaginationEnabled.value ? fetchedObjectsCount.value : obStore.state.objectsCount);
});
/**
@ -373,7 +417,7 @@ const allFilesSelected = computed((): boolean => {
});
const files = computed((): BrowserObject[] => {
return obStore.sortedFiles;
return isPaginationEnabled.value ? obStore.displayedObjects : obStore.sortedFiles;
});
/**
@ -397,6 +441,30 @@ const bucket = computed((): string => {
return bucketsStore.state.fileComponentBucketName;
});
/**
* Changes table page and limit.
*/
function changePageAndLimit(page: number, limit: number): void {
obStore.setCursor({ limit, page });
const lastObjectOnPage = page * limit;
const activeRange = obStore.state.activeObjectsRange;
if (lastObjectOnPage > activeRange.start && lastObjectOnPage <= activeRange.end) {
return;
}
const tokenKey = Math.ceil(lastObjectOnPage / MAX_KEY_COUNT) * MAX_KEY_COUNT;
const tokenToBeFetched = obStore.state.continuationTokens.get(tokenKey);
if (!tokenToBeFetched) {
obStore.listByToken(routePath.value, 1, tokenToBeFetched);
return;
}
obStore.listByToken(routePath.value, tokenKey, tokenToBeFetched);
}
/**
* Closes multiple passphrase banner.
*/
@ -427,7 +495,12 @@ async function onBack(): Promise<void> {
async function onRouteChange(): Promise<void> {
routePath.value = calculateRoutePath();
obStore.closeDropdown();
await list(routePath.value);
if (isPaginationEnabled.value) {
await obStore.initList(routePath.value);
} else {
await list(routePath.value);
}
}
/**
@ -487,7 +560,6 @@ async function list(path: string): Promise<void> {
} catch (error) {
notify.error(error.message, AnalyticsErrorEventSource.FILE_BROWSER_LIST_CALL);
}
}
/**
@ -585,10 +657,14 @@ onBeforeMount(async () => {
fetchingFilesSpinner.value = true;
try {
await Promise.all([
list(''),
obStore.getObjectCount(),
]);
if (isPaginationEnabled.value) {
await obStore.initList('');
} else {
await Promise.all([
list(''),
obStore.getObjectCount(),
]);
}
} catch (err) {
notify.error(err.message, AnalyticsErrorEventSource.FILE_BROWSER_LIST_CALL);
}
@ -596,6 +672,10 @@ onBeforeMount(async () => {
// remove the spinner after files have been fetched
fetchingFilesSpinner.value = false;
});
onBeforeUnmount(() => {
obStore.setCursor({ limit: DEFAULT_PAGE_LIMIT, page: 1 });
});
</script>
<style scoped lang="scss">

View File

@ -105,15 +105,15 @@ interface PaginationControlItem {
const { withLoading } = useLoading();
const props = withDefaults(defineProps<{
itemsLabel?: string,
totalPageCount?: number;
limit?: number;
totalItemsCount?: number;
simplePagination?: boolean;
onPageChange?: PageChangeCallback | null;
onNextClicked?: (() => Promise<void>) | null;
onPreviousClicked?: (() => Promise<void>) | null;
onPageSizeChanged?: ((size: number) => Promise<void>) | null;
itemsLabel?: string,
totalPageCount?: number;
limit?: number;
totalItemsCount?: number;
simplePagination?: boolean;
onPageChange?: PageChangeCallback | null;
onNextClicked?: (() => Promise<void>) | null;
onPreviousClicked?: (() => Promise<void>) | null;
onPageSizeChanged?: ((size: number) => Promise<void> | void) | null;
}>(), {
itemsLabel: 'items',
totalPageCount: 0,
@ -196,7 +196,7 @@ function sizeChanged(size: number) {
await props.onPageSizeChanged(size);
pageSize.value = size;
}
// if the new size is large enough to cause the page index to be out of range
// if the new size is large enough to cause the page index to be out of range
// we calculate an appropriate new page index.
const maxPage = Math.ceil(Math.ceil(props.totalItemsCount / size));
const page = currentPageNumber.value > maxPage ? maxPage : currentPageNumber.value;
@ -209,8 +209,8 @@ function sizeChanged(size: number) {
});
}
async function goToPage(index: number) {
if (index === currentPageNumber.value) {
async function goToPage(index?: number): Promise<void> {
if (index === undefined || index === currentPageNumber.value) {
return;
}
if (index < 1) {
@ -219,7 +219,7 @@ async function goToPage(index: number) {
index = props.totalPageCount ?? 1;
}
await withLoading(async () => {
if (!props.onPageChange) {
if (!props.onPageChange || index === undefined) {
return;
}
await props.onPageChange(index, pageSize.value);
@ -303,21 +303,21 @@ async function prevPage(): Promise<void> {
color: var(--c-grey-6);
text-align: center;
line-height: 24px;
width: 24px;
height: 24px;
padding: 2px 5px;
margin-left: 2px;
box-sizing: border-box;
border: 1px solid transparent;
cursor: pointer;
&:hover {
border: 1px solid var(--c-grey-3);
border-color: var(--c-grey-3);
border-radius: 5px;
line-height: 22px;
}
&.selected {
background: var(--c-grey-2);
border: 1px solid var(--c-grey-3);
border-color: var(--c-grey-3);
border-radius: 5px;
line-height: 22px;
}

View File

@ -39,7 +39,7 @@ const appStore = useAppStore();
const props = defineProps<{
selected: number | null;
itemCount: number;
itemCount?: number;
simplePagination?: boolean;
}>();
@ -54,7 +54,7 @@ const options = computed((): {label:string, value:number}[] => {
{ label: '50', value: 50 },
{ label: '100', value: 100 },
];
if (props.itemCount < 1000 && !props.simplePagination) {
if (props.itemCount && props.itemCount < 1000 && !props.simplePagination) {
return [{ label: 'All', value: props.itemCount }, ...opts];
}
return opts;

View File

@ -46,7 +46,7 @@ const props = withDefaults(defineProps<{
onPageChange?: PageChangeCallback | null;
onNextClicked?: (() => Promise<void>) | null;
onPreviousClicked?: (() => Promise<void>) | null;
onPageSizeChanged?: ((size: number) => Promise<void>) | null;
onPageSizeChanged?: ((size: number) => Promise<void> | void) | null;
totalPageCount?: number,
selectable?: boolean,
selected?: boolean,

View File

@ -11,7 +11,7 @@
</template>
<script setup lang="ts">
import { computed, onBeforeMount, ref, watch } from 'vue';
import { computed, onBeforeMount, watch } from 'vue';
import { useRouter } from 'vue-router';
import { RouteConfig } from '@/types/router';
@ -24,10 +24,13 @@ import { useAppStore } from '@/store/modules/appStore';
import { useBucketsStore } from '@/store/modules/bucketsStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useObjectBrowserStore } from '@/store/modules/objectBrowserStore';
import { useConfigStore } from '@/store/modules/configStore';
import { DEFAULT_PAGE_LIMIT } from '@/types/pagination';
import FileBrowser from '@/components/browser/FileBrowser.vue';
import UploadCancelPopup from '@/components/objects/UploadCancelPopup.vue';
const configStore = useConfigStore();
const obStore = useObjectBrowserStore();
const bucketsStore = useBucketsStore();
const appStore = useAppStore();
@ -35,6 +38,13 @@ const projectsStore = useProjectsStore();
const router = useRouter();
const notify = useNotify();
/**
* Indicates if pagination should be used.
*/
const isPaginationEnabled = computed((): boolean => {
return configStore.state.config.objectBrowserPaginationEnabled;
});
/**
* Indicates if upload cancel popup is visible.
*/
@ -107,11 +117,22 @@ watch(passphrase, async () => {
});
try {
await Promise.all([
let promises: Promise<void>[] = [
bucketsStore.getBuckets(bucketPage.value.currentPage, projectID),
obStore.list(''),
obStore.getObjectCount(),
]);
];
if (isPaginationEnabled.value) {
promises.push(obStore.initList(''));
} else {
promises = [
...promises,
obStore.list(''),
obStore.getObjectCount(),
];
}
await Promise.all(promises);
obStore.setCursor({ limit: DEFAULT_PAGE_LIMIT, page: 1 });
} catch (error) {
notify.error(error.message, AnalyticsErrorEventSource.UPLOAD_FILE_VIEW);
}

View File

@ -9,6 +9,7 @@ import {
CreateBucketCommand,
DeleteBucketCommand,
ListObjectsV2Command,
ListObjectsV2CommandOutput,
} from '@aws-sdk/client-s3';
import { SignatureV4 } from '@smithy/signature-v4';
@ -147,7 +148,7 @@ export const useBucketsStore = defineStore('buckets', () => {
throw new Error(error.message);
};
await worker.postMessage({
worker.postMessage({
'type': 'SetPermission',
'isDownload': true,
'isUpload': true,
@ -238,17 +239,19 @@ export const useBucketsStore = defineStore('buckets', () => {
}
async function getObjectsCount(name: string): Promise<number> {
const maxKeys = 1000; // Default max keys count.
const abortController = new AbortController();
const request = state.s3Client.send(new ListObjectsV2Command({
Bucket: name,
MaxKeys: maxKeys,
}), { abortSignal: abortController.signal });
const timeout = setTimeout(() => {
abortController.abort();
}, 10000); // abort request in 10 seconds.
let response;
let response: ListObjectsV2CommandOutput;
try {
response = await request;
clearTimeout(timeout);
@ -262,7 +265,9 @@ export const useBucketsStore = defineStore('buckets', () => {
throw error;
}
return (!response || response.KeyCount === undefined) ? 0 : response.KeyCount;
if (!response || response.KeyCount === undefined) return 0;
return response.IsTruncated ? maxKeys : response.KeyCount;
}
function clearS3Data(): void {

View File

@ -13,16 +13,19 @@ import {
PutObjectCommand,
_Object,
GetObjectCommand,
paginateListObjectsV2,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { Progress, Upload } from '@aws-sdk/lib-storage';
import { SignatureV4 } from '@smithy/signature-v4';
import { ListObjectsV2CommandInput } from '@aws-sdk/client-s3/dist-types/commands/ListObjectsV2Command';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { MODALS } from '@/utils/constants/appStatePopUps';
import { useAppStore } from '@/store/modules/appStore';
import { useNotificationsStore } from '@/store/modules/notificationsStore';
import { useConfigStore } from '@/store/modules/configStore';
import { DEFAULT_PAGE_LIMIT } from '@/types/pagination';
const listCache = new Map();
@ -62,6 +65,18 @@ export type PreviewCache = {
lastModified: number,
}
export const MAX_KEY_COUNT = 500;
export type ObjectBrowserCursor = {
page: number,
limit: number,
}
export type ObjectRange = {
start: number,
end: number,
}
export class FilesState {
s3: S3Client | null = null;
accessKey: null | string = null;
@ -69,6 +84,10 @@ export class FilesState {
bucket = '';
browserRoot = '/';
files: BrowserObject[] = [];
cursor: ObjectBrowserCursor = { limit: DEFAULT_PAGE_LIMIT, page: 1 };
continuationTokens: Map<number, string> = new Map<number, string>();
totalObjectCount = 0;
activeObjectsRange: ObjectRange = { start: 1, end: 500 };
uploadChain: Promise<void> = Promise.resolve();
uploading: UploadingBrowserObject[] = [];
selectedAnchorFile: BrowserObject | null = null;
@ -113,6 +132,8 @@ declare global {
export const useObjectBrowserStore = defineStore('objectBrowser', () => {
const state = reactive<FilesState>(new FilesState());
const config = useConfigStore();
const sortedFiles = computed(() => {
// key-specific sort cases
const fns = {
@ -140,13 +161,24 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
];
});
const displayedObjects = computed(() => {
let end = state.cursor.limit * state.cursor.page;
let start = end - state.cursor.limit;
// We check if current active range is not initial and recalculate slice indexes.
if (state.activeObjectsRange.end !== MAX_KEY_COUNT) {
end -= state.activeObjectsRange.start;
start = end - state.cursor.limit;
}
return sortedFiles.value.slice(start, end);
});
const isInitialized = computed(() => {
return state.s3 !== null;
});
const uploadingLength = computed(() => {
const config = useConfigStore();
if (config.state.config.newUploadModalEnabled) {
return state.uploading.filter(f => f.status === UploadingStatus.InProgress).length;
}
@ -154,6 +186,10 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
return state.uploading.length;
});
function setCursor(cursor: ObjectBrowserCursor): void {
state.cursor = cursor;
}
function init({
accessKey,
secretKey,
@ -232,8 +268,66 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
Prefix: path,
}));
let { Contents, CommonPrefixes } = response;
const { Contents, CommonPrefixes } = response;
processFetchedObjects(path, Contents, CommonPrefixes);
}
async function initList(path = state.path): Promise<void> {
assertIsInitialized(state);
const input: ListObjectsV2CommandInput = {
Bucket: state.bucket,
Delimiter: '/',
Prefix: path,
};
const paginator = paginateListObjectsV2({ client: state.s3, pageSize: MAX_KEY_COUNT }, input);
let iteration = 1;
let keyCount = 0;
for await (const response of paginator) {
if (iteration === 1) {
const { Contents, CommonPrefixes } = response;
processFetchedObjects(path, Contents, CommonPrefixes);
}
keyCount += response.KeyCount ?? 0;
if (!response.NextContinuationToken) break;
state.continuationTokens.set(MAX_KEY_COUNT * (iteration + 1), response.NextContinuationToken);
iteration++;
}
state.totalObjectCount = keyCount;
}
async function listByToken(path: string, key: number, continuationToken?: string): Promise<void> {
assertIsInitialized(state);
const input: ListObjectsV2CommandInput = {
Bucket: state.bucket,
Delimiter: '/',
Prefix: path,
ContinuationToken: continuationToken,
};
const response = await state.s3.send(new ListObjectsV2Command(input));
const { Contents, CommonPrefixes } = response;
processFetchedObjects(path, Contents, CommonPrefixes);
state.activeObjectsRange = {
start: key === 1 ? key : key - MAX_KEY_COUNT,
end: key === 1 ? MAX_KEY_COUNT : key,
};
}
function processFetchedObjects(path: string, Contents: _Object[] | undefined, CommonPrefixes: CommonPrefix[] | undefined): void {
if (Contents === undefined) {
Contents = [];
}
@ -294,7 +388,7 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
}
async function back(): Promise<void> {
const getParentDirectory = (path) => {
const getParentDirectory = (path: string) => {
let i = path.length - 2;
while (path[i - 1] !== '/' && i > 0) {
@ -420,7 +514,6 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
assertIsInitialized(state);
const appStore = useAppStore();
const config = useConfigStore();
const { notifyError } = useNotificationsStore();
const params = {
@ -517,7 +610,11 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
upload.off('httpUploadProgress', progressListener);
}
await list();
if (config.state.config.objectBrowserPaginationEnabled) {
await initList();
} else {
await list();
}
const uploadedFiles = state.files.filter(f => f.type === 'file');
if (uploadedFiles.length === 1 && !key.includes('/') && state.openModalOnFirstUpload) {
@ -539,7 +636,6 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
function handleUploadError(item: UploadingBrowserObject, error: Error): void {
if (error.name === 'AbortError' && item.status === UploadingStatus.Cancelled) return;
const config = useConfigStore();
if (config.state.config.newUploadModalEnabled) {
item.status = UploadingStatus.Failed;
item.failedMessage = FailedUploadMessage.Failed;
@ -561,7 +657,7 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
}
}
async function createFolder(name): Promise<void> {
async function createFolder(name: string): Promise<void> {
assertIsInitialized(state);
await state.s3.send(new PutObjectCommand({
@ -570,7 +666,11 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
Body: '',
}));
list();
if (config.state.config.objectBrowserPaginationEnabled) {
initList();
} else {
list();
}
}
async function deleteObject(path: string, file?: _Object | BrowserObject, isFolder = false): Promise<void> {
@ -585,13 +685,17 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
Key: path + file.Key,
}));
const config = useConfigStore();
if (config.state.config.newUploadModalEnabled) {
state.uploading = state.uploading.filter(f => f.Key !== file.Key);
}
if (!isFolder) {
await list();
if (config.state.config.objectBrowserPaginationEnabled) {
await initList();
} else {
await list();
}
removeFileFromToBeDeleted(file);
}
}
@ -599,7 +703,7 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
async function deleteFolder(file: BrowserObject, path: string): Promise<void> {
assertIsInitialized(state);
async function recurse(filePath) {
async function recurse(filePath: string) {
assertIsInitialized(state);
let { Contents, CommonPrefixes } = await state.s3.send(new ListObjectsCommand({
@ -631,14 +735,18 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
await Promise.all([thread(), thread(), thread()]);
for (const { Prefix } of CommonPrefixes) {
await recurse(Prefix);
await recurse(Prefix ?? '');
}
}
await recurse(path.length > 0 ? path + file.Key : file.Key + '/');
removeFileFromToBeDeleted(file);
await list();
if (config.state.config.objectBrowserPaginationEnabled) {
await initList();
} else {
await list();
}
}
async function deleteSelected(): Promise<void> {
@ -770,6 +878,10 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
state.bucket = '';
state.browserRoot = '/';
state.files = [];
state.cursor = { limit: DEFAULT_PAGE_LIMIT, page: 1 };
state.continuationTokens = new Map<number, string>();
state.totalObjectCount = 0;
state.activeObjectsRange = { start: 1, end: 500 };
state.uploadChain = Promise.resolve();
state.uploading = [];
state.selectedAnchorFile = null;
@ -788,12 +900,16 @@ export const useObjectBrowserStore = defineStore('objectBrowser', () => {
return {
state,
sortedFiles,
displayedObjects,
isInitialized,
uploadingLength,
init,
reinit,
list,
initList,
listByToken,
back,
setCursor,
sort,
getObjectCount,
upload,

View File

@ -46,6 +46,7 @@ export class FrontendConfig {
newUploadModalEnabled: boolean;
galleryViewEnabled: boolean;
neededTransactionConfirmations: number;
objectBrowserPaginationEnabled: boolean;
}
export class MultiCaptchaConfig {