diff --git a/desk/sys.kelvin b/desk/sys.kelvin index 5d8a35d..fd409d2 100644 --- a/desk/sys.kelvin +++ b/desk/sys.kelvin @@ -1 +1,2 @@ [%zuse 412] +[%zuse 411] \ No newline at end of file diff --git a/ui/src/app.tsx b/ui/src/app.tsx index ceb9432..40f8783 100644 --- a/ui/src/app.tsx +++ b/ui/src/app.tsx @@ -11,13 +11,13 @@ import { Details } from "./pages/Details"; import { Empty } from "./pages/Empty"; import { Settings } from "./pages/Settings"; import { api } from "./state/api"; -import useStorageState from "./state/storage"; +import { useStorage } from "./state/storage"; import { useFileStore } from "./state/useFileStore"; import { useMedia } from "./lib/useMedia"; import { isDev } from "./lib/util"; export function App() { - const { s3 } = useStorageState(); + const { s3 } = useStorage(); const credentials = s3.credentials; const configuration = s3.configuration; const { client, createClient, getFiles } = useFileStore(); @@ -31,7 +31,7 @@ export function App() { useEffect(() => { async function init() { - useStorageState.getState().initialize(api); + useStorage.getState().initialize(api); } init(); @@ -45,7 +45,7 @@ export function App() { if (hasCredentials) { createClient(credentials, configuration.region); - useStorageState.setState({ hasCredentials: true }); + useStorage.setState({ hasCredentials: true }); isDev && console.log("client initialized"); } }, [credentials, configuration]); diff --git a/ui/src/components/FileActions.tsx b/ui/src/components/FileActions.tsx index e45870a..1d331f3 100644 --- a/ui/src/components/FileActions.tsx +++ b/ui/src/components/FileActions.tsx @@ -2,7 +2,7 @@ import { DownloadIcon, LinkIcon, TrashIcon } from "@heroicons/react/solid"; import classNames from "classnames"; import copy from 'copy-to-clipboard'; import React, { useCallback, useState } from "react"; -import useStorageState from "../state/storage"; +import { useStorage } from "../state/storage"; import { File, getFileUrl, useFileStore } from "../state/useFileStore"; interface FileActionsProps { @@ -39,7 +39,7 @@ function downloadResource(url: string, filename: string) { } export const FileActions = ({ file, className }: FileActionsProps) => { - const { s3 } = useStorageState(); + const { s3 } = useStorage(); const { deleteFile } = useFileStore(); const url = getFileUrl(file.data.Key || '', s3); const [copied, setCopied] = useState(null); diff --git a/ui/src/components/FolderEdit.tsx b/ui/src/components/FolderEdit.tsx index 372ed9e..ef3e143 100644 --- a/ui/src/components/FolderEdit.tsx +++ b/ui/src/components/FolderEdit.tsx @@ -1,7 +1,7 @@ import { FolderIcon, XIcon } from "@heroicons/react/solid"; import React, { useCallback } from "react"; import { useForm } from "react-hook-form"; -import useStorageState from "../state/storage"; +import { useStorage } from "../state/storage"; import { FileStore } from "../state/useFileStore"; interface FolderEditForm { @@ -17,7 +17,7 @@ export const FolderEdit = ({ makeFolder, removeEditingFolder, }: FolderEditProps) => { - const { s3 } = useStorageState(); + const { s3 } = useStorage(); const { handleSubmit, register } = useForm(); const onSubmit = useCallback( diff --git a/ui/src/components/FolderTree.tsx b/ui/src/components/FolderTree.tsx index 11955af..1f7126b 100644 --- a/ui/src/components/FolderTree.tsx +++ b/ui/src/components/FolderTree.tsx @@ -11,7 +11,7 @@ import React, { useCallback } from "react"; import { useDrop } from "react-dnd"; import { Link } from "react-router-dom"; import { dragTypes } from "../pages/Catalog"; -import useStorageState from "../state/storage"; +import { useStorage } from "../state/storage"; import { File, FolderTree as FolderTreeType, @@ -120,7 +120,7 @@ export const FolderTree = ({ topLevelAccordion = false, onClick, }: FolderProps) => { - const { s3 } = useStorageState(); + const { s3 } = useStorage(); const { addFolder, hasFiles, diff --git a/ui/src/components/Refresh.tsx b/ui/src/components/Refresh.tsx index 5702ac1..4c65fa5 100644 --- a/ui/src/components/Refresh.tsx +++ b/ui/src/components/Refresh.tsx @@ -1,11 +1,11 @@ import { RefreshIcon } from '@heroicons/react/solid'; import classNames from 'classnames'; import React from 'react'; -import useStorageState from '../state/storage'; +import { useStorage } from "../state/storage"; import { useFileStore } from '../state/useFileStore'; export const Refresh = ({ className }: { className?: string }) => { - const { s3 } = useStorageState(); + const { s3 } = useStorage(); const { getFiles } = useFileStore(); return ( diff --git a/ui/src/lib/useS3Redirect.ts b/ui/src/lib/useS3Redirect.ts index 122008b..4117653 100644 --- a/ui/src/lib/useS3Redirect.ts +++ b/ui/src/lib/useS3Redirect.ts @@ -1,10 +1,10 @@ import { useEffect } from "react"; import { useNavigate } from "react-router-dom"; -import useStorageState from "../state/storage"; +import { useStorage } from "../state/storage"; export function useS3Redirect() { const navigate = useNavigate(); - const { loaded, hasCredentials, s3 } = useStorageState(); + const { loaded, hasCredentials, s3 } = useStorage(); useEffect(() => { if (loaded && (!hasCredentials || !s3.configuration.currentBucket)) { diff --git a/ui/src/pages/Catalog.tsx b/ui/src/pages/Catalog.tsx index 9b77fbd..80d77cf 100644 --- a/ui/src/pages/Catalog.tsx +++ b/ui/src/pages/Catalog.tsx @@ -7,7 +7,7 @@ import { FileActions } from "../components/FileActions"; import { Folder } from "../components/Folder"; import { Spinner } from "../components/Spinner"; import { useS3Redirect } from "../lib/useS3Redirect"; -import useStorageState, { StorageState } from "../state/storage"; +import { useStorage, StorageState } from "../state/storage"; import { File as FileType, FolderTree, @@ -80,7 +80,7 @@ export function Catalog() { const { pathname } = useLocation(); const match = pathname.replace(/\/page\/\d+/, '').match(/^\/folder\/(.*)/); const page = pathname.match(/\/page\/(\d+)/)?.[1]; - const { s3 } = useStorageState(); + const { s3 } = useStorage(); const isMobile = useMedia('(max-width: 639px), (pointer: coarse)'); const { files, folders, currentFolder, status } = useFileStore(); const { pageSize } = useCatalog(); diff --git a/ui/src/pages/Details.tsx b/ui/src/pages/Details.tsx index d310ce7..62561a7 100644 --- a/ui/src/pages/Details.tsx +++ b/ui/src/pages/Details.tsx @@ -8,7 +8,7 @@ import { isImage, isVideo } from '../lib/file'; import { useS3Redirect } from '../lib/useS3Redirect'; import { getFileInfo, traverseTree, useFileStore } from '../state/useFileStore'; import { FolderTree } from '../components/FolderTree'; -import useStorageState from '../state/storage'; +import { useStorage } from "../state/storage"; import { Header } from '../components/Header'; function formatBytes(bytes: number, decimals = 2) { @@ -27,7 +27,7 @@ export const Details = () => { useS3Redirect(); const navigate = useNavigate(); const { pathname } = useLocation(); - const { s3 } = useStorageState(); + const { s3 } = useStorage(); const { currentFile, currentFolder, diff --git a/ui/src/pages/Empty.tsx b/ui/src/pages/Empty.tsx index 2ee1380..9c7d46a 100644 --- a/ui/src/pages/Empty.tsx +++ b/ui/src/pages/Empty.tsx @@ -1,12 +1,10 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { Navigate } from 'react-router-dom'; -import { addBucket, setAccessKeyId, setCurrentBucket, setEndpoint, setSecretAccessKey } from '@urbit/api'; +import { addBucket, setAccessKeyId, setCurrentBucket, setEndpoint, setSecretAccessKey } from '../state/storage/lib'; import { api } from '../state/api'; -import useStorageState from '../state/storage'; +import { useStorage } from "../state/storage"; import { useAsyncCall } from '../lib/useAsyncCall'; -import { useFileStore } from '../state/useFileStore'; -import { Bucket, ListBucketsCommand } from '@aws-sdk/client-s3'; import { Spinner } from '../components/Spinner'; interface CredentialsSubmit { @@ -18,7 +16,7 @@ interface CredentialsSubmit { } export const Empty = () => { - const { hasCredentials, s3, loaded } = useStorageState(); + const { hasCredentials, s3, loaded } = useStorage(); // const credentials = s3.credentials; // const { client } = useFileStore(); // const [buckets, setBuckets] = useState(); @@ -32,8 +30,8 @@ export const Empty = () => { api.poke(addBucket(data.bucket)); api.poke(setCurrentBucket(data.bucket)) api.poke({ - app: 's3-store', - mark: 's3-action', + app: 'storage', + mark: 'storage-action', json: { 'set-region': data.region }, }); }, [])); diff --git a/ui/src/pages/Settings.tsx b/ui/src/pages/Settings.tsx index 66a6843..0d16770 100644 --- a/ui/src/pages/Settings.tsx +++ b/ui/src/pages/Settings.tsx @@ -1,8 +1,8 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { useForm } from 'react-hook-form'; -import { addBucket, setAccessKeyId, setCurrentBucket, setEndpoint, setSecretAccessKey } from '@urbit/api'; +import { addBucket, setAccessKeyId, setCurrentBucket, setEndpoint, setSecretAccessKey } from '../state/storage/lib'; import { api } from '../state/api'; -import useStorageState from '../state/storage'; +import { useStorage } from "../state/storage"; import { useAsyncCall } from '../lib/useAsyncCall'; interface CredentialsSubmit { @@ -14,8 +14,22 @@ interface CredentialsSubmit { } export const Settings = () => { - const { s3 } = useStorageState(); - const { register, handleSubmit, formState: { errors } } = useForm(); + const { s3 } = useStorage(); + debugger; + const { register, handleSubmit, reset } = useForm(); + + useEffect(() => { + if (s3.credentials && s3.configuration) { + reset({ + endpoint: s3.credentials?.endpoint || '', + accessId: s3.credentials?.accessKeyId || '', + accessSecret: s3.credentials?.secretAccessKey || '', + bucket: s3.configuration.currentBucket, + region: s3.configuration?.region || '', + }) + } + + }, [s3]); const { call: addS3Credentials, status } = useAsyncCall(useCallback(async (data: CredentialsSubmit) => { api.poke(setEndpoint(data.endpoint)) @@ -24,8 +38,8 @@ export const Settings = () => { api.poke(addBucket(data.bucket)); api.poke(setCurrentBucket(data.bucket)) api.poke({ - app: 's3-store', - mark: 's3-action', + app: 'storage', + mark: 'storage-action', json: { 'set-region': data.region }, }); }, [])); diff --git a/ui/src/state/storage/index.ts b/ui/src/state/storage/index.ts index 9181e16..dd7cc44 100644 --- a/ui/src/state/storage/index.ts +++ b/ui/src/state/storage/index.ts @@ -1,89 +1,53 @@ import _ from 'lodash'; -import { api } from '../api'; -import { reduce } from './reducer'; import { enableMapSet } from 'immer'; -import { createState, createSubscription, reduceStateN } from '../base'; -import { S3Credentials } from '@urbit/api'; +import { BaseStorageState, StorageUpdate } from './types'; +import reduce from './reducer'; +import { + createState, + createSubscription, + reduceStateN, + BaseState, +} from '../base'; enableMapSet(); -export interface GcpToken { - accessKey: string; - expiresIn: number; -} - -export interface StorageState { - loaded: boolean; - hasCredentials: boolean; - gcp: { - configured?: boolean; - token?: GcpToken; - isConfigured: () => Promise; - getToken: () => Promise; - }; - s3: { - configuration: { - buckets: Set; - currentBucket: string; - region: string; - }; - credentials: S3Credentials | null; - }; -} - let numLoads = 0; -// @ts-ignore investigate zustand types -const useStorageState = createState( +export type StorageState = BaseStorageState & BaseState; + +export const useStorage = createState( 'Storage', - (set, get) => ({ + () => ({ loaded: false, hasCredentials: false, - gcp: { - isConfigured: () => { - return api.thread({ - inputMark: 'noun', - outputMark: 'json', - threadName: 'gcp-is-configured', - body: {} - }); - }, - getToken: async () => { - const token = await api.thread({ - inputMark: 'noun', - outputMark: 'gcp-token', - threadName: 'gcp-get-token', - body: {} - }); - get().set((state) => { - state.gcp.token = token; - }); - } - }, s3: { configuration: { buckets: new Set(), currentBucket: '', - region: '' + region: '', + publicUrlBase: '', + presignedUrl: '', + service: 'credentials', }, - credentials: null - } + credentials: null, + }, }), - ['loaded', 's3', 'gcp'], + ['loaded', 'hasCredentials', 's3'], [ (set, get) => - createSubscription('storage', '/all', (e) => { - const d = _.get(e, 'storage-update', false); - if (d) { - reduceStateN(get(), d, reduce); - } - - numLoads++; - if (numLoads === 2) { - set({ loaded: true }); + createSubscription( + 'storage', + '/all', + (e: { 'storage-update': StorageUpdate }) => { + const data = _.get(e, 'storage-update', false); + if (data) { + reduceStateN(get(), data, reduce); + } + numLoads += 1; + if (numLoads === 2) { + set({ loaded: true }); + } } - }) + ), ] ); - -export default useStorageState; diff --git a/ui/src/state/storage/lib.ts b/ui/src/state/storage/lib.ts new file mode 100644 index 0000000..a81b846 --- /dev/null +++ b/ui/src/state/storage/lib.ts @@ -0,0 +1,83 @@ +import { Poke } from '@urbit/http-api'; +import { + StorageUpdate, + StorageUpdateCurrentBucket, + StorageUpdateAddBucket, + StorageUpdateRemoveBucket, + StorageUpdateEndpoint, + StorageUpdateAccessKeyId, + StorageUpdateSecretAccessKey, + StorageUpdateRegion, + StorageUpdatePublicUrlBase, + StorageService, + StorageUpdateSetPresignedUrl, + StorageUpdateToggleService, +} from './types'; + +const storageAction = (data: any): Poke => ({ + app: 'storage', + mark: 'storage-action', + json: data, +}); + +export const setCurrentBucket = ( + bucket: string +): Poke => + storageAction({ + 'set-current-bucket': bucket, + }); + +export const addBucket = (bucket: string): Poke => + storageAction({ + 'add-bucket': bucket, + }); + +export const removeBucket = (bucket: string): Poke => + storageAction({ + 'remove-bucket': bucket, + }); + +export const setEndpoint = (endpoint: string): Poke => + storageAction({ + 'set-endpoint': endpoint, + }); + +export const setAccessKeyId = ( + accessKeyId: string +): Poke => + storageAction({ + 'set-access-key-id': accessKeyId, + }); + +export const setSecretAccessKey = ( + secretAccessKey: string +): Poke => + storageAction({ + 'set-secret-access-key': secretAccessKey, + }); + +export const setRegion = (region: string): Poke => + storageAction({ + 'set-region': region, + }); + +export const setPublicUrlBase = ( + publicUrlBase: string +): Poke => + storageAction({ + 'set-public-url-base': publicUrlBase, + }); + +export const setPresignedUrl = ( + presignedUrl: string +): Poke => + storageAction({ + 'set-presigned-url': presignedUrl, + }); + +export const toggleService = ( + service: StorageService +): Poke => + storageAction({ + 'toggle-service': service, + }); diff --git a/ui/src/state/storage/reducer.ts b/ui/src/state/storage/reducer.ts index 2b628dc..7822aaf 100644 --- a/ui/src/state/storage/reducer.ts +++ b/ui/src/state/storage/reducer.ts @@ -1,11 +1,14 @@ -import { S3Update } from '@urbit/api'; +/* eslint-disable no-param-reassign */ import _ from 'lodash'; +import { StorageUpdate, BaseStorageState } from './types'; import { BaseState } from '../base'; -import { StorageState as State } from './index'; -type StorageState = State & BaseState; +export type StorageState = BaseStorageState & BaseState; -const credentials = (json: S3Update, state: StorageState): StorageState => { +const credentials = ( + json: StorageUpdate, + state: StorageState +): StorageState => { const data = _.get(json, 'credentials', false); if (data) { state.s3.credentials = data; @@ -13,19 +16,30 @@ const credentials = (json: S3Update, state: StorageState): StorageState => { return state; }; -const configuration = (json: S3Update, state: StorageState): StorageState => { +const configuration = ( + json: StorageUpdate, + state: StorageState +): StorageState => { const data = _.get(json, 'configuration', false); if (data) { state.s3.configuration = { buckets: new Set(data.buckets), currentBucket: data.currentBucket, - region: data.region + region: data.region, + publicUrlBase: data.publicUrlBase, + // if landscape is not up to date we need to default these so the + // client init logic still works + presignedUrl: data.presignedUrl, + service: data.service || 'credentials', }; } return state; }; -const currentBucket = (json: S3Update, state: StorageState): StorageState => { +const currentBucket = ( + json: StorageUpdate, + state: StorageState +): StorageState => { const data = _.get(json, 'setCurrentBucket', false); if (data && state.s3) { state.s3.configuration.currentBucket = data; @@ -33,16 +47,18 @@ const currentBucket = (json: S3Update, state: StorageState): StorageState => { return state; }; -const addBucket = (json: S3Update, state: StorageState): StorageState => { +const addBucket = (json: StorageUpdate, state: StorageState): StorageState => { const data = _.get(json, 'addBucket', false); if (data) { - state.s3.configuration.buckets = - state.s3.configuration.buckets.add(data); + state.s3.configuration.buckets = state.s3.configuration.buckets.add(data); } return state; }; -const removeBucket = (json: S3Update, state: StorageState): StorageState => { +const removeBucket = ( + json: StorageUpdate, + state: StorageState +): StorageState => { const data = _.get(json, 'removeBucket', false); if (data) { state.s3.configuration.buckets.delete(data); @@ -50,7 +66,7 @@ const removeBucket = (json: S3Update, state: StorageState): StorageState => { return state; }; -const endpoint = (json: S3Update, state: StorageState): StorageState => { +const endpoint = (json: StorageUpdate, state: StorageState): StorageState => { const data = _.get(json, 'setEndpoint', false); if (data && state.s3.credentials) { state.s3.credentials.endpoint = data; @@ -58,7 +74,10 @@ const endpoint = (json: S3Update, state: StorageState): StorageState => { return state; }; -const accessKeyId = (json: S3Update , state: StorageState): StorageState => { +const accessKeyId = ( + json: StorageUpdate, + state: StorageState +): StorageState => { const data = _.get(json, 'setAccessKeyId', false); if (data && state.s3.credentials) { state.s3.credentials.accessKeyId = data; @@ -66,7 +85,10 @@ const accessKeyId = (json: S3Update , state: StorageState): StorageState => { return state; }; -const secretAccessKey = (json: S3Update, state: StorageState): StorageState => { +const secretAccessKey = ( + json: StorageUpdate, + state: StorageState +): StorageState => { const data = _.get(json, 'setSecretAccessKey', false); if (data && state.s3.credentials) { state.s3.credentials.secretAccessKey = data; @@ -74,7 +96,47 @@ const secretAccessKey = (json: S3Update, state: StorageState): StorageState => { return state; }; -export const reduce = [ +const region = (json: StorageUpdate, state: StorageState): StorageState => { + const data = _.get(json, 'setRegion', false); + if (data && state.s3.configuration) { + state.s3.configuration.region = data; + } + return state; +}; + +const publicUrlBase = ( + json: StorageUpdate, state: StorageState +): StorageState => { + const data = _.get(json, 'setPublicUrlBase', false); + if (data && state.s3.configuration) { + state.s3.configuration.publicUrlBase = data; + } + return state; +}; + +const presignedUrl = ( + json: StorageUpdate, + state: StorageState +): StorageState => { + const data = _.get(json, 'setPresignedUrl', false); + if (data && state.s3.configuration) { + state.s3.configuration.presignedUrl = data; + } + return state; +}; + +const toggleService = ( + json: StorageUpdate, + state: StorageState +): StorageState => { + const data = _.get(json, 'toggleService', false); + if (data && state.s3.configuration) { + state.s3.configuration.service = data; + } + return state; +}; + +const reduce = [ credentials, configuration, currentBucket, @@ -82,5 +144,11 @@ export const reduce = [ removeBucket, endpoint, accessKeyId, - secretAccessKey + secretAccessKey, + region, + publicUrlBase, + presignedUrl, + toggleService, ]; + +export default reduce; diff --git a/ui/src/state/storage/types.ts b/ui/src/state/storage/types.ts new file mode 100644 index 0000000..eadb9f8 --- /dev/null +++ b/ui/src/state/storage/types.ts @@ -0,0 +1,91 @@ +export type StorageService = 'presigned-url' | 'credentials'; + +export interface StorageConfiguration { + buckets: Set; + currentBucket: string; + region: string; + publicUrlBase: string; + presignedUrl: string; + service: StorageService; +} + +export interface BaseStorageState { + loaded?: boolean; + hasCredentials?: boolean; + s3: { + configuration: StorageConfiguration; + credentials: StorageCredentials | null; + }; + [ref: string]: unknown; +} + +export interface StorageCredentials { + endpoint: string; + accessKeyId: string; + secretAccessKey: string; +} + +export interface StorageUpdateCredentials { + credentials: StorageCredentials; +} + +export interface StorageUpdateConfiguration { + configuration: { + buckets: string[]; + currentBucket: string; + }; +} + +export interface StorageUpdateCurrentBucket { + setCurrentBucket: string; +} + +export interface StorageUpdateAddBucket { + addBucket: string; +} + +export interface StorageUpdateRemoveBucket { + removeBucket: string; +} + +export interface StorageUpdateEndpoint { + setEndpoint: string; +} + +export interface StorageUpdateAccessKeyId { + setAccessKeyId: string; +} + +export interface StorageUpdateSecretAccessKey { + setSecretAccessKey: string; +} + +export interface StorageUpdateRegion { + setRegion: string; +} + +export interface StorageUpdatePublicUrlBase { + setPublicUrlBase: string; +} + +export interface StorageUpdateToggleService { + toggleService: string; +} + +export interface StorageUpdateSetPresignedUrl { + setPresignedUrl: string; +} + +export declare type StorageUpdate = + | StorageUpdateCredentials + | StorageUpdateConfiguration + | StorageUpdateCurrentBucket + | StorageUpdateAddBucket + | StorageUpdateRemoveBucket + | StorageUpdateEndpoint + | StorageUpdateAccessKeyId + | StorageUpdateSecretAccessKey + | StorageUpdateRegion + | StorageUpdatePublicUrlBase + | StorageUpdateToggleService + | StorageUpdateSetPresignedUrl; diff --git a/ui/src/state/useFileStore.ts b/ui/src/state/useFileStore.ts index 8c39a85..db359a6 100644 --- a/ui/src/state/useFileStore.ts +++ b/ui/src/state/useFileStore.ts @@ -315,7 +315,7 @@ export const useFileStore = create( const key = file.Key || ""; const { folder, filename, ...info } = getFileInfo(key); const newTree = parseFolderIntoTree(splitPath(folder)); - isDev && console.log(key); + // isDev && console.log(key); if (newTree) { const mergedTrees = mergeTrees(tree, newTree); diff --git a/ui/src/upload/DropZone.tsx b/ui/src/upload/DropZone.tsx index d695cf6..31fab29 100644 --- a/ui/src/upload/DropZone.tsx +++ b/ui/src/upload/DropZone.tsx @@ -14,7 +14,7 @@ import { defaultStyles, FileIcon } from "react-file-icon"; import create from "zustand"; import { Spinner } from "../components/Spinner"; import { Status } from "../lib/useAsyncCall"; -import useStorageState from "../state/storage"; +import { useStorage } from "../state/storage"; import { getFilenameParts, useFileStore } from "../state/useFileStore"; export type DropStatus = "initial" | "open" | "dropping" | "dropped"; @@ -36,7 +36,7 @@ export const useDropZone = create(() => ({ })); export const DropZone = () => { - const { s3 } = useStorageState(); + const { s3 } = useStorage(); const { client, currentFolder, getFiles } = useFileStore(); const { status, files } = useDropZone(); const dropZone = useRef(null);