Skip to content

Commit

Permalink
♻️ Rewrite UI using a hint of typescript, and add select folder
Browse files Browse the repository at this point in the history
  • Loading branch information
cp2004 committed Dec 6, 2021
1 parent 8af8b42 commit 1fedbcd
Show file tree
Hide file tree
Showing 19 changed files with 1,011 additions and 347 deletions.
4 changes: 2 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ trim_trailing_whitespace = true
indent_style = space
indent_size = 4

[**.js]
[**.js|**.jsx|**.ts|**.tsx]
indent_style = space
indent_size = 2
indent_size = 4
7 changes: 6 additions & 1 deletion octoprint_onedrive_backup/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ def send_message(self, msg_type: str, msg_content: dict):

# SettingsPlugin
def get_settings_defaults(self):
return {}
"""
Quite basic settings as the authentication tokens are stored separately, outside config.yaml.
"""
return {
"folder": {"id": "", "path": ""},
}

# AssetPlugin
def get_assets(self):
Expand Down
12 changes: 12 additions & 0 deletions octoprint_onedrive_backup/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ class Commands:
StartAuth = "startAuth"
GetFolders = "folders"
GetFoldersByID = "foldersById"
SetFolder = "setFolder"

@staticmethod
def list_commands():
return {
Commands.StartAuth: [],
Commands.GetFolders: [],
Commands.GetFoldersByID: ["id"],
Commands.SetFolder: ["id", "path"],
}


Expand All @@ -26,6 +28,7 @@ def on_api_get(self, request):
return {
"accounts": self.plugin.onedrive.list_accounts(),
"flow": self.plugin.onedrive.flow_in_progress,
"folder": self.plugin._settings.get(["folder"], merged=True),
}

def on_api_command(self, command, data):
Expand Down Expand Up @@ -57,3 +60,12 @@ def on_api_command(self, command, data):
folders = self.plugin.onedrive.list_folders(item_id)

return folders

if command == Commands.SetFolder:
folder_id = data.get("id")
folder_path = data.get("path")

self.plugin._settings.set(["folder", "id"], folder_id)
self.plugin._settings.set(["folder", "path"], folder_path)

self.plugin._settings.save()
5 changes: 4 additions & 1 deletion octoprint_onedrive_backup/onedrive.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ def list_folders(self, item_id=None):
"id": item["id"],
"parent": item["parentReference"]["id"],
"childCount": item["folder"]["childCount"],
"path": item["parentReference"]["path"].split("/root:")[1]
+ "/"
+ item["name"], # Human readable path
}
)
else:
Expand All @@ -95,7 +98,7 @@ def _get_headers(self):
) # TODO select active account

if token is None:
# Auth failed, do something about it
# Auth failed, do something about it TODO - when requested too fast, this seems to fail sometimes.
return {}

return {"Authorization": f"Bearer {token['access_token']}"}
Expand Down
45 changes: 45 additions & 0 deletions octoprint_onedrive_backup/static/src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import * as React from "react"

import FileBrowser from "./FileBrowser"
import Footer from "./Footer"
import {QueryClient, QueryClientProvider} from "react-query";
import { ReactQueryDevtools } from 'react-query/devtools'
import Auth from "./Auth";

// @ts-ignore:next-line
const OctoPrint = window.OctoPrint
const PLUGIN_ID = "onedrive_backup"

const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
}
}
})

export default function Index() {
return (
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false}/>
<App />
</QueryClientProvider>
)
}

function App () {
// TODO error boundary
return (
<>
<h5>OneDrive Backup Plugin</h5>

<Auth />

<hr />

<FileBrowser />

<Footer />
</>
)
}
106 changes: 106 additions & 0 deletions octoprint_onedrive_backup/static/src/components/Auth.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as React from "react"
import {useQuery} from "react-query";
import useSocket from "../hooks/useSocket";

// @ts-ignore:next-line
const OctoPrint = window.OctoPrint
const PLUGIN_ID = "onedrive_backup"

interface AuthData {
url: string,
code: string
}

interface PluginSocketMessage {
data: {
plugin: string;
data: {
type: string;
content: object;
}
};
}

interface AuthProps {
accounts: string[];
}

