mirror of
https://github.com/khairul169/garage-webui.git
synced 2026-09-18 10:23:21 +07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
611258d0db | ||
|
|
91c396dd68 | ||
|
|
5a90dd8377 | ||
|
|
145bf3f1a9 | ||
|
|
7532c6330c | ||
|
|
4861e1bbb1 |
@@ -31,7 +31,7 @@ If you install Garage using Docker, you can install this web UI alongside Garage
|
|||||||
```yml
|
```yml
|
||||||
services:
|
services:
|
||||||
garage:
|
garage:
|
||||||
image: dxflrs/garage:v1.0.0
|
image: dxflrs/garage:v1.0.1
|
||||||
container_name: garage
|
container_name: garage
|
||||||
volumes:
|
volumes:
|
||||||
- ./garage.toml:/etc/garage.toml
|
- ./garage.toml:/etc/garage.toml
|
||||||
@@ -55,7 +55,7 @@ services:
|
|||||||
Get the latest binary from the [release page](https://github.com/khairul169/garage-webui/releases/latest) according to your OS architecture. For example:
|
Get the latest binary from the [release page](https://github.com/khairul169/garage-webui/releases/latest) according to your OS architecture. For example:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ wget -O garage-webui https://github.com/khairul169/garage-webui/releases/download/1.0.1/garage-webui-v1.0.1-linux-amd64
|
$ wget -O garage-webui https://github.com/khairul169/garage-webui/releases/download/1.0.4/garage-webui-v1.0.4-linux-amd64
|
||||||
$ chmod +x garage-webui
|
$ chmod +x garage-webui
|
||||||
$ sudo cp garage-webui /usr/local/bin
|
$ sudo cp garage-webui /usr/local/bin
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||||
"github.com/aws/smithy-go"
|
"github.com/aws/smithy-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -183,6 +184,8 @@ func (b *Browse) PutObject(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) {
|
func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) {
|
||||||
bucket := r.PathValue("bucket")
|
bucket := r.PathValue("bucket")
|
||||||
key := r.PathValue("key")
|
key := r.PathValue("key")
|
||||||
|
recursive := r.URL.Query().Get("recursive") == "true"
|
||||||
|
isDirectory := strings.HasSuffix(key, "/")
|
||||||
|
|
||||||
client, err := getS3Client(bucket)
|
client, err := getS3Client(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -190,7 +193,52 @@ func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = client.DeleteObject(context.Background(), &s3.DeleteObjectInput{
|
// Delete directory and its content
|
||||||
|
if isDirectory && recursive {
|
||||||
|
objects, err := client.ListObjectsV2(context.Background(), &s3.ListObjectsV2Input{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
Prefix: aws.String(key),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
utils.ResponseError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(objects.Contents) == 0 {
|
||||||
|
utils.ResponseSuccess(w, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make([]types.ObjectIdentifier, 0, len(objects.Contents))
|
||||||
|
|
||||||
|
for _, object := range objects.Contents {
|
||||||
|
keys = append(keys, types.ObjectIdentifier{
|
||||||
|
Key: object.Key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := client.DeleteObjects(context.Background(), &s3.DeleteObjectsInput{
|
||||||
|
Bucket: aws.String(bucket),
|
||||||
|
Delete: &types.Delete{Objects: keys},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
utils.ResponseError(w, fmt.Errorf("cannot delete object: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res.Errors) > 0 {
|
||||||
|
utils.ResponseError(w, fmt.Errorf("cannot delete object: %v", res.Errors[0]))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ResponseSuccess(w, res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete single object
|
||||||
|
res, err := client.DeleteObject(context.Background(), &s3.DeleteObjectInput{
|
||||||
Bucket: aws.String(bucket),
|
Bucket: aws.String(bucket),
|
||||||
Key: aws.String(key),
|
Key: aws.String(key),
|
||||||
})
|
})
|
||||||
@@ -200,7 +248,7 @@ func (b *Browse) DeleteObject(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.ResponseSuccess(w, nil)
|
utils.ResponseSuccess(w, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) {
|
func getBucketCredentials(bucket string) (aws.CredentialsProvider, error) {
|
||||||
|
|||||||
@@ -34,13 +34,14 @@ func (b *Buckets) GetAll(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var bucket schema.Bucket
|
var data schema.Bucket
|
||||||
if err := json.Unmarshal(body, &bucket); err != nil {
|
if err := json.Unmarshal(body, &data); err != nil {
|
||||||
ch <- schema.Bucket{ID: bucket.ID, GlobalAliases: bucket.GlobalAliases}
|
ch <- schema.Bucket{ID: bucket.ID, GlobalAliases: bucket.GlobalAliases}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ch <- bucket
|
data.LocalAliases = bucket.LocalAliases
|
||||||
|
ch <- data
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
package schema
|
package schema
|
||||||
|
|
||||||
type GetBucketsRes struct {
|
type GetBucketsRes struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
GlobalAliases []string `json:"globalAliases"`
|
GlobalAliases []string `json:"globalAliases"`
|
||||||
LocalAliases []string `json:"localAliases"`
|
LocalAliases []LocalAlias `json:"localAliases"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Bucket struct {
|
type Bucket struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
GlobalAliases []string `json:"globalAliases"`
|
GlobalAliases []string `json:"globalAliases"`
|
||||||
|
LocalAliases []LocalAlias `json:"localAliases"`
|
||||||
WebsiteAccess bool `json:"websiteAccess"`
|
WebsiteAccess bool `json:"websiteAccess"`
|
||||||
WebsiteConfig WebsiteConfig `json:"websiteConfig"`
|
WebsiteConfig WebsiteConfig `json:"websiteConfig"`
|
||||||
Keys []KeyElement `json:"keys"`
|
Keys []KeyElement `json:"keys"`
|
||||||
@@ -21,12 +22,17 @@ type Bucket struct {
|
|||||||
Quotas Quotas `json:"quotas"`
|
Quotas Quotas `json:"quotas"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LocalAlias struct {
|
||||||
|
AccessKeyID string `json:"accessKeyId"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
}
|
||||||
|
|
||||||
type KeyElement struct {
|
type KeyElement struct {
|
||||||
AccessKeyID string `json:"accessKeyId"`
|
AccessKeyID string `json:"accessKeyId"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Permissions Permissions `json:"permissions"`
|
Permissions Permissions `json:"permissions"`
|
||||||
BucketLocalAliases []interface{} `json:"bucketLocalAliases"`
|
BucketLocalAliases []string `json:"bucketLocalAliases"`
|
||||||
SecretAccessKey string `json:"secretAccessKey"`
|
SecretAccessKey string `json:"secretAccessKey"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Permissions struct {
|
type Permissions struct {
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
package schema
|
package schema
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
CompressionLevel int64 `json:"compression_level" toml:"compression_level"`
|
RPCBindAddr string `json:"rpc_bind_addr" toml:"rpc_bind_addr"`
|
||||||
DataDir string `json:"data_dir" toml:"data_dir"`
|
RPCPublicAddr string `json:"rpc_public_addr" toml:"rpc_public_addr"`
|
||||||
DBEngine string `json:"db_engine" toml:"db_engine"`
|
RPCSecret string `json:"rpc_secret" toml:"rpc_secret"`
|
||||||
MetadataAutoSnapshotInterval string `json:"metadata_auto_snapshot_interval" toml:"metadata_auto_snapshot_interval"`
|
Admin Admin `json:"admin" toml:"admin"`
|
||||||
MetadataDir string `json:"metadata_dir" toml:"metadata_dir"`
|
S3API S3API `json:"s3_api" toml:"s3_api"`
|
||||||
ReplicationFactor int64 `json:"replication_factor" toml:"replication_factor"`
|
S3Web S3Web `json:"s3_web" toml:"s3_web"`
|
||||||
RPCBindAddr string `json:"rpc_bind_addr" toml:"rpc_bind_addr"`
|
|
||||||
RPCPublicAddr string `json:"rpc_public_addr" toml:"rpc_public_addr"`
|
|
||||||
RPCSecret string `json:"rpc_secret" toml:"rpc_secret"`
|
|
||||||
Admin Admin `json:"admin" toml:"admin"`
|
|
||||||
S3API S3API `json:"s3_api" toml:"s3_api"`
|
|
||||||
S3Web S3Web `json:"s3_web" toml:"s3_web"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Admin struct {
|
type Admin struct {
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "garage-webui",
|
"name": "garage-webui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.2",
|
"version": "1.0.4",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:client": "vite",
|
"dev:client": "vite",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const MainLayout = () => {
|
|||||||
<Drawer
|
<Drawer
|
||||||
open={sidebar.isOpen}
|
open={sidebar.isOpen}
|
||||||
onClickOverlay={sidebar.onClose}
|
onClickOverlay={sidebar.onClose}
|
||||||
className="md:drawer-open h-screen"
|
className="md:drawer-open h-screen max-h-dvh"
|
||||||
side={<Sidebar />}
|
side={<Sidebar />}
|
||||||
contentClassName="flex flex-col overflow-hidden"
|
contentClassName="flex flex-col overflow-hidden"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { readableBytes } from "@/lib/utils";
|
|||||||
import Button from "@/components/ui/button";
|
import Button from "@/components/ui/button";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: Bucket;
|
data: Bucket & { aliases: string[] };
|
||||||
};
|
};
|
||||||
|
|
||||||
const BucketCard = ({ data }: Props) => {
|
const BucketCard = ({ data }: Props) => {
|
||||||
@@ -15,7 +15,7 @@ const BucketCard = ({ data }: Props) => {
|
|||||||
<ArchiveIcon size={28} className="shrink-0" />
|
<ArchiveIcon size={28} className="shrink-0" />
|
||||||
|
|
||||||
<p className="text-xl font-medium truncate">
|
<p className="text-xl font-medium truncate">
|
||||||
{data.globalAliases?.join(", ")}
|
{data.aliases?.join(", ")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -40,10 +40,13 @@ export const usePutObject = (
|
|||||||
|
|
||||||
export const useDeleteObject = (
|
export const useDeleteObject = (
|
||||||
bucket: string,
|
bucket: string,
|
||||||
options?: UseMutationOptions<any, Error, string>
|
options?: UseMutationOptions<any, Error, { key: string; recursive?: boolean }>
|
||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (key) => api.delete(`/browse/${bucket}/${key}`),
|
mutationFn: (data) =>
|
||||||
|
api.delete(`/browse/${bucket}/${data.key}`, {
|
||||||
|
params: { recursive: data.recursive },
|
||||||
|
}),
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ import { shareDialog } from "./share-dialog";
|
|||||||
type Props = {
|
type Props = {
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
object: Pick<Object, "objectKey" | "downloadUrl">;
|
object: Pick<Object, "objectKey" | "downloadUrl">;
|
||||||
|
end?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ObjectActions = ({ prefix = "", object }: Props) => {
|
const ObjectActions = ({ prefix = "", object, end }: Props) => {
|
||||||
const { bucketName } = useBucketContext();
|
const { bucketName } = useBucketContext();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const isDirectory = object.objectKey.endsWith("/");
|
const isDirectory = object.objectKey.endsWith("/");
|
||||||
@@ -33,8 +34,17 @@ const ObjectActions = ({ prefix = "", object }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onDelete = () => {
|
const onDelete = () => {
|
||||||
if (window.confirm("Are you sure you want to delete this object?")) {
|
if (
|
||||||
deleteObject.mutate(prefix + object.objectKey);
|
window.confirm(
|
||||||
|
`Are you sure you want to delete this ${
|
||||||
|
isDirectory ? "directory and its content" : "object"
|
||||||
|
}?`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
deleteObject.mutate({
|
||||||
|
key: prefix + object.objectKey,
|
||||||
|
recursive: isDirectory,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,12 +55,12 @@ const ObjectActions = ({ prefix = "", object }: Props) => {
|
|||||||
<Button icon={DownloadIcon} color="ghost" onClick={onDownload} />
|
<Button icon={DownloadIcon} color="ghost" onClick={onDownload} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Dropdown end>
|
<Dropdown end vertical={end ? "top" : "bottom"}>
|
||||||
<Dropdown.Toggle button={false}>
|
<Dropdown.Toggle button={false}>
|
||||||
<Button icon={EllipsisVertical} color="ghost" />
|
<Button icon={EllipsisVertical} color="ghost" />
|
||||||
</Dropdown.Toggle>
|
</Dropdown.Toggle>
|
||||||
|
|
||||||
<Dropdown.Menu className="bg-base-300 gap-y-1">
|
<Dropdown.Menu className="gap-y-1">
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
shareDialog.open({ key: object.objectKey, prefix })
|
shareDialog.open({ key: object.objectKey, prefix })
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const ObjectList = ({ prefix, onPrefixChange }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto pb-32">
|
<div className="overflow-x-auto overflow-y-hidden">
|
||||||
<Table>
|
<Table>
|
||||||
<Table.Head>
|
<Table.Head>
|
||||||
<span>Name</span>
|
<span>Name</span>
|
||||||
@@ -63,7 +63,7 @@ const ObjectList = ({ prefix, onPrefixChange }: Props) => {
|
|||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{data?.objects.map((object) => {
|
{data?.objects.map((object, idx) => {
|
||||||
const extIdx = object.objectKey.lastIndexOf(".");
|
const extIdx = object.objectKey.lastIndexOf(".");
|
||||||
const filename =
|
const filename =
|
||||||
extIdx >= 0
|
extIdx >= 0
|
||||||
@@ -93,7 +93,11 @@ const ObjectList = ({ prefix, onPrefixChange }: Props) => {
|
|||||||
<td className="whitespace-nowrap">
|
<td className="whitespace-nowrap">
|
||||||
{dayjs(object.lastModified).fromNow()}
|
{dayjs(object.lastModified).fromNow()}
|
||||||
</td>
|
</td>
|
||||||
<ObjectActions prefix={data.prefix} object={object} />
|
<ObjectActions
|
||||||
|
prefix={data.prefix}
|
||||||
|
object={object}
|
||||||
|
end={idx >= data.objects.length - 2}
|
||||||
|
/>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const AllowKeyDialog = ({ currentKeys }: Props) => {
|
|||||||
Allow Key
|
Allow Key
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Modal ref={dialogRef} backdrop open={isOpen}>
|
<Modal ref={dialogRef} backdrop open={isOpen} className="max-w-2xl">
|
||||||
<Modal.Header className="mb-1">Allow Key</Modal.Header>
|
<Modal.Header className="mb-1">Allow Key</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<p>Enter the key you want to allow access to.</p>
|
<p>Enter the key you want to allow access to.</p>
|
||||||
@@ -100,6 +100,7 @@ const AllowKeyDialog = ({ currentKeys }: Props) => {
|
|||||||
/>
|
/>
|
||||||
Key
|
Key
|
||||||
</label>
|
</label>
|
||||||
|
<label>Local Aliases</label>
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -129,23 +130,31 @@ const AllowKeyDialog = ({ currentKeys }: Props) => {
|
|||||||
<Table.Body>
|
<Table.Body>
|
||||||
{!keyFields.length ? (
|
{!keyFields.length ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={4} className="text-center">
|
<td colSpan={5} className="text-center">
|
||||||
No keys found
|
No keys found
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : null}
|
) : null}
|
||||||
{keyFields.map((field, index) => (
|
{keyFields.map((field, index) => {
|
||||||
<Table.Row key={field.id}>
|
const curKey = bucket.keys.find(
|
||||||
<CheckboxField
|
(key) => key.accessKeyId === field.keyId
|
||||||
form={form}
|
);
|
||||||
name={`keys.${index}.checked`}
|
return (
|
||||||
label={field.name || field.keyId?.substring(0, 8)}
|
<Table.Row key={field.id}>
|
||||||
/>
|
<CheckboxField
|
||||||
<CheckboxField form={form} name={`keys.${index}.read`} />
|
form={form}
|
||||||
<CheckboxField form={form} name={`keys.${index}.write`} />
|
name={`keys.${index}.checked`}
|
||||||
<CheckboxField form={form} name={`keys.${index}.owner`} />
|
label={field.name || field.keyId?.substring(0, 8)}
|
||||||
</Table.Row>
|
/>
|
||||||
))}
|
<span>
|
||||||
|
{curKey?.bucketLocalAliases?.join(", ") || "-"}
|
||||||
|
</span>
|
||||||
|
<CheckboxField form={form} name={`keys.${index}.read`} />
|
||||||
|
<CheckboxField form={form} name={`keys.${index}.write`} />
|
||||||
|
<CheckboxField form={form} name={`keys.${index}.owner`} />
|
||||||
|
</Table.Row>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ const PermissionsTab = () => {
|
|||||||
<Table.Head>
|
<Table.Head>
|
||||||
<span>#</span>
|
<span>#</span>
|
||||||
<span>Key</span>
|
<span>Key</span>
|
||||||
|
<span>Aliases</span>
|
||||||
<span>Read</span>
|
<span>Read</span>
|
||||||
<span>Write</span>
|
<span>Write</span>
|
||||||
<span>Owner</span>
|
<span>Owner</span>
|
||||||
@@ -61,6 +62,7 @@ const PermissionsTab = () => {
|
|||||||
<Table.Row>
|
<Table.Row>
|
||||||
<span>{idx + 1}</span>
|
<span>{idx + 1}</span>
|
||||||
<span>{key.name || key.accessKeyId?.substring(0, 8)}</span>
|
<span>{key.name || key.accessKeyId?.substring(0, 8)}</span>
|
||||||
|
<span>{key.bucketLocalAliases?.join(", ") || "-"}</span>
|
||||||
<span>
|
<span>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={key.permissions?.read}
|
checked={key.permissions?.read}
|
||||||
|
|||||||
@@ -10,20 +10,27 @@ const BucketsPage = () => {
|
|||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
let buckets = data || [];
|
let buckets =
|
||||||
|
data?.map((bucket) => {
|
||||||
|
return {
|
||||||
|
...bucket,
|
||||||
|
aliases: [
|
||||||
|
...(bucket.globalAliases || []),
|
||||||
|
...(bucket.localAliases?.map((l) => l.alias) || []),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
|
||||||
if (search?.length > 0) {
|
if (search?.length > 0) {
|
||||||
const q = search.toLowerCase();
|
const q = search.toLowerCase();
|
||||||
buckets = buckets.filter(
|
buckets = buckets.filter(
|
||||||
(bucket) =>
|
(bucket) =>
|
||||||
bucket.id.includes(q) ||
|
bucket.id.includes(q) ||
|
||||||
bucket.globalAliases.find((alias) => alias.includes(q))
|
bucket.aliases.find((alias) => alias.includes(q))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
buckets = buckets.sort((a, b) =>
|
buckets = buckets.sort((a, b) => a.aliases[0].localeCompare(b.aliases[0]));
|
||||||
a.globalAliases[0].localeCompare(b.globalAliases[0])
|
|
||||||
);
|
|
||||||
|
|
||||||
return buckets;
|
return buckets;
|
||||||
}, [data, search]);
|
}, [data, search]);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export type GetBucketRes = Bucket[];
|
|||||||
export type Bucket = {
|
export type Bucket = {
|
||||||
id: string;
|
id: string;
|
||||||
globalAliases: string[];
|
globalAliases: string[];
|
||||||
|
localAliases: LocalAlias[];
|
||||||
websiteAccess: boolean;
|
websiteAccess: boolean;
|
||||||
websiteConfig?: WebsiteConfig | null;
|
websiteConfig?: WebsiteConfig | null;
|
||||||
keys: Key[];
|
keys: Key[];
|
||||||
@@ -17,11 +18,16 @@ export type Bucket = {
|
|||||||
quotas: Quotas;
|
quotas: Quotas;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LocalAlias = {
|
||||||
|
accessKeyId: string;
|
||||||
|
alias: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type Key = {
|
export type Key = {
|
||||||
accessKeyId: string;
|
accessKeyId: string;
|
||||||
name: string;
|
name: string;
|
||||||
permissions: Permissions;
|
permissions: Permissions;
|
||||||
bucketLocalAliases: any[];
|
bucketLocalAliases: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Permissions = {
|
export type Permissions = {
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ const NodesList = ({ nodes }: NodeListProps) => {
|
|||||||
</Alert>
|
</Alert>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="w-full overflow-x-auto min-h-[400px] pb-16">
|
<div className="w-full overflow-x-auto overflow-y-hidden min-h-[400px]">
|
||||||
<Table size="sm" className="min-w-[800px]">
|
<Table size="sm" className="min-w-[800px]">
|
||||||
<Table.Head>
|
<Table.Head>
|
||||||
<span>#</span>
|
<span>#</span>
|
||||||
@@ -266,7 +266,10 @@ const NodesList = ({ nodes }: NodeListProps) => {
|
|||||||
: "Inactive"}
|
: "Inactive"}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
<Dropdown end>
|
<Dropdown
|
||||||
|
end
|
||||||
|
vertical={idx >= items.length - 2 ? "top" : "bottom"}
|
||||||
|
>
|
||||||
<Dropdown.Toggle button={false}>
|
<Dropdown.Toggle button={false}>
|
||||||
<Button shape="circle" color="ghost">
|
<Button shape="circle" color="ghost">
|
||||||
<EllipsisVertical />
|
<EllipsisVertical />
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ const KeysPage = () => {
|
|||||||
icon={Eye}
|
icon={Eye}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => fetchSecretKey(key.id)}
|
onClick={() => fetchSecretKey(key.id)}
|
||||||
|
className="shrink-0 min-w-[80px]"
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user