Developer Mode (#190)

* feat: adding developer mode toggle to add and display plugins

* chore: lint

* chore: cleanup
This commit is contained in:
Tanner
2025-06-26 17:54:22 +01:00
committed by GitHub
parent 5a9e5bc77d
commit ea12322686
5 changed files with 141 additions and 6 deletions

View File

@@ -5,7 +5,12 @@ import React, {
useEffect,
useState,
} from 'react';
import { fetchPluginHashes, removePlugin, runPlugin } from '../../utils/rpc';
import {
fetchPluginHashes,
removePlugin,
runPlugin,
addPlugin,
} from '../../utils/rpc';
import { usePluginHashes } from '../../reducers/plugins';
import {
getPluginConfig,
@@ -31,20 +36,78 @@ export function PluginList({
className,
unremovable,
onClick,
showAddButton = false,
}: {
className?: string;
unremovable?: boolean;
onClick?: (hash: string) => void;
showAddButton?: boolean;
}): ReactElement {
const hashes = usePluginHashes();
const [uploading, setUploading] = useState(false);
useEffect(() => {
fetchPluginHashes();
}, []);
const handleFileUpload = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.name.endsWith('.wasm')) {
alert('Please select a .wasm file');
return;
}
setUploading(true);
try {
const arrayBuffer = await file.arrayBuffer();
const hex = Buffer.from(arrayBuffer).toString('hex');
const url = `file://${file.name}`;
await addPlugin(hex, url);
await fetchPluginHashes();
} catch (error: any) {
alert(`Failed to add plugin: ${error.message}`);
} finally {
setUploading(false);
e.target.value = '';
}
},
[],
);
return (
<div className={classNames('flex flex-col flex-nowrap gap-1', className)}>
{!hashes.length && (
{showAddButton && (
<div className="relative">
<input
type="file"
accept=".wasm"
onChange={handleFileUpload}
disabled={uploading}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10"
/>
<button
className="flex flex-row items-center justify-center gap-2 p-3 border-2 border-dashed border-slate-300 rounded-lg text-slate-500 hover:text-slate-700 hover:border-slate-400 transition-colors cursor-pointer w-full"
disabled={uploading}
>
{uploading ? (
<>
<Icon fa="fa-solid fa-spinner" className="animate-spin" />
<span>Adding Plugin...</span>
</>
) : (
<>
<Icon fa="fa-solid fa-plus" />
<span>Add Plugin (.wasm file)</span>
</>
)}
</button>
</div>
)}
{!hashes.length && !showAddButton && (
<div className="flex flex-col items-center justify-center text-slate-400 cursor-default select-none">
<div>No available plugins</div>
</div>

View File

@@ -44,8 +44,9 @@ export default function SidePanel(): ReactElement {
switch (type) {
case SidePanelActionTypes.execute_plugin_request: {
setConfig(await getPluginConfigByUrl(data.pluginUrl));
setUrl(data.pluginUrl);
const pluginIdentifier = data.pluginUrl || data.pluginHash;
setConfig(await getPluginConfigByUrl(pluginIdentifier));
setUrl(pluginIdentifier);
setParams(data.pluginParams);
setStarted(true);
break;

View File

@@ -11,17 +11,23 @@ import History from '../History';
import './index.scss';
import Requests from '../Requests';
import { fetchPluginHashes } from '../../utils/rpc';
import { PluginList } from '../../components/PluginList';
import { getDeveloperMode } from '../../utils/storage';
export default function Home(props: {
tab?: 'history' | 'network';
}): ReactElement {
const [error, showError] = useState('');
const [tab, setTab] = useState<'history' | 'network'>(props.tab || 'history');
const [tab, setTab] = useState<'history' | 'network' | 'plugins'>(
props.tab || 'history',
);
const scrollableContent = useRef<HTMLDivElement | null>(null);
const [shouldFix, setFix] = useState(false);
const [developerMode, setDeveloperMode] = useState(false);
useEffect(() => {
fetchPluginHashes();
getDeveloperMode().then(setDeveloperMode);
}, []);
useEffect(() => {
@@ -71,10 +77,24 @@ export default function Home(props: {
>
History
</TabSelector>
{developerMode && (
<TabSelector
onClick={() => setTab('plugins')}
selected={tab === 'plugins'}
>
Plugins
</TabSelector>
)}
</div>
<div className="flex-grow">
{tab === 'history' && <History />}
{tab === 'network' && <Requests shouldFix={shouldFix} />}
{tab === 'plugins' && (
<PluginList
className="p-2 overflow-y-auto"
showAddButton={developerMode}
/>
)}
</div>
</div>
);

View File

@@ -19,6 +19,8 @@ import {
LOGGING_FILTER_KEY,
getRendezvousApi,
RENDEZVOUS_API_LS_KEY,
getDeveloperMode,
DEVELOPER_MODE_LS_KEY,
} from '../../utils/storage';
import {
EXPLORER_API,
@@ -41,6 +43,7 @@ export default function Options(): ReactElement {
const [maxReceived, setMaxReceived] = useState(MAX_RECV);
const [loggingLevel, setLoggingLevel] = useState<LoggingLevel>('Info');
const [rendezvous, setRendezvous] = useState(RENDEZVOUS_API);
const [developerMode, setDeveloperMode] = useState(false);
const [dirty, setDirty] = useState(false);
const [shouldReload, setShouldReload] = useState(false);
@@ -61,6 +64,7 @@ export default function Options(): ReactElement {
(async () => {
setNotary(await getNotaryApi());
setProxy(await getProxyApi());
setDeveloperMode(await getDeveloperMode());
})();
}, []);
@@ -85,6 +89,7 @@ export default function Options(): ReactElement {
await set(MAX_RECEIVED_LS_KEY, maxReceived.toString());
await set(LOGGING_FILTER_KEY, loggingLevel);
await set(RENDEZVOUS_API_LS_KEY, rendezvous);
await set(DEVELOPER_MODE_LS_KEY, developerMode.toString());
setDirty(false);
},
[
@@ -94,6 +99,7 @@ export default function Options(): ReactElement {
maxReceived,
loggingLevel,
rendezvous,
developerMode,
shouldReload,
],
);
@@ -157,6 +163,8 @@ export default function Options(): ReactElement {
proxy={proxy}
setProxy={setProxy}
setDirty={setDirty}
developerMode={developerMode}
setDeveloperMode={setDeveloperMode}
/>
<div className="justify-left px-2 pt-3 gap-2">
<button className="font-bold" onClick={onAdvanced}>
@@ -255,8 +263,18 @@ function NormalOptions(props: {
proxy: string;
setProxy: (value: string) => void;
setDirty: (value: boolean) => void;
developerMode: boolean;
setDeveloperMode: (value: boolean) => void;
}) {
const { notary, setNotary, proxy, setProxy, setDirty } = props;
const {
notary,
setNotary,
proxy,
setProxy,
setDirty,
developerMode,
setDeveloperMode,
} = props;
return (
<div>
@@ -288,6 +306,33 @@ function NormalOptions(props: {
<div className="font-semibold">Explorer URL</div>
<div className="input border bg-slate-100">{EXPLORER_API}</div>
</div>
<div className="flex flex-row items-center py-3 px-2 gap-2">
<div className="font-semibold">Developer Mode</div>
<div className="relative inline-block w-9 h-5">
<input
type="checkbox"
id="developer-mode"
checked={developerMode}
onChange={(e) => {
setDeveloperMode(e.target.checked);
setDirty(true);
}}
className="sr-only"
/>
<label
htmlFor="developer-mode"
className={`block h-5 rounded-full cursor-pointer transition-all duration-300 ease-in-out ${
developerMode ? 'bg-blue-500' : 'bg-gray-300'
}`}
>
<span
className={`absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow-sm transform transition-all duration-300 ease-in-out ${
developerMode ? 'translate-x-4' : 'translate-x-0'
}`}
/>
</label>
</div>
</div>
</div>
);
}

View File

@@ -8,6 +8,7 @@ export const MAX_SENT_LS_KEY = 'max-sent';
export const MAX_RECEIVED_LS_KEY = 'max-received';
export const LOGGING_FILTER_KEY = 'logging-filter-2';
export const RENDEZVOUS_API_LS_KEY = 'rendezvous-api';
export const DEVELOPER_MODE_LS_KEY = 'developer-mode';
export async function set(key: string, value: string) {
return chrome.storage.sync.set({ [key]: value });
@@ -43,3 +44,8 @@ export async function getLoggingFilter(): Promise<LoggingLevel> {
export async function getRendezvousApi(): Promise<string> {
return await get(RENDEZVOUS_API_LS_KEY, RENDEZVOUS_API);
}
export async function getDeveloperMode(): Promise<boolean> {
const value = await get(DEVELOPER_MODE_LS_KEY, 'false');
return value === 'true';
}