export default function Auth () {
const [authSuccess, setAuthSuccess] = React.useState<boolean>(false)
const [authLoading, setAuthLoading] = React.useState<boolean>(false)

const {data, isLoading, error, refetch} = useQuery(
"accounts",
() => {
return OctoPrint.simpleApiGet(PLUGIN_ID)
}
)

const hasAccount = Boolean(data?.accounts.length)
const addingAccount = data?.flow

const accountsList = hasAccount ? data.accounts.map(account => (
<li key={account}>{account}</li>
)) : []

useSocket("plugin", (message) => {
const plugin = message.data.plugin
if (plugin === "onedrive_backup") {
const type = message.data.data.type
if (type === "auth_done") {
setAuthSuccess(true)
// Rerun query to make new data show up
refetch()
}
}
})

const addAccount = () => {
setAuthLoading(true)
OctoPrint.simpleApiCommand(PLUGIN_ID, "startAuth").done((data) => {
// The parameters are also passed through `data` here, but refetching the original query is less code
refetch()
setAuthLoading(false)
})
}

const loading = isLoading || authLoading

return (
<>
{hasAccount
? <>
<p>Account registered:</p>
<ul>{accountsList}</ul>
</>
: <p>
No Microsoft accounts registered, add one below
</p>
}

<div className={"row-fluid"} >
<button className={"btn btn-success"} onClick={addAccount}>
<i className={"fas fa-fw " + (loading ? "fa-spin fa-spinner" : hasAccount ? "fa-user-edit" : "fa-user-plus" )} />
{" "}{hasAccount ? "Change Account" : "Add account"}
</button>
</div>

{addingAccount && <div className={"row-fluid"}>
<p style={{marginTop: "10px"}}>
Head to <a href={data.flow.verification_uri} target={"_blank"} rel={"noreferrer"}>{data.flow.verification_uri}</a> and enter code
{" "}<code>{data.flow.user_code}</code> to connect your Microsoft account
</p>
</div>
}

{authSuccess && <div className={"alert alert-success"}>
<p>
<strong>Success! </strong>
Your account has been successfully added to the plugin.
Make sure to configure the path to upload your backups to below.
</p>
</div>
}
</>
)
}
133 changes: 133 additions & 0 deletions octoprint_onedrive_backup/static/src/components/FileBrowser.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import * as React from "react"
import { Alert } from "./bootstrap/"
import {useQuery} from "react-query";

//@ts-ignore:next-line
const OctoPrint = window.OctoPrint

export default function FileBrowser () {
const [active, setActive] = React.useState<boolean>(false)
const [currentFolder, setCurrentFolder] = React.useState<string>("root")

const [history, setHistory] = React.useState<string[]>([])
const [historyPos, setHistoryPos] = React.useState<number>(0)

const fetchFiles = (itemId: string = "root") => {
if (itemId === "root") {
return OctoPrint.simpleApiCommand("onedrive_backup", "folders")
} else {
return OctoPrint.simpleApiCommand("onedrive_backup", "foldersById", { id: itemId })
}
}

const {data, isLoading, error: queryError} = useQuery(
["folders", currentFolder],
() => fetchFiles(currentFolder),
{
enabled: active,
}
)

// TODO - make sure no duplicate network requests, with the auth component
const {data: configData, isLoading: configDataLoading, refetch: refetchConfig} = useQuery(
"accounts",
() => {
return OctoPrint.simpleApiGet("onedrive_backup")
}
)

const handleSelectFolder = (target) => {
setHistory(prevState => prevState.concat([currentFolder]))
setCurrentFolder(target)
setHistoryPos(prevState => prevState + 1)
}

const handleBack = () => {
if (history.length >= 0 && historyPos > 0) {
const newHistoryPos = historyPos - 1
const newFolder = history[newHistoryPos]
setHistoryPos(newHistoryPos)
setCurrentFolder(newFolder)
setHistory(prevState => prevState.slice(0, newHistoryPos))
}
}

const handleActivateFolder = (folder) => {
OctoPrint.simpleApiCommand("onedrive_backup", "setFolder", { id: folder.id, path: folder.path }).done(
() => refetchConfig()
)
}

const files = data?.folders ? data.folders.map(folder => (
<tr key={folder.id}>
<td>
{folder.childCount > 0 ?
<a onClick={() => handleSelectFolder(folder.id)} style={{ cursor: "pointer" }}>
<i className={"far fa-folder-open"}/> {folder.name}
</a>
: <span><i className={"far fa-folder-open"}/> {folder.name}</span>
}
</td>
<td>
<button className={"btn btn-primary btn-mini"} onClick={() => handleActivateFolder(folder)}>
Set upload destination
</button>
</td>
</tr>
)) : []

const hasError = queryError || data?.error
const loading = isLoading || configDataLoading

return (
<>
{
configDataLoading
? <span><i className={"fas fa-spin fa-spinner"} /> Loading...</span>
: <span>Currently configured upload destination: {configData?.folder.path ? <code>{configData.folder.path}</code> : "None"}</span>
}
{hasError &&
<Alert variant={"error"}>
<i className={"fas fa-times text-error"} /><strong> Error:</strong>
{typeof data.error === "string" ? data.error : "Unknown error. Check octoprint.log for details."}
</Alert>}
{active
? (
<table className={"table"}>
<thead>
<tr>
<th>Folder name {loading && <i className={"fas fa-spin fa-spinner"} />} </th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{historyPos > 0 &&
<tr>
<td>
<span onClick={handleBack} style={{ cursor: "pointer" }}><i className={"fas fa-arrow-left"} /> Back</span>
</td>
<td/>
</tr>}
{files.length ? files : (
<>
{!isLoading && !hasError && (
<tr>
<td>
<i className={"fas fa-times"} /> No sub-folders found
</td>
</tr>
)}
</>
)}
</tbody>
</table>
) :
<div className={"row-fluid"}>
<button className={"btn btn-primary"} onClick={() => setActive(true)}>
<i className={"fa-fw " + (isLoading ? "fas fa-spin fa-spinner" : "far fa-folder-open")}/> Change folder
</button>
</div>
}
</>
)
}
Loading

0 comments on commit 1fedbcd

Please sign in to comment.