mirror of
				https://github.com/khairul169/garage-webui.git
				synced 2025-10-29 22:29:32 +07:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			611258d0db
			...
			b8b87d8289
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b8b87d8289 | |||
| 1b1b815443 | |||
| 37027396ca | |||
| c1619276c0 | 
| @ -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.4/garage-webui-v1.0.4-linux-amd64 | $ wget -O garage-webui https://github.com/khairul169/garage-webui/releases/download/1.0.5/garage-webui-v1.0.5-linux-amd64 | ||||||
| $ chmod +x garage-webui | $ chmod +x garage-webui | ||||||
| $ sudo cp garage-webui /usr/local/bin | $ sudo cp garage-webui /usr/local/bin | ||||||
| ``` | ``` | ||||||
| @ -134,6 +134,8 @@ However, if it fails to load, you can set these environment variables instead: | |||||||
| - `CONFIG_PATH`: Path to the Garage `config.toml` file. Defaults to `/etc/garage.toml`. | - `CONFIG_PATH`: Path to the Garage `config.toml` file. Defaults to `/etc/garage.toml`. | ||||||
| - `API_BASE_URL`: Garage admin API endpoint URL. | - `API_BASE_URL`: Garage admin API endpoint URL. | ||||||
| - `API_ADMIN_KEY`: Admin API key. | - `API_ADMIN_KEY`: Admin API key. | ||||||
|  | - `S3_REGION`: S3 Region. | ||||||
|  | - `S3_ENDPOINT_URL`: S3 Endpoint url. | ||||||
| 
 | 
 | ||||||
