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

244 lines
11 KiB
TypeScript

import { useActions, useValues } from 'kea'
import { IconBug } from '@hanzo/icons'
import { Button, Label, Switch } from '@hanzo/elements'
import { AccessControlAction } from 'lib/components/AccessControlAction'
import { superpowersLogic } from 'lib/components/Superpowers/superpowersLogic'
import { toast } from 'lib/elements/Toast/Toast'
import { getAccessControlDisabledReason } from 'lib/utils/accessControlUtils'
import { objectsEqual } from 'lib/utils/objects'
import { SceneTitleSection } from '~/layout/scenes/components/SceneTitleSection'
import { EndpointRequest } from '~/queries/schema/schema-general'
import { isInsightVizNode } from '~/queries/utils'
import { AccessControlLevel, AccessControlResourceType, EndpointType, EndpointVersionType } from '~/types'
import { endpointLogic } from './endpointLogic'
import { endpointSceneLogic, extractBreakdownPropertyNames } from './endpointSceneLogic'
export const EndpointSceneHeader = (): JSX.Element => {
const {
endpoint,
endpointLoading,
localQuery,
isMaterialized,
viewingVersion,
bucketOverrides,
debugInfoExpanded,
dataFreshness,
optionalBreakdownProperties,
} = useValues(endpointSceneLogic)
const { endpointName, endpointDescription, updatingEndpoint } = useValues(endpointLogic)
const { setEndpointDescription, updateEndpoint } = useActions(endpointLogic)
const {
setLocalQuery,
setDataFreshness,
setIsMaterialized,
resetBucketOverrides,
resetOptionalBreakdownProperties,
setDebugInfoExpanded,
} = useActions(endpointSceneLogic)
const { superpowersEnabled } = useValues(superpowersLogic)
// SceneTitleSection takes a boolean `canEdit` rather than disabled/disabledReason, so we can't
// wrap it with AccessControlAction — use the same helper AccessControlAction relies on internally.
const editAccessDisabledReason = getAccessControlDisabledReason(
AccessControlResourceType.Endpoint,
AccessControlLevel.Editor
)
// When viewing a non-current version, target that version for updates
const targetVersion =
viewingVersion && viewingVersion.version !== endpoint?.current_version ? viewingVersion.version : undefined
const hasNameChange = endpointName && endpointName !== endpoint?.name
// When viewing a version, compare against that version's description
const baseDescription = viewingVersion?.description ?? endpoint?.description
const hasDescriptionChange = endpointDescription !== null && endpointDescription !== baseDescription
const hasQueryChange = localQuery !== null
// When viewing a version, compare against that version's values
const baseDataFreshness = viewingVersion?.data_freshness_seconds ?? endpoint?.data_freshness_seconds ?? 86400
const hasDataFreshnessChange = dataFreshness !== baseDataFreshness
const baseIsMaterialized = viewingVersion?.is_materialized ?? endpoint?.is_materialized
const hasIsMaterializedChange = isMaterialized !== null && isMaterialized !== baseIsMaterialized
const baseBucketOverrides = viewingVersion?.bucket_overrides ?? endpoint?.bucket_overrides ?? {}
const hasBucketOverridesChange = !objectsEqual(bucketOverrides, baseBucketOverrides)
const baseOptionalBreakdowns =
viewingVersion?.optional_breakdown_properties ?? endpoint?.optional_breakdown_properties ?? []
const hasOptionalBreakdownChange = !objectsEqual(
[...optionalBreakdownProperties].sort(),
[...baseOptionalBreakdowns].sort()
)
const hasChanges =
hasNameChange ||
hasDescriptionChange ||
hasQueryChange ||
hasDataFreshnessChange ||
hasIsMaterializedChange ||
hasBucketOverridesChange ||
hasOptionalBreakdownChange
const handleSave = (): void => {
let queryToSave = (localQuery || endpoint?.query) as any
if (queryToSave && isInsightVizNode(queryToSave)) {
queryToSave = queryToSave.source
}
if (!endpoint) {
return
}
// Prune the optional list against the query actually being saved — a query edit can
// remove a breakdown after it was marked optional, and the backend rejects unknown names.
const savedQueryBreakdowns = extractBreakdownPropertyNames(
hasQueryChange ? queryToSave : (viewingVersion?.query ?? endpoint.query)
)
const prunedOptionalBreakdowns = optionalBreakdownProperties.filter((p) => savedQueryBreakdowns.includes(p))
const updatePayload: Partial<EndpointRequest> = {
description: hasDescriptionChange ? endpointDescription : undefined,
data_freshness_seconds: hasDataFreshnessChange ? dataFreshness : undefined,
query: hasQueryChange ? queryToSave : undefined,
is_materialized: hasIsMaterializedChange ? isMaterialized : undefined,
bucket_overrides: hasBucketOverridesChange ? bucketOverrides : undefined,
optional_breakdown_properties: hasOptionalBreakdownChange ? prunedOptionalBreakdowns : undefined,
}
updateEndpoint(endpoint.name, updatePayload, targetVersion ? { version: targetVersion } : undefined)
}
const handleDiscardChanges = (): void => {
if (!endpoint) {
return
}
// Reset to viewed version values if viewing a specific version
const sourceDescription = viewingVersion?.description ?? endpoint.description
const sourceDataFreshness = viewingVersion?.data_freshness_seconds ?? endpoint.data_freshness_seconds ?? 86400
setEndpointDescription(sourceDescription || '')
setDataFreshness(sourceDataFreshness)
setIsMaterialized(null)
setLocalQuery(null)
resetBucketOverrides(viewingVersion?.bucket_overrides ?? endpoint.bucket_overrides ?? {})
resetOptionalBreakdownProperties(
viewingVersion?.optional_breakdown_properties ?? endpoint.optional_breakdown_properties ?? []
)
}
return (
<>
<SceneTitleSection
name={endpointName || endpoint?.name}
description={endpointDescription ?? viewingVersion?.description ?? endpoint?.description}
resourceType={{ type: 'endpoints' }}
canEdit={!editAccessDisabledReason}
// onNameChange={} - we explicitly disallow this
onDescriptionChange={(description) => setEndpointDescription(description)}
isLoading={endpointLoading && !endpoint}
renameDebounceMs={200}
actions={
<>
{superpowersEnabled && endpoint && (
<Switch
bordered
checked={debugInfoExpanded}
onChange={setDebugInfoExpanded}
label={
<span className="inline-flex items-center gap-1">
<IconBug />
Debug info
</span>
}
tooltip="Visible to staff (and during impersonation) only"
/>
)}
{endpoint && (
<Button
type="secondary"
onClick={handleDiscardChanges}
disabledReason={!hasChanges && 'No changes to discard'}
>
Discard changes
</Button>
)}
<AccessControlAction
resourceType={AccessControlResourceType.Endpoint}
minAccessLevel={AccessControlLevel.Editor}
>
<Button
type="primary"
onClick={handleSave}
loading={updatingEndpoint}
disabledReason={
!endpoint
? 'Endpoint not loaded'
: updatingEndpoint
? 'Update in progress'
: !hasChanges
? 'No changes to save'
: hasQueryChange && targetVersion
? 'Query can only be changed when on the latest version'
: undefined
}
>
Update
</Button>
</AccessControlAction>
</>
}
/>
{superpowersEnabled && endpoint && debugInfoExpanded && (
<DebugInfoPanel endpoint={endpoint} viewingVersion={viewingVersion} />
)}
</>
)
}
interface DebugInfoPanelProps {
endpoint: EndpointType
viewingVersion: EndpointVersionType | null
}
function DebugInfoPanel({ endpoint, viewingVersion }: DebugInfoPanelProps): JSX.Element {
const savedQueryId = endpoint.materialization?.saved_query_id
// Prefer the version being viewed; fall back to the endpoint's current version UUID.
const versionId = viewingVersion?.version_id ?? endpoint.current_version_id
return (
<div className="flex flex-col gap-2 border-dashed border rounded p-2 bg-bg-light">
<div className="inline-flex flex-wrap gap-6">
<DebugField label="Endpoint ID" value={endpoint.id} />
{versionId && <DebugField label="Version ID" value={versionId} />}
{savedQueryId ? (
<DebugField label="Saved query ID" value={savedQueryId} />
) : (
<div className="flex flex-col">
<Label>Saved query ID</Label>
<span className="text-xs text-muted">Not materialized</span>
</div>
)}
</div>
</div>
)
}
function DebugField({ label, value }: { label: string; value: string }): JSX.Element {
return (
<div className="flex flex-col">
<Label>{label}</Label>
<Button
type="secondary"
size="xsmall"
onClick={() => {
navigator.clipboard.writeText(value)
toast.success(`${label} copied to clipboard`)
}}
className="font-mono text-xs"
>
{value}
</Button>
</div>
)
}