Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes to AppWindow #65

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
32 changes: 32 additions & 0 deletions backend/internal/compile/client.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,44 @@
window.addEventListener('error', (event) => {
window.parent.postMessage(
{
source: 'robin-platform',
type: 'appError',
error: String(event.error?.stack || event.error),
},
'*',
);
});

(() => {
let lastLocation = null;
let lastTitle = null;

setInterval(() => {
if (lastLocation !== window.location.href) {
window.parent.postMessage(
{
source: 'robin-platform',
type: 'locationUpdate',
location: window.location.href,
},
'*',
);
lastLocation = window.location.href;
}

if (lastTitle !== document.title) {
window.parent.postMessage(
{
source: 'robin-platform',
type: 'titleUpdate',
title: document.title,
},
'*',
);
lastReportedTitle = document.title;
}
}, 250);
})();
</script>
<script>
{{.ScriptSource}}
Expand Down
50 changes: 29 additions & 21 deletions example/my-ext/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { getAppSettings } from '@robinplatform/toolkit';
import { renderApp } from '@robinplatform/toolkit/react';
import { useRpcQuery } from '@robinplatform/toolkit/react/rpc';
import React from 'react';
import { getSelfSource } from './page.server';
import '@robinplatform/toolkit/styles.css';
import './ext.scss';
import { z } from 'zod';
Expand All @@ -12,29 +11,38 @@ function Page() {
getAppSettings,
z.object({ filename: z.string().optional() }),
);
const { data, error: errFetchingFile } = useRpcQuery(
getSelfSource,
{
filename: String(settings?.filename ?? './package.json'),
},
{
enabled: !!settings,
},
);

const error = errFetchingSettings || errFetchingFile;
const error = errFetchingSettings;

return (
<pre
style={{
margin: '1rem',
padding: '1rem',
background: '#e3e3e3',
borderRadius: 'var(--robin-border-radius)',
}}
>
<code>{error ? String(error) : data ? String(data) : 'Loading ...'}</code>
</pre>
<div>
<div>
LOCATION: {String(window.location.href)}
<a href="./blahblah">Link</a>
<button
onClick={() => window.history.pushState(null, '', './blahblah2')}
>
History Change
</button>
</div>

<pre
style={{
margin: '1rem',
padding: '1rem',
background: '#e3e3e3',
borderRadius: 'var(--robin-border-radius)',
}}
>
<code>
{error
? JSON.stringify(error)
: settings
? JSON.stringify(settings, undefined, 2)
: 'Loading ...'}
</code>
</pre>
</div>
);
}

Expand Down
3 changes: 2 additions & 1 deletion example/robin.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"name": "example",
"apps": [
"./my-ext/robin.app.json",
"./bad-js-ext/robin.app.json",
"./failing-ext/robin.app.json",
"https://esm.sh/@robinplatform/[email protected]"
]
}
}
3 changes: 0 additions & 3 deletions frontend/components/AppToolbar.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@

.toolbar {
padding: 0.5rem;
margin: 1.25rem;
margin-bottom: 0;

border-radius: 0.25rem;
background: $dark-blue;

display: flex;
Expand Down
100 changes: 78 additions & 22 deletions frontend/components/AppWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import styles from './AppToolbar.module.scss';
type AppWindowProps = {
id: string;
setTitle: React.Dispatch<React.SetStateAction<string>>;
route: string;
setRoute: (route: string) => void;
};

const RestartAppButton: React.FC = () => {
Expand Down Expand Up @@ -54,50 +56,99 @@ const RestartAppButton: React.FC = () => {
);
};

function AppWindowContent({ id, setTitle }: AppWindowProps) {
const router = useRouter();

// NOTE: Changes to the route here will create an additional history entry.
function AppWindowContent({ id, setTitle, route, setRoute }: AppWindowProps) {
const iframeRef = React.useRef<HTMLIFrameElement | null>(null);
const [error, setError] = React.useState<string | null>(null);
const subRoute = React.useMemo(
() =>
router.isReady
? router.asPath.substring('/app/'.length + id.length)
: null,
[router.isReady, router.asPath, id],
);
const mostCurrentRouteRef = React.useRef<string>(route);
const mostCurrentLocationUpdateRef = React.useRef<string | null>(null);

React.useEffect(() => {
mostCurrentRouteRef.current = route;
}, [route]);

React.useEffect(() => {
if (!iframeRef.current) {
return;
}

const target = `http://localhost:9010/api/app-resources/${id}/base${route}`;
if (target === mostCurrentLocationUpdateRef.current) {
return;
}

if (iframeRef.current.src !== target) {
console.log('switching to', target, 'from', iframeRef.current.src);
iframeRef.current.src = target;
}
}, [id, route]);

React.useEffect(() => {
const onMessage = (message: MessageEvent) => {
try {
if (message.data.source !== 'robin-platform') {
// e.g. react-dev-tools uses iframe messages, so we shouldn't
// handle them.
return;
}

switch (message.data.type) {
case 'locationUpdate': {
const location = {
pathname: window.location.pathname,
search: new URL(message.data.location).search,
};
router.push(location, undefined, { shallow: true });
const location = message.data.location;
if (!location || typeof location !== 'string') {
break;
}

console.log('received location update', location);

const url = new URL(location);
const newRoute = url.pathname.substring(
`/api/app-resources/${id}/base`.length,
);

const currentRoute = mostCurrentRouteRef.current;
if (newRoute !== currentRoute) {
setRoute(newRoute);
mostCurrentLocationUpdateRef.current = url.href;
}
break;
}

case 'titleUpdate':
setTitle((title) => message.data.title || title);
if (message.data.title) {
setTitle(message.data.title);
}
break;

case 'appError':
setError(message.data.error);
break;

default:
// toast.error(`Unknown app message type: ${message.data.type}`, {
// id: 'unknown-message-type',
// });
console.warn(
`Unknown app message type on message: ${JSON.stringify(
message.data,
)}`,
);
}
} catch {}
} catch (e: any) {
toast.error(
`Error when receiving app message: ${String(e)}\ndata:\n${
message.data
}`,
{ id: 'unknown-message-type' },
);
}
};

window.addEventListener('message', onMessage);
return () => window.removeEventListener('message', onMessage);
}, [router, setTitle]);
}, [id, setTitle, setRoute]);