| ### Running | ### Running | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ require ( | |||||||
| 	github.com/aws/aws-sdk-go-v2/service/s3 v1.59.0 | 	github.com/aws/aws-sdk-go-v2/service/s3 v1.59.0 | ||||||
| 	github.com/aws/smithy-go v1.20.4 | 	github.com/aws/smithy-go v1.20.4 | ||||||
| 	github.com/joho/godotenv v1.5.1 | 	github.com/joho/godotenv v1.5.1 | ||||||
|  | 	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 | ||||||
| 	github.com/pelletier/go-toml/v2 v2.2.2 | 	github.com/pelletier/go-toml/v2 v2.2.2 | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -27,6 +27,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c | |||||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||||
| github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= | ||||||
| github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= | ||||||
|  | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= | ||||||
|  | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= | ||||||
| github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= | ||||||
| github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= | ||||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ func main() { | |||||||
| 	utils.InitCacheManager() | 	utils.InitCacheManager() | ||||||
| 
 | 
 | ||||||
| 	if err := utils.Garage.LoadConfig(); err != nil { | 	if err := utils.Garage.LoadConfig(); err != nil { | ||||||
| 		log.Fatal("Failed to load config! ", err) | 		log.Println("Cannot load garage config!", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	http.Handle("/api/", http.StripPrefix("/api", router.HandleApiRouter())) | 	http.Handle("/api/", http.StripPrefix("/api", router.HandleApiRouter())) | ||||||
|  | |||||||
| @ -73,8 +73,7 @@ func (b *Browse) GetObjects(w http.ResponseWriter, r *http.Request) { | |||||||
| 			ObjectKey:    &key, | 			ObjectKey:    &key, | ||||||
| 			LastModified: object.LastModified, | 			LastModified: object.LastModified, | ||||||
| 			Size:         object.Size, | 			Size:         object.Size, | ||||||
| 			ViewUrl:      fmt.Sprintf("/browse/%s/%s?view=1", bucket, *object.Key), | 			Url:          fmt.Sprintf("/browse/%s/%s", bucket, *object.Key), | ||||||
| 			DownloadUrl:  fmt.Sprintf("/browse/%s/%s?dl=1", bucket, *object.Key), |  | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -84,8 +83,10 @@ func (b *Browse) GetObjects(w http.ResponseWriter, r *http.Request) { | |||||||
| func (b *Browse) GetOneObject(w http.ResponseWriter, r *http.Request) { | func (b *Browse) GetOneObject(w http.ResponseWriter, r *http.Request) { | ||||||
| 	bucket := r.PathValue("bucket") | 	bucket := r.PathValue("bucket") | ||||||
| 	key := r.PathValue("key") | 	key := r.PathValue("key") | ||||||
| 	view := r.URL.Query().Get("view") == "1" | 	queryParams := r.URL.Query() | ||||||
| 	download := r.URL.Query().Get("dl") == "1" | 	view := queryParams.Get("view") == "1" | ||||||
|  | 	thumbnail := queryParams.Get("thumb") == "1" | ||||||
|  | 	download := queryParams.Get("dl") == "1" | ||||||
| 
 | 
 | ||||||
| 	client, err := getS3Client(bucket) | 	client, err := getS3Client(bucket) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -93,6 +94,18 @@ func (b *Browse) GetOneObject(w http.ResponseWriter, r *http.Request) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if !view && !download && !thumbnail { | ||||||
|  | 		object, err := client.HeadObject(context.Background(), &s3.HeadObjectInput{ | ||||||
|  | 			Bucket: aws.String(bucket), | ||||||
|  | 			Key:    aws.String(key), | ||||||
|  | 		}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			utils.ResponseError(w, err) | ||||||
|  | 		} | ||||||
|  | 		utils.ResponseSuccess(w, object) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	object, err := client.GetObject(context.Background(), &s3.GetObjectInput{ | 	object, err := client.GetObject(context.Background(), &s3.GetObjectInput{ | ||||||
| 		Bucket: aws.String(bucket), | 		Bucket: aws.String(bucket), | ||||||
| 		Key:    aws.String(key), | 		Key:    aws.String(key), | ||||||
| @ -109,31 +122,42 @@ func (b *Browse) GetOneObject(w http.ResponseWriter, r *http.Request) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if view || download { | 	defer object.Body.Close() | ||||||
| 		defer object.Body.Close() | 	keys := strings.Split(key, "/") | ||||||
| 		keys := strings.Split(key, "/") |  | ||||||
| 
 |  | ||||||
| 		w.Header().Set("Content-Type", *object.ContentType) |  | ||||||
| 		w.Header().Set("Content-Length", strconv.FormatInt(*object.ContentLength, 10)) |  | ||||||
| 		w.Header().Set("Cache-Control", "max-age=86400") |  | ||||||
| 		w.Header().Set("Last-Modified", object.LastModified.Format(time.RFC1123)) |  | ||||||
| 		w.Header().Set("Etag", *object.ETag) |  | ||||||
| 
 |  | ||||||
| 		if download { |  | ||||||
| 			w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", keys[len(keys)-1])) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		w.WriteHeader(http.StatusOK) |  | ||||||
| 		_, err = io.Copy(w, object.Body) |  | ||||||
| 
 | 
 | ||||||
|  | 	if download { | ||||||
|  | 		w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", keys[len(keys)-1])) | ||||||
|  | 	} else if thumbnail { | ||||||
|  | 		body, err := io.ReadAll(object.Body) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			utils.ResponseError(w, err) | 			utils.ResponseError(w, err) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
|  | 		thumb, err := utils.CreateThumbnailImage(body, 64, 64) | ||||||
|  | 		if err != nil { | ||||||
|  | 
 | ||||||
|  | 			utils.ResponseError(w, err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		w.Header().Set("Content-Type", "image/png") | ||||||
|  | 		w.Write(thumb) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	utils.ResponseSuccess(w, object) | 	w.Header().Set("Content-Type", *object.ContentType) | ||||||
|  | 	w.Header().Set("Content-Length", strconv.FormatInt(*object.ContentLength, 10)) | ||||||
|  | 	w.Header().Set("Cache-Control", "max-age=86400") | ||||||
|  | 	w.Header().Set("Last-Modified", object.LastModified.Format(time.RFC1123)) | ||||||
|  | 	w.Header().Set("Etag", *object.ETag) | ||||||
|  | 
 | ||||||
|  | 	_, err = io.Copy(w, object.Body) | ||||||
|  | 
 | ||||||
|  | 	if err != nil { | ||||||
|  | 		utils.ResponseError(w, err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (b *Browse) PutObject(w http.ResponseWriter, r *http.Request) { | func (b *Browse) PutObject(w http.ResponseWriter, r *http.Request) { | ||||||
| @ -300,7 +324,7 @@ func getS3Client(bucket string) (*s3.Client, error) { | |||||||
| 
 | 
 | ||||||
| 	awsConfig := aws.Config{ | 	awsConfig := aws.Config{ | ||||||
| 		Credentials:  creds, | 		Credentials:  creds, | ||||||
| 		Region:       "garage", | 		Region:       utils.Garage.GetS3Region(), | ||||||
| 		BaseEndpoint: aws.String(utils.Garage.GetS3Endpoint()), | 		BaseEndpoint: aws.String(utils.Garage.GetS3Endpoint()), | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -13,6 +13,5 @@ type BrowserObject struct { | |||||||
| 	ObjectKey    *string    `json:"objectKey"` | 	ObjectKey    *string    `json:"objectKey"` | ||||||
| 	LastModified *time.Time `json:"lastModified"` | 	LastModified *time.Time `json:"lastModified"` | ||||||
| 	Size         *int64     `json:"size"` | 	Size         *int64     `json:"size"` | ||||||
| 	ViewUrl      string     `json:"viewUrl"` | 	Url          string     `json:"url"` | ||||||
| 	DownloadUrl  string     `json:"downloadUrl"` |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ package utils | |||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"khairul169/garage-webui/schema" | 	"khairul169/garage-webui/schema" | ||||||
| @ -73,6 +74,17 @@ func (g *garage) GetS3Endpoint() string { | |||||||
| 	return endpoint | 	return endpoint | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (g *garage) GetS3Region() string { | ||||||
|  | 	endpoint := os.Getenv("S3_REGION") | ||||||
|  | 	if len(endpoint) > 0 { | ||||||
|  | 		return endpoint | ||||||
|  | 	} | ||||||
|  | 	if len(g.Config.S3API.S3Region) == 0 { | ||||||
|  | 		return "garage" | ||||||
|  | 	} | ||||||
|  | 	return g.Config.S3API.S3Region | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (g *garage) GetAdminKey() string { | func (g *garage) GetAdminKey() string { | ||||||
| 	key := os.Getenv("API_ADMIN_KEY") | 	key := os.Getenv("API_ADMIN_KEY") | ||||||
| 	if len(key) > 0 { | 	if len(key) > 0 { | ||||||
| @ -153,7 +165,7 @@ func (g *garage) Fetch(url string, options *FetchOptions) ([]byte, error) { | |||||||
| 			message = fmt.Sprintf("%v", data["message"]) | 			message = fmt.Sprintf("%v", data["message"]) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return nil, fmt.Errorf(message) | 		return nil, errors.New(message) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	body, err := io.ReadAll(res.Body) | 	body, err := io.ReadAll(res.Body) | ||||||
|  | |||||||
							
								
								
									
										26
									
								
								backend/utils/image.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								backend/utils/image.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | |||||||
|  | package utils | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"image" | ||||||
|  | 	_ "image/gif" | ||||||
|  | 	"image/jpeg" | ||||||
|  | 	_ "image/png" | ||||||
|  | 
 | ||||||
|  | 	"github.com/nfnt/resize" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func CreateThumbnailImage(buffer []byte, width uint, height uint) ([]byte, error) { | ||||||
|  | 	img, _, err := image.Decode(bytes.NewReader(buffer)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	thumb := resize.Thumbnail(width, height, img, resize.NearestNeighbor) | ||||||
|  | 	buf := new(bytes.Buffer) | ||||||
|  | 	if err := jpeg.Encode(buf, thumb, nil); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return buf.Bytes(), nil | ||||||
|  | } | ||||||
| @ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|   "name": "garage-webui", |   "name": "garage-webui", | ||||||
|   "private": true, |   "private": true, | ||||||
|   "version": "1.0.4", |   "version": "1.0.5", | ||||||
|   "type": "module", |   "type": "module", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "dev:client": "vite", |     "dev:client": "vite", | ||||||
|  | |||||||
| @ -30,23 +30,21 @@ const api = { | |||||||
|       headers: { ...headers, ...(options?.headers || {}) }, |       headers: { ...headers, ...(options?.headers || {}) }, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     if (!res.ok) { |  | ||||||
|       const json = await res.json().catch(() => {}); |  | ||||||
|       const message = json?.message || res.statusText; |  | ||||||
|       throw new Error(message); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const isJson = res.headers |     const isJson = res.headers | ||||||
|       .get("Content-Type") |       .get("Content-Type") | ||||||
|       ?.includes("application/json"); |       ?.includes("application/json"); | ||||||
|  |     const data = isJson ? await res.json() : await res.text(); | ||||||
| 
 | 
 | ||||||
|     if (isJson) { |     if (!res.ok) { | ||||||
|       const json = (await res.json()) as T; |       const message = isJson | ||||||
|       return json; |         ? data?.message | ||||||
|  |         : typeof data === "string" | ||||||
|  |         ? data | ||||||
|  |         : res.statusText; | ||||||
|  |       throw new Error(message); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const text = await res.text(); |     return data as unknown as T; | ||||||
|     return text as unknown as T; |  | ||||||
|   }, |   }, | ||||||
| 
 | 
 | ||||||
|   async get<T = any>(url: string, options?: Partial<FetchOptions>) { |   async get<T = any>(url: string, options?: Partial<FetchOptions>) { | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ import { shareDialog } from "./share-dialog"; | |||||||
| 
 | 
 | ||||||
| type Props = { | type Props = { | ||||||
|   prefix?: string; |   prefix?: string; | ||||||
|   object: Pick<Object, "objectKey" | "downloadUrl">; |   object: Pick<Object, "objectKey" | "url">; | ||||||
|   end?: boolean; |   end?: boolean; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| @ -30,7 +30,7 @@ const ObjectActions = ({ prefix = "", object, end }: Props) => { | |||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   const onDownload = () => { |   const onDownload = () => { | ||||||
|     window.open(API_URL + object.downloadUrl, "_blank"); |     window.open(API_URL + object.url + "?dl=1", "_blank"); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const onDelete = () => { |   const onDelete = () => { | ||||||
|  | |||||||
| @ -1,10 +1,16 @@ | |||||||
| import { Table } from "react-daisyui"; | import { Alert, Loading, Table } from "react-daisyui"; | ||||||
| import { useBrowseObjects } from "./hooks"; | import { useBrowseObjects } from "./hooks"; | ||||||
| import { dayjs, readableBytes } from "@/lib/utils"; | import { dayjs, readableBytes } from "@/lib/utils"; | ||||||
| import mime from "mime/lite"; | import mime from "mime/lite"; | ||||||
| import { Object } from "./types"; | import { Object } from "./types"; | ||||||
| import { API_URL } from "@/lib/api"; | import { API_URL } from "@/lib/api"; | ||||||
| import { FileArchive, FileIcon, FileType, Folder } from "lucide-react"; | import { | ||||||
|  |   CircleXIcon, | ||||||
|  |   FileArchive, | ||||||
|  |   FileIcon, | ||||||
|  |   FileType, | ||||||
|  |   Folder, | ||||||
|  | } from "lucide-react"; | ||||||
| import { useBucketContext } from "../context"; | import { useBucketContext } from "../context"; | ||||||
| import ObjectActions from "./object-actions"; | import ObjectActions from "./object-actions"; | ||||||
| import GotoTopButton from "@/components/ui/goto-top-btn"; | import GotoTopButton from "@/components/ui/goto-top-btn"; | ||||||
| @ -16,14 +22,17 @@ type Props = { | |||||||
| 
 | 
 | ||||||
| const ObjectList = ({ prefix, onPrefixChange }: Props) => { | const ObjectList = ({ prefix, onPrefixChange }: Props) => { | ||||||
|   const { bucketName } = useBucketContext(); |   const { bucketName } = useBucketContext(); | ||||||
|   const { data } = useBrowseObjects(bucketName, { prefix, limit: 1000 }); |   const { data, error, isLoading } = useBrowseObjects(bucketName, { | ||||||
|  |     prefix, | ||||||
|  |     limit: 1000, | ||||||
|  |   }); | ||||||
| 
 | 
 | ||||||
|   const onObjectClick = (object: Object) => { |   const onObjectClick = (object: Object) => { | ||||||
|     window.open(API_URL + object.viewUrl, "_blank"); |     window.open(API_URL + object.url + "?view=1", "_blank"); | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className="overflow-x-auto overflow-y-hidden"> |     <div className="overflow-x-auto min-h-[400px]"> | ||||||
|       <Table> |       <Table> | ||||||
|         <Table.Head> |         <Table.Head> | ||||||
|           <span>Name</span> |           <span>Name</span> | ||||||
| @ -32,13 +41,29 @@ const ObjectList = ({ prefix, onPrefixChange }: Props) => { | |||||||
|         </Table.Head> |         </Table.Head> | ||||||
| 
 | 
 | ||||||
|         <Table.Body> |         <Table.Body> | ||||||
|           {!data?.prefixes?.length && !data?.objects?.length && ( |           {isLoading ? ( | ||||||
|             <tr> |             <tr> | ||||||
|               <td className="text-center py-8" colSpan={3}> |               <td colSpan={3}> | ||||||
|  |                 <div className="h-[320px] flex items-center justify-center"> | ||||||
|  |                   <Loading /> | ||||||
|  |                 </div> | ||||||
|  |               </td> | ||||||
|  |             </tr> | ||||||
|  |           ) : error ? ( | ||||||
|  |             <tr> | ||||||
|  |               <td colSpan={3}> | ||||||
|  |                 <Alert status="error" icon={<CircleXIcon />}> | ||||||
|  |                   <span>{error.message}</span> | ||||||
|  |                 </Alert> | ||||||
|  |               </td> | ||||||
|  |             </tr> | ||||||
|  |           ) : !data?.prefixes?.length && !data?.objects?.length ? ( | ||||||
|  |             <tr> | ||||||
|  |               <td className="text-center py-16" colSpan={3}> | ||||||
|                 No objects |                 No objects | ||||||
|               </td> |               </td> | ||||||
|             </tr> |             </tr> | ||||||
|           )} |           ) : null} | ||||||
| 
 | 
 | ||||||
|           {data?.prefixes.map((prefix) => ( |           {data?.prefixes.map((prefix) => ( | ||||||
|             <tr |             <tr | ||||||
| @ -59,7 +84,7 @@ const ObjectList = ({ prefix, onPrefixChange }: Props) => { | |||||||
|                 </span> |                 </span> | ||||||
|               </td> |               </td> | ||||||
|               <td colSpan={2} /> |               <td colSpan={2} /> | ||||||
|               <ObjectActions object={{ objectKey: prefix, downloadUrl: "" }} /> |               <ObjectActions object={{ objectKey: prefix, url: "" }} /> | ||||||
|             </tr> |             </tr> | ||||||
|           ))} |           ))} | ||||||
| 
 | 
 | ||||||
| @ -96,7 +121,9 @@ const ObjectList = ({ prefix, onPrefixChange }: Props) => { | |||||||
|                 <ObjectActions |                 <ObjectActions | ||||||
|                   prefix={data.prefix} |                   prefix={data.prefix} | ||||||
|                   object={object} |                   object={object} | ||||||
|                   end={idx >= data.objects.length - 2} |                   end={ | ||||||
|  |                     idx >= data.objects.length - 2 && data.objects.length > 5 | ||||||
|  |                   } | ||||||
|                 /> |                 /> | ||||||
|               </tr> |               </tr> | ||||||
|             ); |             ); | ||||||
| @ -125,9 +152,10 @@ const FilePreview = ({ ext, object }: FilePreviewProps) => { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if (type === "image") { |   if (type === "image") { | ||||||
|  |     const thumbnailSupport = ["jpg", "jpeg", "png", "gif"].includes(ext || ""); | ||||||
|     return ( |     return ( | ||||||
|       <img |       <img | ||||||
|         src={API_URL + object.viewUrl} |         src={API_URL + object.url + (thumbnailSupport ? "?thumb=1" : "?view=1")} | ||||||
|         alt={object.objectKey} |         alt={object.objectKey} | ||||||
|         className="size-5 object-cover overflow-hidden mr-2" |         className="size-5 object-cover overflow-hidden mr-2" | ||||||
|       /> |       /> | ||||||
|  | |||||||
| @ -15,8 +15,7 @@ export type Object = { | |||||||
|   objectKey: string; |   objectKey: string; | ||||||
|   lastModified: Date; |   lastModified: Date; | ||||||
|   size: number; |   size: number; | ||||||
|   viewUrl: string; |   url: string; | ||||||
|   downloadUrl: string; |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export type PutObjectPayload = { | export type PutObjectPayload = { | ||||||
|  | |||||||
| @ -2,12 +2,18 @@ import { useParams } from "react-router-dom"; | |||||||
| import { useBucket } from "./hooks"; | import { useBucket } from "./hooks"; | ||||||
| import Page from "@/context/page-context"; | import Page from "@/context/page-context"; | ||||||
| import TabView, { Tab } from "@/components/containers/tab-view"; | import TabView, { Tab } from "@/components/containers/tab-view"; | ||||||
| import { ChartLine, FolderSearch, LockKeyhole } from "lucide-react"; | import { | ||||||
|  |   ChartLine, | ||||||
|  |   CircleXIcon, | ||||||
|  |   FolderSearch, | ||||||
|  |   LockKeyhole, | ||||||
|  | } from "lucide-react"; | ||||||
| import OverviewTab from "./overview/overview-tab"; | import OverviewTab from "./overview/overview-tab"; | ||||||
| import PermissionsTab from "./permissions/permissions-tab"; | import PermissionsTab from "./permissions/permissions-tab"; | ||||||
| import MenuButton from "./components/menu-button"; | import MenuButton from "./components/menu-button"; | ||||||
| import BrowseTab from "./browse/browse-tab"; | import BrowseTab from "./browse/browse-tab"; | ||||||
| import { BucketContext } from "./context"; | import { BucketContext } from "./context"; | ||||||
|  | import { Alert, Loading } from "react-daisyui"; | ||||||
| 
 | 
 | ||||||
| const tabs: Tab[] = [ | const tabs: Tab[] = [ | ||||||
|   { |   { | ||||||
| @ -32,26 +38,40 @@ const tabs: Tab[] = [ | |||||||
| 
 | 
 | ||||||
| const ManageBucketPage = () => { | const ManageBucketPage = () => { | ||||||
|   const { id } = useParams(); |   const { id } = useParams(); | ||||||
|   const { data, refetch } = useBucket(id); |   const { data, error, isLoading, refetch } = useBucket(id); | ||||||
| 
 | 
 | ||||||
|   const name = data?.globalAliases[0]; |   const name = data?.globalAliases[0]; | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className="container"> |     <> | ||||||
|       <Page |       <Page | ||||||
|         title={name || "Manage Bucket"} |         title={name || "Manage Bucket"} | ||||||
|         prev="/buckets" |         prev="/buckets" | ||||||
|         actions={data ? <MenuButton /> : undefined} |         actions={data ? <MenuButton /> : undefined} | ||||||
|       /> |       /> | ||||||
| 
 | 
 | ||||||
|       {data && ( |       {isLoading && ( | ||||||
|         <BucketContext.Provider |         <div className="h-full flex items-center justify-center"> | ||||||
|           value={{ bucket: data, refetch, bucketName: name || "" }} |           <Loading size="lg" /> | ||||||
|         > |         </div> | ||||||
|           <TabView tabs={tabs} className="bg-base-100 h-14 px-1.5" /> |  | ||||||
|         </BucketContext.Provider> |  | ||||||
|       )} |       )} | ||||||
|     </div> | 
 | ||||||
|  |       {error != null && ( | ||||||
|  |         <Alert status="error" icon={<CircleXIcon />}> | ||||||
|  |           <span>{error.message}</span> | ||||||
|  |         </Alert> | ||||||
|  |       )} | ||||||
|  | 
 | ||||||
|  |       {data && ( | ||||||
|  |         <div className="container"> | ||||||
|  |           <BucketContext.Provider | ||||||
|  |             value={{ bucket: data, refetch, bucketName: name || "" }} | ||||||
|  |           > | ||||||
|  |             <TabView tabs={tabs} className="bg-base-100 h-14 px-1.5" /> | ||||||
|  |           </BucketContext.Provider> | ||||||
|  |         </div> | ||||||
|  |       )} | ||||||
|  |     </> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user