Files
zeekay 1b367e2b62 chore: rebase the tree onto upstream master
The tree was a debranded snapshot of upstream taken around 2026-02-20, and
upstream has moved about five months since. This replaces it with upstream
master, mechanically re-derived: bin/debrand does the rename, and the
pipeline and squashed migrations are restored from the shipped tree.

Not a merge. The previous import replaced the tree wholesale in a single
commit, so there is no shared content history to merge against. Reproducing
the rename on the new tree is what actually converges.

What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints,
compose files, LLM.md, and insights/migrations. The squashed initial
migration is load-bearing — the live database records it by name, so
upstream's ~918 individual migrations cannot come back.

Nine products are dropped as unreferenced. Twenty-one more that we do not run
are kept anyway because the core app still imports them; unpicking those is a
refactor and does not belong in a rebase.

This base does not build yet, and the reasons are not in the rename:
upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against
our 4.2, and the dependency manifests still name upstream packages that we
republish under our own names. Those are decisions, not renames.
2026-08-01 15:44:40 -07:00

183 lines
7.7 KiB
TypeScript

import { useActions, useValues } from 'kea'
import { IconPencil } from '@hanzo/icons'
import { Banner, Button } from '@hanzo/elements'
import { NotFound } from 'lib/components/NotFound'
import { useFeatureFlag } from 'lib/hooks/useFeatureFlag'
import { Spinner } from 'lib/elements/Spinner/Spinner'
import { SceneExport } from 'scenes/sceneTypes'
import { urls } from 'scenes/urls'
import { SceneTitleSection } from '~/layout/scenes/components/SceneTitleSection'
import { StreamlitAppLoading } from './StreamlitAppLoading'
import { StreamlitAppLogicProps, streamlitAppLogic } from './streamlitAppLogic'
export const scene: SceneExport = {
component: StreamlitAppViewer,
logic: streamlitAppLogic,
paramsToProps: ({ params: { id } }): StreamlitAppLogicProps => ({
shortId: id as string,
}),
}
const GENERIC_ERROR_MESSAGE = 'Something went wrong starting your app. Check the logs or contact support.'
// Keeps raw stack traces and DB errors out of the viewer UI.
const ERROR_PREFIX_MESSAGES: Array<[string, string]> = [
[
'Max restart count',
"We've hit the restart limit for this app. Try editing the source and uploading a new version.",
],
['Auth proxy failed to become ready', "The app's auth proxy didn't start in time. Try restarting."],
['Startup timed out', 'Startup timed out. Try restarting.'],
['Sandbox terminated', 'The sandbox stopped unexpectedly. Restart to try again.'],
['No active version', 'No version uploaded yet. Upload a zip file from the edit page.'],
]
function curateErrorMessage(raw?: string | null): string {
if (!raw) {
return GENERIC_ERROR_MESSAGE
}
for (const [prefix, friendly] of ERROR_PREFIX_MESSAGES) {
if (raw.startsWith(prefix)) {
return friendly
}
}
return GENERIC_ERROR_MESSAGE
}
export function StreamlitAppViewer(props: Record<string, any>): JSX.Element {
const streamlitAppsFeatureFlagEnabled = useFeatureFlag('STREAMLIT_APPS')
const shortId = props.id as string
const { streamlitApp, streamlitAppLoading, appStatus, iframeSrc, sandboxStatus, connectError } = useValues(
streamlitAppLogic({ shortId })
)
const { startApp, restartApp, loadConnectInfo } = useActions(streamlitAppLogic({ shortId }))
if (!streamlitAppsFeatureFlagEnabled) {
return <NotFound object="page" />
}
if (streamlitAppLoading && !streamlitApp) {
return (
<div className="flex items-center justify-center py-20">
<Spinner className="text-4xl" />
</div>
)
}
if (!streamlitApp) {
return (
<div className="flex flex-col items-center justify-center py-20 text-center">
<h2 className="text-lg font-semibold mb-2">App not found</h2>
<p className="text-muted mb-4">This Streamlit app doesn't exist or you don't have access to it.</p>
<Button type="primary" to={urls.streamlitApps()}>
Back to apps
</Button>
</div>
)
}
return (
<div className="flex flex-col h-full">
<SceneTitleSection
name={streamlitApp.name}
resourceType={{ type: 'streamlit_app' }}
actions={
<Button
type="secondary"
size="small"
icon={<IconPencil />}
to={urls.streamlitAppEdit(streamlitApp.short_id)}
>
Edit
</Button>
}
/>
{/* Nudge on version mismatch between activated and running versions. */}
{appStatus === 'running' &&
streamlitApp.active_version &&
sandboxStatus?.version_number != null &&
streamlitApp.active_version.version_number !== sandboxStatus.version_number && (
<Banner
type="info"
className="mb-2"
action={{
children: 'Restart now',
onClick: () => restartApp(),
}}
>
New version available — restart to apply the changes.
</Banner>
)}
<div className="flex-1 min-h-0">
{appStatus === 'starting' && <StreamlitAppLoading />}
{appStatus === 'stopping' && <StreamlitAppLoading message="Stopping the app..." />}
{appStatus === 'running' && iframeSrc && (
<iframe
src={iframeSrc}
// allow-same-origin lets Streamlit read its own cookies/sessionStorage.
// Insights is a different origin, so the proxy's scope check is the real boundary.
sandbox="allow-scripts allow-forms allow-popups allow-modals allow-same-origin"
referrerPolicy="no-referrer"
className="w-full h-full border-0 rounded-lg"
style={{ minHeight: '600px' }}
title={streamlitApp.name}
/>
)}
{appStatus === 'running' && !iframeSrc && connectError && (
<div className="flex flex-col items-center justify-center py-20 text-center">
<h2 className="text-lg font-semibold mb-2">Connection failed</h2>
<p className="text-muted mb-6">{connectError}</p>
<Button type="primary" onClick={() => loadConnectInfo()}>
Retry
</Button>
</div>
)}
{appStatus === 'running' && !iframeSrc && !connectError && <StreamlitAppLoading />}
{appStatus === 'error' && (
<div className="flex flex-col items-center justify-center py-20 text-center">
<h2 className="text-lg font-semibold mb-2">App failed to start</h2>
<p className="text-muted mb-4 max-w-lg">{curateErrorMessage(sandboxStatus?.last_error)}</p>
<div className="flex gap-2">
<Button type="primary" onClick={restartApp}>
Try again
</Button>
<Button type="secondary" to={urls.streamlitAppEdit(streamlitApp.short_id)}>
View settings
</Button>
</div>
</div>
)}
{appStatus === 'stopped' && !streamlitApp.active_version && (
<div className="flex flex-col items-center justify-center py-20 text-center">
<h2 className="text-lg font-semibold mb-2">No version uploaded</h2>
<p className="text-muted mb-4">Upload a zip file to get started.</p>
<Button type="primary" to={urls.streamlitAppEdit(streamlitApp.short_id)}>
Upload
</Button>
</div>
)}
{appStatus === 'stopped' && streamlitApp.active_version && (
<div className="flex flex-col items-center justify-center py-20 text-center">
<h2 className="text-lg font-semibold mb-2">App is stopped</h2>
<p className="text-muted mb-4">Start the app to view it.</p>
<Button type="primary" onClick={startApp}>
Start app
</Button>
</div>
)}
</div>
</div>
)
}