Skip to content

Commit

Permalink
Merge pull request #1634 from SanjalKatiyar/search_objects_prefix
Browse files Browse the repository at this point in the history
Search objects by prefix
  • Loading branch information
openshift-merge-bot[bot] authored Oct 14, 2024
2 parents 2ba9581 + 08ee6ed commit 6f3bea9
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 10 deletions.
1 change: 1 addition & 0 deletions locales/en/plugin__odf-console.json
Original file line number Diff line number Diff line change
Expand Up @@ -1139,6 +1139,7 @@
"Value (optional)": "Value (optional)",
"Delete objects": "Delete objects",
"Actions": "Actions",
"Search objects in the bucket using prefix": "Search objects in the bucket using prefix",
"Create folder": "Create folder",
"Failed to delete {{ errorCount }} object from the bucket. View deletion summary for details.": "Failed to delete {{ errorCount }} object from the bucket. View deletion summary for details.",
"Failed to delete {{ errorCount }} objects from the bucket. View deletion summary for details.": "Failed to delete {{ errorCount }} objects from the bucket. View deletion summary for details.",
Expand Down
105 changes: 96 additions & 9 deletions packages/odf/components/s3-browser/objects-list/ObjectsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
import { useModal } from '@openshift-console/dynamic-plugin-sdk';
import { LaunchModal } from '@openshift-console/dynamic-plugin-sdk/lib/app/modal-support/ModalProvider';
import { TFunction } from 'i18next';
import { useParams, useSearchParams } from 'react-router-dom-v5-compat';
import {
useParams,
useSearchParams,
useNavigate,
} from 'react-router-dom-v5-compat';
import useSWRMutation from 'swr/mutation';
import {
Button,
Expand All @@ -24,13 +28,20 @@ import {
AlertVariant,
AlertActionCloseButton,
AlertActionLink,
SearchInput,
} from '@patternfly/react-core';
import {
ActionsColumn,
IAction,
CustomActionsToggleProps,
} from '@patternfly/react-table';
import { LIST_OBJECTS, DELIMITER, MAX_KEYS, PREFIX } from '../../../constants';
import {
LIST_OBJECTS,
DELIMITER,
MAX_KEYS,
PREFIX,
SEARCH,
} from '../../../constants';
import {
ObjectsDeleteResponse,
SetObjectsDeleteResponse,
Expand All @@ -40,7 +51,12 @@ import {
LazyDeleteObjectsSummary,
} from '../../../modals/s3-browser/delete-objects/LazyDeleteModals';
import { ObjectCrFormat } from '../../../types';
import { getPath, convertObjectsDataToCrFormat } from '../../../utils';
import {
getPath,
getPrefix as getSearchWithPrefix,
convertObjectsDataToCrFormat,
getNavigationURL,
} from '../../../utils';
import { NoobaaS3Context } from '../noobaa-context';
import {
Pagination,
Expand All @@ -60,6 +76,14 @@ const LazyCreateFolderModal = React.lazy(
() => import('../../../modals/s3-browser/create-folder/CreateFolderModal')
);

type SearchObjectsProps = {
foldersPath: string;
bucketName: string;
searchInput: string;
setSearchInput: React.Dispatch<React.SetStateAction<string>>;
className: string;
};

type TableActionsProps = {
launcher: LaunchModal;
selectedRows: ObjectCrFormat[];
Expand All @@ -69,6 +93,8 @@ type TableActionsProps = {
noobaaS3: S3Commands;
setDeleteResponse: SetObjectsDeleteResponse;
refreshTokens: () => Promise<void>;
searchInput: string;
setSearchInput: React.Dispatch<React.SetStateAction<string>>;
};

type DeletionAlertsProps = {
Expand Down Expand Up @@ -117,6 +143,38 @@ export const CustomActionsToggle = (props: CustomActionsToggleProps) => {
);
};

const SearchObjects: React.FC<SearchObjectsProps> = ({
foldersPath,
bucketName,
searchInput,
setSearchInput,
className,
}) => {
const { t } = useCustomTranslation();

const navigate = useNavigate();
const onClearURL = getNavigationURL(bucketName, foldersPath, '');

return (
<SearchInput
placeholder={t('Search objects in the bucket using prefix')}
value={searchInput}
onChange={(_event, value) => {
setSearchInput(value);
if (value === '') navigate(onClearURL);
}}
onSearch={(_event, value) =>
!!value && navigate(getNavigationURL(bucketName, foldersPath, value))
}
onClear={() => {
setSearchInput('');
navigate(onClearURL);
}}
className={className}
/>
);
};

const TableActions: React.FC<PaginationProps & TableActionsProps> = ({
onNext,
onPrevious,
Expand All @@ -130,15 +188,24 @@ const TableActions: React.FC<PaginationProps & TableActionsProps> = ({
noobaaS3,
setDeleteResponse,
refreshTokens,
searchInput,
setSearchInput,
}) => {
const { t } = useCustomTranslation();

const anySelection = !!selectedRows.length;

return (
<Level hasGutter>
<LevelItem>
<LevelItem className="pf-v5-u-w-50">
<div className="pf-v5-u-display-flex pf-v5-u-flex-direction-row">
<SearchObjects
foldersPath={foldersPath}
bucketName={bucketName}
searchInput={searchInput}
setSearchInput={setSearchInput}
className="pf-v5-u-mr-sm"
/>
<Button
variant={ButtonVariant.secondary}
className="pf-v5-u-mr-sm"
Expand Down Expand Up @@ -279,36 +346,47 @@ export const ObjectsList: React.FC<{}> = () => {
const launcher = useModal();

// if non-empty means we are inside particular folder(s) of a bucket, else just inside a bucket (top-level)
const foldersPath = searchParams.get(PREFIX);
const foldersPath = searchParams.get(PREFIX) || '';
// search objects within a bucket
const searchQuery = searchParams.get(SEARCH) || '';

const { noobaaS3 } = React.useContext(NoobaaS3Context);
const cacheKey = LIST_OBJECTS + DELIMITER + getPath(bucketName, foldersPath);
const searchWithPrefix = getSearchWithPrefix(searchQuery, foldersPath);
const cacheKey =
LIST_OBJECTS + DELIMITER + getPath(bucketName, searchWithPrefix);
const { data, error, isMutating, trigger } = useSWRMutation(
cacheKey,
(_url, { arg }: { arg: string }) =>
noobaaS3.listObjects({
Bucket: bucketName,
MaxKeys: MAX_KEYS,
Delimiter: DELIMITER,
...(!!foldersPath && { Prefix: foldersPath }),
...(!!searchWithPrefix && { Prefix: searchWithPrefix }),
...(!!arg && { ContinuationToken: arg }),
})
);

const loadedWOError = !isMutating && !error;

// used for pagination
const [continuationTokens, setContinuationTokens] =
React.useState<ContinuationTokens>({
previous: [],
current: '',
next: '',
});
// used for multi-select bulk operations
const [selectedRows, setSelectedRows] = React.useState<ObjectCrFormat[]>([]);
// used for storing API's response on performing delete operation on objects
const [deleteResponse, setDeleteResponse] =
React.useState<ObjectsDeleteResponse>({
selectedObjects: [] as ObjectCrFormat[],
deleteResponse: {} as DeleteObjectsCommandOutput,
});
// used for storing input to the objects' search bar
const [searchInput, setSearchInput] = React.useState(searchQuery);
// used to store previous value of "foldersPath";
const foldersPathPrevRef = React.useRef<string>();

const structuredObjects: ObjectCrFormat[] = React.useMemo(() => {
const objects: ObjectCrFormat[] = [];
Expand All @@ -334,11 +412,18 @@ export const ObjectsList: React.FC<{}> = () => {
setSelectedRows
);

// initial fetch on first mount or on route update (drilling in/out of the folder view)
// initial fetch on first mount or on route update (drilling in/out of the folder view or searching objects using prefix)
React.useEffect(() => {
refreshTokens();
if (foldersPathPrevRef.current !== foldersPath) {
// only reset filter if navigated in/out of the current folder
// not when search query is changed,
// also, not when URL contains search query param
if (!searchQuery) setSearchInput('');
foldersPathPrevRef.current = foldersPath;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [foldersPath]);
}, [foldersPath, searchQuery]);

return (
<div className="pf-v5-u-m-lg">
Expand Down Expand Up @@ -386,6 +471,8 @@ export const ObjectsList: React.FC<{}> = () => {
noobaaS3={noobaaS3}
setDeleteResponse={setDeleteResponse}
refreshTokens={refreshTokens}
searchInput={searchInput}
setSearchInput={setSearchInput}
/>
<SelectableTable
className="pf-v5-u-mt-lg"
Expand Down
1 change: 1 addition & 0 deletions packages/odf/constants/s3-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const NOOBAA_SECRET_ACCESS_KEY = 'AWS_SECRET_ACCESS_KEY';

export const DELIMITER = '/';
export const PREFIX = 'prefix';
export const SEARCH = 'search';
export const MAX_KEYS = 300;
export const MAX_BUCKETS = 100;

Expand Down
20 changes: 19 additions & 1 deletion packages/odf/utils/s3-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { DASH } from '@odf/shared/constants';
import { getName } from '@odf/shared/selectors';
import { humanizeBinaryBytes } from '@odf/shared/utils';
import { TFunction } from 'i18next';
import { DELIMITER, BUCKETS_BASE_ROUTE, PREFIX } from '../constants';
import { DELIMITER, BUCKETS_BASE_ROUTE, PREFIX, SEARCH } from '../constants';
import { BucketCrFormat, ObjectCrFormat } from '../types';

export const getBreadcrumbs = (
Expand Down Expand Up @@ -118,3 +118,21 @@ export const convertBucketDataToCrFormat = (
owner: listBucketsCommandOutput?.Owner?.DisplayName,
},
})) || [];

export const getNavigationURL = (
bucketName: string,
foldersPath: string,
inputValue: string
): string => {
const queryParams = [
...(!!foldersPath
? [`${PREFIX}=${getEncodedPrefix('', foldersPath)}`]
: []),
...(!!inputValue ? [`${SEARCH}=${getEncodedPrefix(inputValue, '')}`] : []),
];
const queryParamsString = !!queryParams.length
? '?' + queryParams.join('&')
: '';

return `${BUCKETS_BASE_ROUTE}/${bucketName}` + queryParamsString;
};

0 comments on commit 6f3bea9

Please sign in to comment.