diff --git a/fission/.env b/fission/.env deleted file mode 100644 index f5c2b2b526..0000000000 --- a/fission/.env +++ /dev/null @@ -1,2 +0,0 @@ -VITE_SYNTHESIS_SERVER_PATH=/ -# VITE_SYNTHESIS_SERVER_PATH=https://synthesis.autodesk.com/ \ No newline at end of file diff --git a/fission/bun.lockb b/fission/bun.lockb deleted file mode 100755 index 397fe36750..0000000000 Binary files a/fission/bun.lockb and /dev/null differ diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 9edd6954a1..d303e6c0b3 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -52,9 +52,9 @@ import DriverStationPanel from "@/panels/simulation/DriverStationPanel" import ManageAssembliesModal from "@/modals/spawning/ManageAssembliesModal.tsx" import World from "@/systems/World.ts" import { AddRobotsModal, AddFieldsModal, SpawningModal } from "@/modals/spawning/SpawningModals.tsx" -import ImportMirabufModal from "@/modals/mirabuf/ImportMirabufModal.tsx" import ImportLocalMirabufModal from "@/modals/mirabuf/ImportLocalMirabufModal.tsx" import APS from "./aps/APS.ts" +import ImportMirabufPanel from "@/ui/panels/mirabuf/ImportMirabufPanel.tsx" import Skybox from "./ui/components/Skybox.tsx" import PokerPanel from "@/panels/PokerPanel.tsx" @@ -212,7 +212,6 @@ const initialModals = [ , , , - , , ] @@ -225,6 +224,7 @@ const initialPanels: ReactElement[] = [ , , , + , , ] diff --git a/fission/src/aps/APS.ts b/fission/src/aps/APS.ts index 6c5ae6388c..4ac006addf 100644 --- a/fission/src/aps/APS.ts +++ b/fission/src/aps/APS.ts @@ -8,14 +8,14 @@ export const APS_USER_INFO_UPDATE_EVENT = "aps_user_info_update" const CLIENT_ID = "GCxaewcLjsYlK8ud7Ka9AKf9dPwMR3e4GlybyfhAK2zvl3tU" -const ENDPOINT_SYNTHESIS_CODE = `${import.meta.env.VITE_SYNTHESIS_SERVER_PATH}api/aps/code` -export const ENDPOINT_SYNTHESIS_CHALLENGE = `${import.meta.env.VITE_SYNTHESIS_SERVER_PATH}api/aps/challenge` +const ENDPOINT_SYNTHESIS_CODE = `/api/aps/code` +export const ENDPOINT_SYNTHESIS_CHALLENGE = `/api/aps/challenge` const ENDPOINT_AUTODESK_AUTHENTICATION_AUTHORIZE = "https://developer.api.autodesk.com/authentication/v2/authorize" const ENDPOINT_AUTODESK_AUTHENTICATION_TOKEN = "https://developer.api.autodesk.com/authentication/v2/token" const ENDPOINT_AUTODESK_USERINFO = "https://api.userprofile.autodesk.com/userinfo" -interface APSAuth { +export interface APSAuth { access_token: string refresh_token: string expires_in: number @@ -23,7 +23,7 @@ interface APSAuth { token_type: number } -interface APSUserInfo { +export interface APSUserInfo { name: string picture: string givenName: string @@ -73,7 +73,14 @@ class APS { * Returns the auth data of the current user. See {@link APSAuth} * @returns {(APSAuth | undefined)} Auth data of the current user */ - static getAuth(): APSAuth | undefined { + static async getAuth(): Promise { + const auth = this.auth + if (!auth) return undefined + + if (Date.now() > auth.expires_at) { + console.debug("Expired. Refreshing...") + await this.refreshAuthToken(auth.refresh_token, false) + } return this.auth } @@ -83,10 +90,13 @@ class APS { */ static async getAuthOrLogin(): Promise { const auth = this.auth - if (!auth) return undefined + if (!auth) { + this.requestAuthCode() + return undefined + } if (Date.now() > auth.expires_at) { - await this.refreshAuthToken(auth.refresh_token) + await this.refreshAuthToken(auth.refresh_token, true) } return this.auth } @@ -158,9 +168,12 @@ class APS { /** * Refreshes the access token using our refresh token. * @param {string} refresh_token - The refresh token from our auth data + * + * @returns If the promise returns true, that means the auth token is currently available. If not, it means it + * is not readily available, although one may be in the works */ - static async refreshAuthToken(refresh_token: string) { - await this.requestMutex.runExclusive(async () => { + static async refreshAuthToken(refresh_token: string, shouldRelog: boolean): Promise { + return this.requestMutex.runExclusive(async () => { try { const res = await fetch(ENDPOINT_AUTODESK_AUTHENTICATION_TOKEN, { method: "POST", @@ -176,12 +189,16 @@ class APS { }) const json = await res.json() if (!res.ok) { - MainHUD_AddToast("error", "Error signing in.", json.userMessage) - this.auth = undefined - await this.requestAuthCode() - return + if (shouldRelog) { + MainHUD_AddToast("warning", "Must Re-signin.", json.userMessage) + this.auth = undefined + await this.requestAuthCode() + return false + } else { + return false + } } - json.expires_at = json.expires_in + Date.now() + json.expires_at = json.expires_in * 1000 + Date.now() this.auth = json as APSAuth if (this.auth) { await this.loadUserInfo(this.auth) @@ -189,10 +206,12 @@ class APS { MainHUD_AddToast("info", "ADSK Login", `Hello, ${APS.userInfo.givenName}`) } } + return true } catch (e) { MainHUD_AddToast("error", "Error signing in.", "Please try again.") this.auth = undefined await this.requestAuthCode() + return false } }) } @@ -212,7 +231,7 @@ class APS { return } const auth_res = json.response as APSAuth - auth_res.expires_at = auth_res.expires_in + Date.now() + auth_res.expires_at = auth_res.expires_in * 1000 + Date.now() this.auth = auth_res console.log("Preloading user info") const auth = await this.getAuth() diff --git a/fission/src/aps/APSDataManagement.ts b/fission/src/aps/APSDataManagement.ts index 833794583e..6eac626c53 100644 --- a/fission/src/aps/APSDataManagement.ts +++ b/fission/src/aps/APSDataManagement.ts @@ -1,9 +1,45 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { MainHUD_AddToast } from "@/ui/components/MainHUD" import APS from "./APS" +import TaskStatus from "@/util/TaskStatus" +import { Mutex } from "async-mutex" export const FOLDER_DATA_TYPE = "folders" export const ITEM_DATA_TYPE = "items" +let mirabufFiles: Data[] | undefined +const mirabufFilesMutex: Mutex = new Mutex() + +export class APSDataError extends Error { + error_code: string + title: string + detail: string + + constructor(error_code: string, title: string, detail: string) { + super(title) + this.name = "APSDataError" + this.error_code = error_code + this.title = title + this.detail = detail + } +} + +export type APSHubError = { + Id: string | null + HttpStatusCode: string + ErrorCode: string + Title: string + Detail: string + AboutLink: string | null + Source: string | null + meta: object | null +} + +export type Filter = { + fieldName: string + matchValue: string +} + export interface Hub { id: string name: string @@ -15,13 +51,29 @@ export interface Project { folder: Folder } +export type DataAttributes = { + name: string + displayName?: string + versionNumber?: number + fileType?: string +} + export class Data { id: string type: string + attributes: DataAttributes + href: string | undefined + + raw: any constructor(x: any) { this.id = x.id this.type = x.type + this.attributes = x.attributes + + this.raw = x + + this.href = x.relationships?.storage?.meta?.link?.href } } @@ -59,7 +111,7 @@ export class Item extends Data { } export async function getHubs(): Promise { - const auth = APS.getAuth() + const auth = await APS.getAuth() if (!auth) { return undefined } @@ -73,7 +125,7 @@ export async function getHubs(): Promise { }) .then(x => x.json()) .then(x => { - if ((x.data as any[]).length > 0) { + if ((x.data as any[] | undefined)?.length ?? 0 > 0) { return (x.data as any[]).map(y => { return { id: y.id, name: y.attributes.name } }) @@ -83,12 +135,20 @@ export async function getHubs(): Promise { }) } catch (e) { console.error("Failed to get hubs") + console.error(e) + console.log(auth) + console.log(APS.userInfo) + if (e instanceof APSDataError) { + MainHUD_AddToast("error", e.title, e.detail) + } else if (e instanceof Error) { + MainHUD_AddToast("error", "Failed to get hubs.", e.message) + } return undefined } } export async function getProjects(hub: Hub): Promise { - const auth = APS.getAuth() + const auth = await APS.getAuth() if (!auth) { return undefined } @@ -116,12 +176,15 @@ export async function getProjects(hub: Hub): Promise { }) } catch (e) { console.error("Failed to get hubs") + if (e instanceof Error) { + MainHUD_AddToast("error", "Failed to get hubs.", e.message) + } return undefined } } export async function getFolderData(project: Project, folder: Folder): Promise { - const auth = APS.getAuth() + const auth = await APS.getAuth() if (!auth) { return undefined } @@ -157,6 +220,134 @@ export async function getFolderData(project: Project, folder: Folder): Promise encodeURIComponent(`filter[${filter.fieldName}]`) + `=${filter.matchValue}`).join("&") +} + +export async function searchFolder(project: Project, folder: Folder, filters?: Filter[]): Promise { + const auth = await APS.getAuth() + if (!auth) return undefined + let endpoint = `https://developer.api.autodesk.com/data/v1/projects/${project.id}/folders/${folder.id}/search` + if (filters && filters.length > 0) { + endpoint += `?${filterToQuery(filters)}` + } + + const res = await fetch(endpoint, { + method: "GET", + headers: { + Authorization: `Bearer ${auth.access_token}`, + }, + }) + if (!res.ok) { + MainHUD_AddToast("error", "Error getting cloud files.", "Please sign in again.") + return [] + } + const json = await res.json() + return json.data.map((data: any) => new Data(data)) +} + +export async function searchRootForMira(project: Project): Promise { + return searchFolder(project, project.folder, [{ fieldName: "fileType", matchValue: "mira" }]) +} + +export async function downloadData(data: Data): Promise { + if (!data.href) { return undefined } + + const auth = await APS.getAuth() + if (!auth) { + return undefined + } + + return await fetch(data.href, { + method: "GET", + headers: { + Authorization: `Bearer ${auth.access_token}`, + }, + }).then(x => x.arrayBuffer()) +} + +export function HasMirabufFiles(): boolean { + return mirabufFiles != undefined +} + +export async function RequestMirabufFiles() { + console.log("Request") + + if (mirabufFilesMutex.isLocked()) { + return + } + + mirabufFilesMutex.runExclusive(async () => { + const auth = await APS.getAuth() + if (auth) { + getHubs().then(async hubs => { + if (!hubs) { + window.dispatchEvent( + new MirabufFilesStatusUpdateEvent({ isDone: true, message: "Failed to get Hubs" }) + ) + return + } + const fileData: Data[] = [] + for (const hub of hubs) { + const projects = await getProjects(hub) + if (!projects) continue + for (const project of projects) { + window.dispatchEvent( + new MirabufFilesStatusUpdateEvent({ + isDone: false, + message: `Searching Project '${project.name}'`, + }) + ) + const data = await searchRootForMira(project) + if (data) fileData.push(...data) + } + } + window.dispatchEvent( + new MirabufFilesStatusUpdateEvent({ + isDone: true, + message: `Found ${fileData.length} file${fileData.length == 1 ? "" : "s"}`, + }) + ) + mirabufFiles = fileData + window.dispatchEvent(new MirabufFilesUpdateEvent(mirabufFiles)) + }) + } + }) +} + +export function GetMirabufFiles(): Data[] | undefined { + return mirabufFiles +} + +export class MirabufFilesUpdateEvent extends Event { + public static readonly EVENT_KEY: string = "MirabufFilesUpdateEvent" + + public data: Data[] + + public constructor(data: Data[]) { + super(MirabufFilesUpdateEvent.EVENT_KEY) + + this.data = data + } +} + +export class MirabufFilesStatusUpdateEvent extends Event { + public static readonly EVENT_KEY: string = "MirabufFilesStatusUpdateEvent" + + public status: TaskStatus + + public constructor(status: TaskStatus) { + super(MirabufFilesStatusUpdateEvent.EVENT_KEY) + + this.status = status + } } diff --git a/fission/src/mirabuf/MirabufLoader.ts b/fission/src/mirabuf/MirabufLoader.ts index 3b3ecd925b..ce438ec140 100644 --- a/fission/src/mirabuf/MirabufLoader.ts +++ b/fission/src/mirabuf/MirabufLoader.ts @@ -1,3 +1,4 @@ +import { Data, downloadData } from "@/aps/APSDataManagement" import { mirabuf } from "@/proto/mirabuf" import Pako from "pako" @@ -14,6 +15,11 @@ export interface MirabufCacheInfo { thumbnailStorageID?: string } +export interface MirabufRemoteInfo { + displayName: string + src: string +} + type MiraCache = { [id: string]: MirabufCacheInfo } const robotsDirName = "Robots" @@ -68,12 +74,14 @@ class MirabufCachingService { * * @returns {Promise} Promise with the result of the promise. Metadata on the mirabuf file if successful, undefined if not. */ - public static async CacheRemote(fetchLocation: string, miraType: MiraType): Promise { - const map = MirabufCachingService.GetCacheMap(miraType) - const target = map[fetchLocation] + public static async CacheRemote(fetchLocation: string, miraType?: MiraType): Promise { + if (miraType) { + const map = MirabufCachingService.GetCacheMap(miraType) + const target = map[fetchLocation] - if (target) { - return target + if (target) { + return target + } } // Grab file remote @@ -83,6 +91,28 @@ class MirabufCachingService { return await MirabufCachingService.StoreInCache(fetchLocation, miraBuff, miraType) } + public static async CacheAPS(data: Data, miraType: MiraType): Promise { + if (!data.href) { + console.error("Data has no href") + return undefined + } + + const map = MirabufCachingService.GetCacheMap(miraType) + const target = map[data.id] + + if (target) { + return target + } + + const miraBuff = await downloadData(data) + if (!miraBuff) { + console.error("Failed to download file") + return undefined + } + + return await MirabufCachingService.StoreInCache(data.id, miraBuff, miraType) + } + /** * Cache local Mirabuf file * @@ -206,7 +236,14 @@ class MirabufCachingService { */ public static async Remove(key: string, id: MirabufCacheID, miraType: MiraType): Promise { try { - window.localStorage.removeItem(key) + const map = this.GetCacheMap(miraType) + if (map) { + delete map[key] + window.localStorage.setItem( + miraType == MiraType.ROBOT ? robotsDirName : fieldsDirName, + JSON.stringify(map) + ) + } const dir = miraType == MiraType.ROBOT ? robotFolderHandle : fieldFolderHandle await dir.removeEntry(id) @@ -237,12 +274,17 @@ class MirabufCachingService { private static async StoreInCache( key: string, miraBuff: ArrayBuffer, - miraType: MiraType, + miraType?: MiraType, name?: string ): Promise { // Store in OPFS const backupID = Date.now().toString() try { + if (!miraType) { + console.log("Double loading") + miraType = this.AssemblyFromBuffer(miraBuff).dynamic ? MiraType.ROBOT : MiraType.FIELD + } + const fileHandle = await (miraType == MiraType.ROBOT ? robotFolderHandle : fieldFolderHandle).getFileHandle( backupID, { create: true } @@ -282,8 +324,8 @@ class MirabufCachingService { } export enum MiraType { - ROBOT, - FIELD, + ROBOT = 1, + FIELD = 2, } export default MirabufCachingService diff --git a/fission/src/ui/ThemeContext.tsx b/fission/src/ui/ThemeContext.tsx index ef33d33fac..233907d172 100644 --- a/fission/src/ui/ThemeContext.tsx +++ b/fission/src/ui/ThemeContext.tsx @@ -51,6 +51,11 @@ export const colorNameToProp = (colorName: ColorName) => { .toLowerCase() ) } + +export const colorNameToVar = (colorName: ColorName) => { + return `var(${colorNameToProp(colorName)})` +} + export type Theme = { [name in ColorName]: { color: RgbaColor; above: (ColorName | string)[] } } diff --git a/fission/src/ui/ToastContext.tsx b/fission/src/ui/ToastContext.tsx index a6ee866b88..41dcd6c058 100644 --- a/fission/src/ui/ToastContext.tsx +++ b/fission/src/ui/ToastContext.tsx @@ -80,7 +80,7 @@ export const ToastContainer: React.FC = () => { key={t.id} className="w-fit" > - + ))} diff --git a/fission/src/ui/components/Button.tsx b/fission/src/ui/components/Button.tsx index 3d18c026ad..3f6f5ada7e 100644 --- a/fission/src/ui/components/Button.tsx +++ b/fission/src/ui/components/Button.tsx @@ -1,4 +1,4 @@ -import React from "react" +import React, { ReactNode } from "react" import { Button as BaseButton } from "@mui/base/Button" export enum ButtonSize { @@ -8,32 +8,35 @@ export enum ButtonSize { XL, } -type ButtonProps = { - value: string +export type ButtonProps = { + value: ReactNode colorOverrideClass?: string + sizeOverrideClass?: string size?: ButtonSize onClick?: () => void className?: string } -const Button: React.FC = ({ value, colorOverrideClass, size, onClick, className }) => { - let sizeClassNames +const Button: React.FC = ({ value, colorOverrideClass, sizeOverrideClass, size, onClick, className }) => { + let sizeClassNames = sizeOverrideClass if (!size) size = ButtonSize.Medium as ButtonSize - switch (size) { - case ButtonSize.Small: - sizeClassNames = "px-2 py-1" - break - case ButtonSize.Medium: - sizeClassNames = "px-4 py-1" - break - case ButtonSize.Large: - sizeClassNames = "px-8 py-2" - break - case ButtonSize.XL: - sizeClassNames = "px-10 py-2" - break + if (!sizeClassNames) { + switch (size) { + case ButtonSize.Small: + sizeClassNames = "px-4 py-1" + break + case ButtonSize.Medium: + sizeClassNames = "px-6 py-1.5" + break + case ButtonSize.Large: + sizeClassNames = "px-8 py-2" + break + case ButtonSize.XL: + sizeClassNames = "px-10 py-2" + break + } } return ( @@ -43,7 +46,7 @@ const Button: React.FC = ({ value, colorOverrideClass, size, onClic colorOverrideClass ? colorOverrideClass : "bg-gradient-to-r from-interactive-element-left via-interactive-element-right to-interactive-element-left bg-[length:200%_100%] active:bg-right" - } w-full h-full ${sizeClassNames} rounded-sm font-semibold cursor-pointer duration-200 border-none focus-visible:outline-0 focus:outline-0 ${ + } w-fit h-fit ${sizeClassNames} rounded-sm font-semibold cursor-pointer duration-200 border-none focus-visible:outline-0 focus:outline-0 ${ className || "" }`} > diff --git a/fission/src/ui/components/MainHUD.tsx b/fission/src/ui/components/MainHUD.tsx index 2827c61129..48f4eeb917 100644 --- a/fission/src/ui/components/MainHUD.tsx +++ b/fission/src/ui/components/MainHUD.tsx @@ -95,7 +95,7 @@ const MainHUD: React.FC = () => { value={"Spawn Asset"} icon={} larger={true} - onClick={() => openModal("spawning")} + onClick={() => openPanel("import-mirabuf")} />
{ onClick={() => openModal("change-inputs")} /> } onClick={() => openPanel("multibot")} /> - } - onClick={() => openModal("import-mirabuf")} - /> } @@ -130,7 +125,9 @@ const MainHUD: React.FC = () => { } - onClick={() => APS.isSignedIn() && APS.refreshAuthToken(APS.getAuth()!.refresh_token)} + onClick={async () => + APS.isSignedIn() && APS.refreshAuthToken((await APS.getAuth())!.refresh_token, true) + } /> = ({ children, name, + modalId, icon, onCancel, onMiddle, @@ -51,10 +52,11 @@ const Modal: React.FC = ({ const iconEl: ReactNode = typeof icon === "string" ? Icon : icon return ( - closeModal()}> + closeModal()} key={modalId}>
{name && ( diff --git a/fission/src/ui/components/Panel.tsx b/fission/src/ui/components/Panel.tsx index bf0703ffce..b8faee4977 100644 --- a/fission/src/ui/components/Panel.tsx +++ b/fission/src/ui/components/Panel.tsx @@ -125,7 +125,7 @@ const Panel: React.FC = ({ return (
@@ -138,13 +138,16 @@ const Panel: React.FC = ({
{children}
{(cancelEnabled || middleEnabled || acceptEnabled) && ( -