React.useEffect(() => {
setTitle(id);

if (!iframeRef.current) return;
const iframe = iframeRef.current;

Expand Down Expand Up @@ -150,7 +201,6 @@ function AppWindowContent({ id, setTitle }: AppWindowProps) {

<iframe
ref={iframeRef}
src={`http://localhost:9010/api/app-resources/${id}/base${subRoute}`}
style={{ border: '0', flexGrow: 1, width: '100%', height: '100%' }}
/>
</>
Expand All @@ -162,5 +212,11 @@ function AppWindowContent({ id, setTitle }: AppWindowProps) {
export function AppWindow(props: AppWindowProps) {
const numRestarts = useIsMutating({ mutationKey: ['RestartApp'] });

return <AppWindowContent key={String(props.id) + numRestarts} {...props} />;
return (
<AppWindowContent
key={String(props.id) + numRestarts}
{...props}
route={!!props.route ? props.route : '/'}
/>
);
}
114 changes: 114 additions & 0 deletions frontend/pages/app-settings/[id]/settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { useRouter } from 'next/router';
import { AppToolbar } from '../../../components/AppToolbar';
import { Settings } from '../../../components/Settings';
import { z } from 'zod';
import { useRpcMutation, useRpcQuery } from '../../../hooks/useRpcQuery';
import { Button } from '../../../components/Button';
import { ArrowLeftIcon } from '@primer/octicons-react';
import { toast } from 'react-hot-toast';
import { Alert } from '../../../components/Alert';
import { Spinner } from '../../../components/Spinner';
import { useQueryClient } from '@tanstack/react-query';
import Head from 'next/head';

export default function AppSettings() {
const router = useRouter();
const id = typeof router.query.id === 'string' ? router.query.id : null;

const {
data: appSettings,
error: errLoadingAppSettings,
isLoading,
} = useRpcQuery({
method: 'GetAppSettingsById',
pathPrefix: '/api/apps/rpc',
data: { appId: id },
result: z.record(z.string(), z.unknown()),
});

const queryClient = useQueryClient();
const { mutate: updateAppSettings } = useRpcMutation({
method: 'UpdateAppSettings',
pathPrefix: '/api/apps/rpc',
result: z.record(z.string(), z.unknown()),

onSuccess: () => {
toast.success('Updated app settings');
router.push(`/app/${id}`);
queryClient.invalidateQueries(['GetAppSettingsById']);
},
onError: (err) => {
toast.error(`Failed to update app settings: ${String(err)}`);
},
});

if (!id) {
return null;
}
return (
<>
<Head>
<title>{id} Settings</title>
</Head>

<div className="full">
<AppToolbar
appId={id}
actions={
<>
<Button
size="sm"
variant="primary"
onClick={() => router.push(`/app/${id}`)}
>
<span style={{ marginRight: '.5rem' }}>
<ArrowLeftIcon />
</span>
Back
</Button>
</>
}
/>

<div className={'full robin-pad'}>
<>
{isLoading && (
<div
className="full"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<p style={{ display: 'flex', alignItems: 'center' }}>
<Spinner />
<span style={{ marginLeft: '.5rem' }}>Loading...</span>
</p>
</div>
)}
{errLoadingAppSettings && (
<Alert variant="error" title={'Failed to load app settings'}>
{String(errLoadingAppSettings)}
</Alert>
)}
{appSettings && (
<Settings
schema={z.unknown()}
isLoading={false}
error={undefined}
value={appSettings}
onChange={(value) =>
updateAppSettings({
appId: id,
settings: value,
})
}
/>
)}
</>
</div>
</div>
</>
);
}
Loading