feat: improved onboarding part 1 (#2186)

* feat: analytics events

* improved forms

* store state

* lint

* cleanup tenant name

* nits

* add environment to the form

* environment tag

* include env with tenant

* lint

* fix gen

* address comments

* feedback

* fix: layout

* navigation state

* rm dep

* lint

* address review

* lint

* lint

* fix: build
This commit is contained in:
Gabe Ruttner
2025-08-25 11:14:34 -07:00
committed by GitHub
parent 2a8ba155fa
commit 59fe6c110e
31 changed files with 6721 additions and 849 deletions
@@ -34,6 +34,8 @@ TenantMember:
$ref: "./tenant.yaml#/TenantMember"
TenantMemberList:
$ref: "./tenant.yaml#/TenantMemberList"
TenantEnvironment:
$ref: "./tenant.yaml#/TenantEnvironment"
TenantMemberRole:
$ref: "./tenant.yaml#/TenantMemberRole"
TenantResource:
@@ -20,6 +20,9 @@ Tenant:
uiVersion:
$ref: "#/TenantUIVersion"
description: The UI of the tenant.
environment:
$ref: "#/TenantEnvironment"
description: The environment type of the tenant.
required:
- metadata
- name
@@ -67,6 +70,14 @@ TenantAlertingSettings:
- maxAlertingFrequency
type: object
# IMPORTANT: keep values in sync with sql/schema/v0.sql#TenantEnvironment
TenantEnvironment:
enum:
- "local"
- "development"
- "production"
type: string
CreateTenantRequest:
properties:
name:
@@ -85,6 +96,13 @@ CreateTenantRequest:
engineVersion:
$ref: "#/TenantVersion"
description: The engine version of the tenant. Defaults to V0.
environment:
$ref: "#/TenantEnvironment"
description: The environment type of the tenant.
onboardingData:
type: object
description: Additional onboarding data to store with the tenant.
additionalProperties: true
required:
- name
- slug
+9
View File
@@ -50,6 +50,15 @@ func (t *TenantService) TenantCreate(ctx echo.Context, request gen.TenantCreateR
Name: request.Body.Name,
}
if request.Body.OnboardingData != nil {
createOpts.OnboardingData = *request.Body.OnboardingData
}
if request.Body.Environment != nil {
environment := string(*request.Body.Environment)
createOpts.Environment = &environment
}
if t.config.Runtime.Limits.DefaultTenantRetentionPeriod != "" {
createOpts.DataRetentionPeriod = &t.config.Runtime.Limits.DefaultTenantRetentionPeriod
}
+311 -295
View File
@@ -165,6 +165,13 @@ const (
StepRunStatusSUCCEEDED StepRunStatus = "SUCCEEDED"
)
// Defines values for TenantEnvironment.
const (
Development TenantEnvironment = "development"
Local TenantEnvironment = "local"
Production TenantEnvironment = "production"
)
// Defines values for TenantMemberRole.
const (
ADMIN TenantMemberRole = "ADMIN"
@@ -514,11 +521,15 @@ type CreateTenantInviteRequest struct {
// CreateTenantRequest defines model for CreateTenantRequest.
type CreateTenantRequest struct {
EngineVersion *TenantVersion `json:"engineVersion,omitempty"`
EngineVersion *TenantVersion `json:"engineVersion,omitempty"`
Environment *TenantEnvironment `json:"environment,omitempty"`
// Name The name of the tenant.
Name string `json:"name" validate:"required"`
// OnboardingData Additional onboarding data to store with the tenant.
OnboardingData *map[string]interface{} `json:"onboardingData,omitempty"`
// Slug The slug of the tenant.
Slug string `json:"slug" validate:"required,hatchetName"`
UiVersion *TenantUIVersion `json:"uiVersion,omitempty"`
@@ -1010,8 +1021,9 @@ type Tenant struct {
AlertMemberEmails *bool `json:"alertMemberEmails,omitempty"`
// AnalyticsOptOut Whether the tenant has opted out of analytics.
AnalyticsOptOut *bool `json:"analyticsOptOut,omitempty"`
Metadata APIResourceMeta `json:"metadata"`
AnalyticsOptOut *bool `json:"analyticsOptOut,omitempty"`
Environment *TenantEnvironment `json:"environment,omitempty"`
Metadata APIResourceMeta `json:"metadata"`
// Name The name of the tenant.
Name string `json:"name"`
@@ -1057,6 +1069,9 @@ type TenantAlertingSettings struct {
Metadata APIResourceMeta `json:"metadata"`
}
// TenantEnvironment defines model for TenantEnvironment.
type TenantEnvironment string
// TenantInvite defines model for TenantInvite.
type TenantInvite struct {
// Email The email of the user to invite.
@@ -15226,298 +15241,299 @@ func (sh *strictHandler) WorkflowVersionGet(ctx echo.Context, workflow openapi_t
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"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",
"/4PcEhH58wIA",
"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",
}
// GetSwagger returns the content of the embedded swagger specification file
+8
View File
@@ -10,6 +10,13 @@ import (
func ToTenant(tenant *dbsqlc.Tenant) *gen.Tenant {
uiVersion := gen.TenantUIVersion(tenant.UiVersion)
var environment *gen.TenantEnvironment
if tenant.Environment.Valid {
env := gen.TenantEnvironment(tenant.Environment.TenantEnvironment)
environment = &env
}
return &gen.Tenant{
Metadata: *toAPIMetadata(sqlchelpers.UUIDToStr(tenant.ID), tenant.CreatedAt.Time, tenant.UpdatedAt.Time),
Name: tenant.Name,
@@ -18,6 +25,7 @@ func ToTenant(tenant *dbsqlc.Tenant) *gen.Tenant {
AlertMemberEmails: &tenant.AlertMemberEmails,
Version: gen.TenantVersion(tenant.Version),
UiVersion: &uiVersion,
Environment: environment,
}
}
+8
View File
@@ -28,6 +28,13 @@ func ToUser(user *dbsqlc.User, hasPassword bool, hashedEmail *string) *gen.User
func ToTenantMember(tenantMember *dbsqlc.PopulateTenantMembersRow) *gen.TenantMember {
uiVersion := gen.TenantUIVersion(tenantMember.TenantUiVersion)
var environment *gen.TenantEnvironment
if tenantMember.TenantEnvironment.Valid {
env := gen.TenantEnvironment(tenantMember.TenantEnvironment.TenantEnvironment)
environment = &env
}
res := &gen.TenantMember{
Metadata: *toAPIMetadata(sqlchelpers.UUIDToStr(tenantMember.ID), tenantMember.CreatedAt.Time, tenantMember.UpdatedAt.Time),
User: gen.UserTenantPublic{
@@ -42,6 +49,7 @@ func ToTenantMember(tenantMember *dbsqlc.PopulateTenantMembersRow) *gen.TenantMe
AlertMemberEmails: &tenantMember.AlertMemberEmails,
Version: gen.TenantVersion(tenantMember.TenantVersion),
UiVersion: &uiVersion,
Environment: environment,
},
Role: gen.TenantMemberRole(tenantMember.Role),
}
@@ -0,0 +1,17 @@
-- +goose Up
-- +goose StatementBegin
-- enum for environment
CREATE TYPE "TenantEnvironment" AS ENUM ('local', 'development', 'production');
ALTER TABLE "Tenant"
ADD COLUMN "onboardingData" JSONB,
ADD COLUMN "environment" "TenantEnvironment";
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE "Tenant"
DROP COLUMN "onboardingData",
DROP COLUMN "environment";
DROP TYPE "TenantEnvironment";
-- +goose StatementEnd
+383 -316
View File
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,7 @@
import { User } from '@/lib/api';
import { useTenant } from '@/lib/atoms';
import useApiMeta from '@/pages/auth/hooks/use-api-meta';
import { useAnalytics } from '@/hooks/use-analytics';
import React, { PropsWithChildren, useEffect, useMemo } from 'react';
interface AnalyticsProviderProps {
@@ -15,6 +16,7 @@ const AnalyticsProvider: React.FC<
const [loaded, setLoaded] = React.useState(false);
const { tenant } = useTenant();
const { identify } = useAnalytics();
const config = useMemo(() => {
return meta.data?.posthog;
@@ -60,24 +62,16 @@ posthog.init('${config.apiKey}',{
setTimeout(() => {
const ref = localStorage.getItem('ref');
if (ref) {
(window as any).posthog.identify(
ref, // Required. Replace 'distinct_id' with your user's unique identifier
{}, // $set, optional
{}, // $set_once, optional
);
identify(ref, {}, {});
}
if (!user) {
return;
}
(window as any).posthog.identify(
user.metadata.id, // Required. Replace 'distinct_id' with your user's unique identifier
{ email: user.email, name: user.name }, // $set, optional
{}, // $set_once, optional
);
identify(user.metadata.id, { email: user.email, name: user.name }, {});
});
}, [user, config, tenant]);
}, [user, config, tenant, identify]);
return children;
};
@@ -248,10 +248,10 @@ export default function MainNav({ user, setHasBanner }: MainNavProps) {
}, [setHasBanner, banner]);
return (
<div className="fixed top-0 w-screen">
<div className="fixed top-0 w-screen z-50">
{banner && <Banner {...banner} />}
<div className="h-16 border-b">
<div className="h-16 border-b bg-background">
<div className="flex h-16 items-center pr-4 pl-4">
<div className="flex flex-row items-center gap-x-8">
<button
@@ -1,85 +0,0 @@
import { useTenantDetails } from '@/hooks/use-tenant';
import { User } from '@/lib/api';
import useApiMeta from '@/pages/auth/hooks/use-api-meta';
import React, { PropsWithChildren, useEffect, useMemo } from 'react';
interface AnalyticsProviderProps {
user: User;
}
const AnalyticsProvider: React.FC<
PropsWithChildren & AnalyticsProviderProps
> = ({ user, children }) => {
const meta = useApiMeta();
const [loaded, setLoaded] = React.useState(false);
const { tenant } = useTenantDetails();
const config = useMemo(() => {
return meta.data?.posthog;
}, [meta]);
useEffect(() => {
if (loaded) {
return;
}
if (tenant && tenant.analyticsOptOut) {
console.log(
'Skipping Analytics initialization due to opt-out, we respect user privacy.',
);
return;
}
if (!config || !tenant) {
return;
}
console.log('Initializing Analytics, opt out in settings.');
setLoaded(true);
const posthogScript = `
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('${config.apiKey}',{
api_host:'${config.apiHost}',
session_recording: {
maskAllInputs: true,
maskTextSelector: "*"
}
})
`;
document.head.appendChild(document.createElement('script')).innerHTML =
posthogScript;
}, [config, loaded, tenant]);
useEffect(() => {
if (!config) {
return;
}
setTimeout(() => {
const ref = localStorage.getItem('ref');
if (ref) {
(window as any).posthog.identify(
ref, // Required. Replace 'distinct_id' with your user's unique identifier
{}, // $set, optional
{}, // $set_once, optional
);
}
if (!user) {
return;
}
(window as any).posthog.identify(
user.metadata.id, // Required. Replace 'distinct_id' with your user's unique identifier
{ email: user.email, name: user.name }, // $set, optional
{}, // $set_once, optional
);
});
}, [user, config, tenant]);
return children;
};
export default AnalyticsProvider;
+73
View File
@@ -0,0 +1,73 @@
import { useCallback } from 'react';
import { useTenantDetails } from './use-tenant';
interface AnalyticsEvent {
[key: string]: any;
}
interface UseAnalyticsReturn {
capture: (eventName: string, properties?: AnalyticsEvent) => void;
identify: (
userId: string,
properties?: AnalyticsEvent,
setOnceProperties?: AnalyticsEvent,
) => void;
isAvailable: boolean;
}
/**
* Hook for PostHog analytics integration
* Provides a clean interface for tracking events and identifying users
*/
export function useAnalytics(): UseAnalyticsReturn {
const { tenant } = useTenantDetails();
const isAvailable = useCallback(() => {
// Check if PostHog is loaded and user hasn't opted out
return (
typeof window !== 'undefined' &&
(window as any).posthog &&
(!tenant || !tenant.analyticsOptOut)
);
}, [tenant]);
const capture = useCallback(
(eventName: string, properties?: AnalyticsEvent) => {
if (!isAvailable()) {
return;
}
try {
(window as any).posthog.capture(eventName, properties);
} catch (error) {
console.warn('Analytics capture failed:', error);
}
},
[isAvailable],
);
const identify = useCallback(
(
userId: string,
properties?: AnalyticsEvent,
setOnceProperties?: AnalyticsEvent,
) => {
if (!isAvailable()) {
return;
}
try {
(window as any).posthog.identify(userId, properties, setOnceProperties);
} catch (error) {
console.warn('Analytics identify failed:', error);
}
},
[isAvailable],
);
return {
capture,
identify,
isAvailable: isAvailable(),
};
}
@@ -238,6 +238,12 @@ export enum V1WebhookSourceName {
LINEAR = "LINEAR",
}
export enum TenantEnvironment {
Local = "local",
Development = "development",
Production = "production",
}
export enum TenantUIVersion {
V0 = "V0",
V1 = "V1",
@@ -751,6 +757,8 @@ export interface Tenant {
version: TenantVersion;
/** The UI of the tenant. */
uiVersion?: TenantUIVersion;
/** The environment type of the tenant. */
environment?: TenantEnvironment;
}
export interface V1EventWorkflowRunSummary {
@@ -1256,6 +1264,10 @@ export interface CreateTenantRequest {
uiVersion?: TenantUIVersion;
/** The engine version of the tenant. Defaults to V0. */
engineVersion?: TenantVersion;
/** The environment type of the tenant. */
environment?: TenantEnvironment;
/** Additional onboarding data to store with the tenant. */
onboardingData?: Record<string, any>;
}
export interface UpdateTenantRequest {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,122 @@
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import {
Search,
FileText,
Users,
Github,
Calendar,
HelpCircle,
} from 'lucide-react';
import { OnboardingStepProps } from '../types';
import { FaHackerNews, FaLinkedin, FaTwitter } from 'react-icons/fa';
import {
extractOtherSelection,
toggleOtherOption,
toggleRegularOption,
updateOtherText,
} from '../utils/other-selection';
interface HearAboutUsFormProps extends OnboardingStepProps<string | string[]> {}
export function HearAboutUsForm({
value,
onChange,
onNext,
}: HearAboutUsFormProps) {
const options = [
{ value: 'hackernews', label: 'Hacker News', icon: FaHackerNews },
{ value: 'search', label: 'Search Engine', icon: Search },
{ value: 'linkedin', label: 'LinkedIn', icon: FaLinkedin },
{ value: 'twitter', label: 'Twitter', icon: FaTwitter },
{ value: 'blog', label: 'Blog/Article', icon: FileText },
{ value: 'colleague', label: 'Word of Mouth', icon: Users },
{ value: 'github', label: 'GitHub', icon: Github },
{ value: 'conference', label: 'Event', icon: Calendar },
{ value: 'other', label: 'Other', icon: HelpCircle },
];
const selectedValues = Array.isArray(value) ? value : value ? [value] : [];
// Extract "other" selection information using helper
const { isOtherSelected, otherValue, otherSelection } =
extractOtherSelection(selectedValues);
const handleOptionToggle = (optionValue: string) => {
if (optionValue === 'other') {
const newValues = toggleOtherOption(selectedValues, isOtherSelected);
onChange(newValues);
} else {
const newValues = toggleRegularOption(
selectedValues,
optionValue,
isOtherSelected,
otherSelection,
);
onChange(newValues);
}
};
const handleOtherTextChange = (text: string) => {
const newValues = updateOtherText(selectedValues, text);
onChange(newValues);
};
return (
<div className="space-y-4">
<div className="space-y-3">
{options.map((option) => {
const Icon = option.icon;
const isSelected =
option.value === 'other'
? isOtherSelected
: selectedValues.includes(option.value);
return (
<Card
key={option.value}
onClick={() => handleOptionToggle(option.value)}
className={`cursor-pointer transition-all hover:shadow-md ${
isSelected
? 'border-blue-500 bg-blue-50 dark:bg-blue-950'
: 'hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
<CardContent className="p-4">
<div className="flex items-center space-x-3">
<Icon className="w-5 h-5 text-gray-600 dark:text-gray-400 flex-shrink-0" />
<span className="font-medium text-sm">{option.label}</span>
</div>
</CardContent>
</Card>
);
})}
</div>
{isOtherSelected && (
<div className="mt-6 space-y-3">
<Label htmlFor="other-hear-about">Please specify:</Label>
<Textarea
id="other-hear-about"
placeholder="Tell us how you heard about Hatchet..."
className="mt-2"
value={otherValue}
onChange={(e) => handleOtherTextChange(e.target.value)}
/>
</div>
)}
<div className="mt-6">
<Button
onClick={onNext}
className="w-full"
disabled={selectedValues.length === 0}
>
Continue
</Button>
</div>
</div>
);
}
@@ -0,0 +1,44 @@
import { Badge } from '@/components/ui/badge';
export function StepProgress<T = unknown>({
steps,
currentStep,
onStepClick,
}: {
steps: Array<T>;
currentStep: number;
onStepClick?: (stepIndex: number) => void;
}) {
return (
<div className="flex flex-col space-y-2 text-center">
<div className="flex justify-center items-center mb-6">
{steps.map((_, index) => (
<div key={index} className="flex items-center">
<Badge
variant={index <= currentStep ? 'default' : 'secondary'}
className={`h-8 w-8 rounded-full flex items-center justify-center text-sm font-medium transition-colors ${
index <= currentStep
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground'
} ${
onStepClick
? 'cursor-pointer hover:opacity-80 hover:scale-105 active:scale-95'
: ''
}`}
onClick={() => onStepClick?.(index)}
>
{index + 1}
</Badge>
{index < steps.length - 1 && (
<div
className={`w-8 h-0.5 mx-3 transition-colors ${
index < currentStep ? 'bg-primary' : 'bg-muted'
}`}
/>
)}
</div>
))}
</div>
</div>
);
}
@@ -1,132 +1,243 @@
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Spinner } from '@/components/ui/loading.tsx';
import { Card, CardContent } from '@/components/ui/card';
import { Monitor, Settings, Rocket } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useEffect } from 'react';
import { useEffect, useMemo } from 'react';
import { OnboardingStepProps } from '../types';
import { useQuery } from '@tanstack/react-query';
import api, { TenantEnvironment } from '@/lib/api';
import freeEmailDomains from '@/lib/free-email-domains.json';
const schema = z.object({
name: z.string().min(4).max(32),
slug: z.string().min(4).max(32),
environment: z.string().min(1),
});
interface TenantCreateFormProps {
className?: string;
onSubmit: (opts: z.infer<typeof schema>) => void;
isLoading: boolean;
fieldErrors?: Record<string, string>;
}
interface TenantCreateFormProps
extends OnboardingStepProps<{ name: string; environment: string }> {}
export function TenantCreateForm({
value,
onChange,
isLoading,
fieldErrors,
className,
...props
}: TenantCreateFormProps) {
const user = useQuery({
queryKey: ['user:get:current'],
retry: false,
queryFn: async () => {
const res = await api.userGetCurrent();
return res.data;
},
});
const {
register,
handleSubmit,
setValue,
watch,
formState: { errors },
} = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
defaultValues: {
name: '',
environment: 'development',
},
});
useEffect(() => {
const subscription = watch((value, { name }) => {
switch (name) {
case 'name':
if (value.name) {
const slug =
value.name
?.toLowerCase()
.replace(/[^a-z0-9-]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '') +
'-' +
Math.random().toString(36).substr(2, 5);
if (slug) {
setValue('slug', slug);
}
}
break;
case 'slug':
break;
const getEnvironmentPostfix = (environment: string | undefined): string => {
switch (environment) {
case TenantEnvironment.Local:
return '-local';
case TenantEnvironment.Development:
return '-dev';
case TenantEnvironment.Production:
return '-prod';
default: {
// Exhaustiveness check: this should never be reached if all cases are handled
const exhaustiveCheck: never = environment as never;
void exhaustiveCheck;
return '-dev'; // Default to dev if no environment selected
}
}
};
const hasEnvironmentPostfix = (name: string): boolean => {
return (
name.endsWith('-local') || name.endsWith('-dev') || name.endsWith('-prod')
);
};
const removeEnvironmentPostfix = (name: string): string => {
if (name.endsWith('-local')) {
return name.slice(0, -6);
}
if (name.endsWith('-dev')) {
return name.slice(0, -4);
}
if (name.endsWith('-prod')) {
return name.slice(0, -5);
}
return name;
};
const updateNameWithEnvironment = (
currentName: string,
environment: string,
): string => {
const baseName = hasEnvironmentPostfix(currentName)
? removeEnvironmentPostfix(currentName)
: currentName;
return baseName + getEnvironmentPostfix(environment);
};
const emptyState = useMemo(() => {
if (!user.data?.email) {
return '';
}
const email = user.data.email;
const [localPart, domain] = email.split('@');
let baseName = '';
if (freeEmailDomains.includes(domain?.toLowerCase())) {
baseName = localPart;
} else {
// For business emails, use the domain without the TLD
const domainParts = domain?.split('.');
baseName = domainParts?.[0] || localPart;
}
// Add environment-specific postfix using current environment
const currentEnvironment = value?.environment || 'development';
return `${baseName}${getEnvironmentPostfix(currentEnvironment)}`;
}, [user.data?.email, value?.environment]);
// Update form values when parent value changes
useEffect(() => {
const nameValue = value?.name || emptyState;
const environmentValue = value?.environment || 'development';
setValue('name', nameValue);
setValue('environment', environmentValue);
// Also update the parent if we're using the generated name
if (!value?.name && emptyState && nameValue) {
onChange({
name: nameValue,
environment: environmentValue,
});
}
}, [value, setValue, emptyState, onChange]);
const nameError = errors.name?.message?.toString() || fieldErrors?.name;
const environmentOptions = [
{
value: 'local',
label: 'Local Dev',
icon: Monitor,
description: 'Testing and development on your local machine',
},
{
value: 'development',
label: 'Development',
icon: Settings,
description: 'Shared development environment or staging',
},
{
value: 'production',
label: 'Production',
icon: Rocket,
description: 'Live production environment serving real users',
},
];
const handleEnvironmentChange = (selectedEnvironment: string) => {
const currentName = value?.name || '';
const updatedName = currentName
? updateNameWithEnvironment(currentName, selectedEnvironment)
: '';
setValue('environment', selectedEnvironment);
setValue('name', updatedName);
onChange({
name: updatedName,
environment: selectedEnvironment,
});
return () => subscription.unsubscribe();
}, [setValue, watch]);
const nameError =
errors.name?.message?.toString() || props.fieldErrors?.email;
const slugError = errors.slug?.message?.toString() || props.fieldErrors?.slug;
};
return (
<div className={cn('grid gap-6', className)}>
<form
onSubmit={handleSubmit((d) => {
props.onSubmit(d);
})}
>
<div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<div className="text-sm text-gray-700 dark:text-gray-300">
A display name for your tenant.
</div>
<Input
{...register('name')}
id="name"
placeholder="My Awesome Tenant"
type="name"
autoCapitalize="none"
autoCorrect="off"
disabled={props.isLoading}
spellCheck={false}
onChange={(e) => {
setValue('name', e.target.value);
<div className="grid gap-4">
<div className="grid gap-2">
<Label>Environment Type</Label>
<div className="text-sm text-gray-700 dark:text-gray-300">
You can add new tenants for different environments later.
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{environmentOptions.map((option) => {
const Icon = option.icon;
const isSelected =
(value?.environment || 'development') === option.value;
// if value is unset, reset the slug
if (!e.target.value) {
setValue('slug', '');
}
}}
/>
{nameError && (
<div className="text-sm text-red-500">{nameError}</div>
)}
return (
<Card
key={option.value}
onClick={() => handleEnvironmentChange(option.value)}
className={`cursor-pointer transition-all hover:shadow-md ${
isSelected
? 'border-blue-500 bg-blue-50 dark:bg-blue-950'
: 'hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
<CardContent className="p-4 flex flex-col items-center text-center space-y-2">
<Icon
className={`w-6 h-6 ${isSelected ? 'text-blue-600 dark:text-blue-400' : 'text-gray-600 dark:text-gray-400'}`}
/>
<div className="space-y-1">
<div className="font-medium text-sm">{option.label}</div>
<div className="text-xs text-gray-600 dark:text-gray-400">
{option.description}
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
<div className="grid gap-2">
<Label htmlFor="name">Slug</Label>
<div className="text-sm text-gray-700 dark:text-gray-300">
A URI-friendly identifier for your tenant.
</div>
<Input
{...register('slug')}
id="slug"
placeholder="my-awesome-tenant-123456"
type="name"
autoCapitalize="none"
autoCorrect="off"
disabled={props.isLoading}
spellCheck={false}
/>
{slugError && (
<div className="text-sm text-red-500">{slugError}</div>
)}
</div>
<Button disabled={props.isLoading}>
{props.isLoading && <Spinner />}
Create
</Button>
</div>
</form>
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<div className="text-sm text-gray-700 dark:text-gray-300">
A display name for your tenant.
</div>
<Input
{...register('name')}
id="name"
placeholder="My Awesome Tenant"
type="name"
autoCapitalize="none"
autoCorrect="off"
disabled={isLoading}
spellCheck={false}
onChange={(e) => {
setValue('name', e.target.value);
onChange({
name: e.target.value,
environment: value?.environment || 'development',
});
}}
/>
{nameError && <div className="text-sm text-red-500">{nameError}</div>}
</div>
</div>
</div>
);
}
@@ -0,0 +1,129 @@
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import {
Database,
Workflow,
Brain,
HelpCircle,
FileText,
Webhook,
} from 'lucide-react';
import { OnboardingStepProps } from '../types';
import {
extractOtherSelection,
toggleOtherOption,
toggleRegularOption,
updateOtherText,
} from '../utils/other-selection';
interface WhatBuildingFormProps
extends OnboardingStepProps<string | string[]> {}
export function WhatBuildingForm({
value,
onChange,
onNext,
}: WhatBuildingFormProps) {
const options = [
{ value: 'ai-agents', label: 'AI Agents', icon: Brain },
{
value: 'document-ingestion',
label: 'Document Ingestion',
icon: FileText,
},
{ value: 'data-pipeline', label: 'Data Pipelines', icon: Database },
{
value: 'internal-automations',
label: 'Internal Automations',
icon: Workflow,
},
{ value: 'webhooks', label: 'Webhooks', icon: Webhook },
{ value: 'other', label: 'Other', icon: HelpCircle },
];
const selectedValues = Array.isArray(value) ? value : value ? [value] : [];
// Extract "other" selection information using helper
const { isOtherSelected, otherValue, otherSelection } =
extractOtherSelection(selectedValues);
const handleOptionToggle = (optionValue: string) => {
if (optionValue === 'other') {
const newValues = toggleOtherOption(selectedValues, isOtherSelected);
onChange(newValues);
} else {
const newValues = toggleRegularOption(
selectedValues,
optionValue,
isOtherSelected,
otherSelection,
);
onChange(newValues);
}
};
const handleOtherTextChange = (text: string) => {
const newValues = updateOtherText(selectedValues, text);
onChange(newValues);
};
return (
<div className="space-y-4">
<div className="space-y-3">
{options.map((option) => {
const Icon = option.icon;
const isSelected =
option.value === 'other'
? isOtherSelected
: selectedValues.includes(option.value);
return (
<Card
key={option.value}
onClick={() => handleOptionToggle(option.value)}
className={`cursor-pointer transition-all hover:shadow-md ${
isSelected
? 'border-blue-500 bg-blue-50 dark:bg-blue-950'
: 'hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
<CardContent className="p-4">
<div className="flex items-center space-x-3">
<Icon className="w-5 h-5 text-gray-600 dark:text-gray-400 flex-shrink-0" />
<span className="font-medium text-sm">{option.label}</span>
</div>
</CardContent>
</Card>
);
})}
</div>
{isOtherSelected && (
<div className="mt-6 space-y-3">
<Label htmlFor="other-description">
Please describe what you're building:
</Label>
<Textarea
id="other-description"
placeholder="Tell us about your project..."
className="mt-2"
value={otherValue}
onChange={(e) => handleOtherTextChange(e.target.value)}
/>
</div>
)}
<div className="mt-6">
<Button
onClick={onNext}
className="w-full"
disabled={selectedValues.length === 0}
>
Continue
</Button>
</div>
</div>
);
}
@@ -1,16 +1,52 @@
import api, { CreateTenantRequest, queries, TenantVersion } from '@/lib/api';
import api, {
CreateTenantRequest,
queries,
TenantEnvironment,
TenantVersion,
} from '@/lib/api';
import { useApiError } from '@/lib/hooks';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { TenantCreateForm } from './components/tenant-create-form';
import { useTenant } from '@/lib/atoms';
import { Button } from '@/components/ui/button';
import { HearAboutUsForm } from './components/hear-about-us-form';
import { WhatBuildingForm } from './components/what-building-form';
import { StepProgress } from './components/step-progress';
import { OnboardingStepConfig, OnboardingFormData } from './types';
import { useAnalytics } from '@/hooks/use-analytics';
export default function CreateTenant() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const stepFromUrl = parseInt(searchParams.get('step') || '0', 10);
const [currentStep, setCurrentStep] = useState(
Math.max(0, Math.min(stepFromUrl, 2)),
);
const [formData, setFormData] = useState<OnboardingFormData>({
name: '',
slug: '',
hearAboutUs: [],
whatBuilding: [],
environment: TenantEnvironment.Development,
tenantData: { name: '', environment: TenantEnvironment.Development },
});
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
const { handleApiError } = useApiError({
setFieldErrors: setFieldErrors,
});
const { setTenant } = useTenant();
const { capture } = useAnalytics();
// Sync currentStep with URL parameter
useEffect(() => {
const stepFromUrl = parseInt(searchParams.get('step') || '0', 10);
const validStep = Math.max(0, Math.min(stepFromUrl, 2));
setCurrentStep(validStep);
}, [searchParams]);
const listMembershipsQuery = useQuery({
...queries.user.listTenantMemberships,
@@ -20,6 +56,18 @@ export default function CreateTenant() {
mutationKey: ['user:update:login'],
mutationFn: async (data: CreateTenantRequest) => {
const tenant = await api.tenantCreate(data);
// Track onboarding analytics
capture('onboarding_completed', {
hear_about_us: Array.isArray(formData.hearAboutUs)
? formData.hearAboutUs.join(', ')
: formData.hearAboutUs,
what_building: Array.isArray(formData.whatBuilding)
? formData.whatBuilding.join(', ')
: formData.whatBuilding,
tenant_id: tenant.data.metadata.id,
});
return tenant.data;
},
onSuccess: async (tenant) => {
@@ -38,23 +86,209 @@ export default function CreateTenant() {
onError: handleApiError,
});
return (
<div className="flex flex-row flex-1 w-full h-full">
<div className="container relative hidden flex-col items-center justify-center md:grid lg:max-w-none lg:grid-cols-2 lg:px-0">
<div className="lg:p-8 mx-auto w-screen">
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<div className="flex flex-col space-y-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight">
Create a new tenant
</h1>
</div>
<TenantCreateForm
isLoading={createMutation.isPending}
onSubmit={createMutation.mutate}
fieldErrors={fieldErrors}
/>
</div>
const steps: OnboardingStepConfig[] = [
{
title: 'What are you building?',
subtitle: 'Help us personalize your onboarding experience',
component: WhatBuildingForm,
canSkip: true,
key: 'whatBuilding',
},
{
title: 'Where did you hear about Hatchet?',
component: HearAboutUsForm,
canSkip: true,
key: 'hearAboutUs',
},
{
title: 'Create a new tenant',
subtitle:
'A tenant is an isolated environment for your data and workflows.',
component: TenantCreateForm,
canSkip: false,
key: 'tenantData',
buttonLabel: 'Create Tenant',
},
];
const handleNext = () => {
if (currentStep < steps.length - 1) {
const nextStep = currentStep + 1;
navigate(`?step=${nextStep}`, { replace: false });
}
};
const validateCurrentStep = (): boolean => {
// For the tenant create form (step 2), we need to validate the form
if (currentStep === 2) {
const { name } = formData.tenantData;
// Clear previous errors
setFieldErrors({});
// Basic validation
if (!name || name.length < 4 || name.length > 32) {
setFieldErrors((prev) => ({
...prev,
name: 'Name must be between 4 and 32 characters',
}));
return false;
}
return true;
}
return true;
};
const handlePrevious = () => {
if (currentStep > 0) {
const previousStep = currentStep - 1;
navigate(`?step=${previousStep}`, { replace: false });
}
};
const handleStepClick = (stepIndex: number) => {
// Allow navigation to any step within valid range
if (stepIndex >= 0 && stepIndex < steps.length) {
navigate(`?step=${stepIndex}`, { replace: false });
}
};
const generateSlug = (name: string): string => {
return (
name
.toLowerCase()
.replace(/[^a-z0-9-]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '') +
'-' +
Math.random().toString(36).substring(0, 5)
);
};
const handleTenantCreate = (tenantData: {
name: string;
environment: TenantEnvironment;
}) => {
// Generate slug from name
const slug = generateSlug(tenantData.name);
// Prepare the onboarding data to send with the tenant creation request
const onboardingData = {
hearAboutUs: Array.isArray(formData.hearAboutUs)
? formData.hearAboutUs.join(', ')
: formData.hearAboutUs,
whatBuilding: Array.isArray(formData.whatBuilding)
? formData.whatBuilding.join(', ')
: formData.whatBuilding,
};
createMutation.mutate({
name: tenantData.name,
slug,
onboardingData,
environment: tenantData.environment,
});
};
const updateFormData = (key: keyof OnboardingFormData, value: any) => {
setFormData({ ...formData, [key]: value });
// Sync tenantData with name for backward compatibility
if (key === 'tenantData') {
setFormData({
...formData,
[key]: value,
name: value.name,
});
}
};
const renderCurrentStep = () => {
const currentStepConfig = steps[currentStep];
const StepComponent = currentStepConfig.component;
return (
<>
<div className="flex flex-col space-y-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight">
{currentStepConfig.title}
</h1>
{currentStepConfig.subtitle && (
<p className="text-sm text-muted-foreground">
{currentStepConfig.subtitle}
</p>
)}
</div>
<StepComponent
value={formData[currentStepConfig.key]}
onChange={(value) => updateFormData(currentStepConfig.key, value)}
onNext={
currentStep === 2
? () => handleTenantCreate(formData.tenantData)
: handleNext
}
onPrevious={handlePrevious}
isLoading={createMutation.isPending}
fieldErrors={fieldErrors}
formData={formData}
setFormData={setFormData}
className=""
/>
<div className="flex justify-between">
<StepProgress
steps={steps}
currentStep={currentStep}
onStepClick={handleStepClick}
/>
{currentStepConfig.canSkip ? (
<Button
variant="outline"
size="sm"
onClick={() =>
navigate(`?step=${currentStep + 1}`, { replace: false })
}
>
Skip
</Button>
) : (
<Button
variant="default"
onClick={() => {
if (currentStep === 2) {
// For tenant create form, validate and submit
if (validateCurrentStep()) {
handleTenantCreate(formData.tenantData);
}
} else {
handleNext();
}
}}
disabled={createMutation.isPending}
>
{createMutation.isPending ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Creating...
</div>
) : (
currentStepConfig.buttonLabel || 'Next'
)}
</Button>
)}
</div>
</>
);
};
return (
<div className="flex flex-col items-center justify-center min-h-full w-full p-4">
<div className="w-full max-w-[450px] space-y-6">
{renderCurrentStep()}
</div>
</div>
);
@@ -0,0 +1,42 @@
import { TenantEnvironment } from '@/lib/api';
// Base interface that all onboarding step components must implement
export interface OnboardingStepProps<T = any> {
value: T;
onChange: (value: T) => void;
onNext?: () => void;
onPrevious?: () => void;
isLoading?: boolean;
fieldErrors?: Record<string, string>;
formData?: OnboardingFormData;
setFormData?: (data: OnboardingFormData) => void;
className?: string;
}
// Form data interface for the entire onboarding flow
export interface OnboardingFormData {
name: string;
slug: string;
hearAboutUs: string | string[];
whatBuilding: string | string[];
environment: TenantEnvironment;
tenantData: { name: string; environment: TenantEnvironment };
}
// Step configuration interface
export interface OnboardingStepConfig {
title: string;
subtitle?: string;
component:
| React.ComponentType<OnboardingStepProps>
| React.ComponentType<any>;
canSkip: boolean;
key: keyof OnboardingFormData;
validate?: (value: any) => boolean;
buttonLabel?: string;
}
// Base component type that all steps should extend
export type OnboardingStepComponent<T = any> = React.ComponentType<
OnboardingStepProps<T>
>;
@@ -0,0 +1,91 @@
/**
* Utility functions for handling "other" selections in form options
*/
export interface OtherSelectionResult {
isOtherSelected: boolean;
otherValue: string;
otherSelection: string | undefined;
}
/**
* Extracts "other" selection information from an array of selected values
* @param selectedValues Array of selected values that may contain "other: ..." entries
* @returns Object containing other selection state and value
*/
export function extractOtherSelection(
selectedValues: string[],
): OtherSelectionResult {
const otherSelection = selectedValues.find((v) => v.startsWith('other'));
const isOtherSelected = !!otherSelection;
const otherValue = otherSelection
? otherSelection.replace('other: ', '')
: '';
return {
isOtherSelected,
otherValue,
otherSelection,
};
}
/**
* Handles toggling the "other" option in a multi-select form
* @param selectedValues Current array of selected values
* @param isOtherSelected Whether "other" is currently selected
* @param otherSelection The current "other" selection string
* @returns Updated array of selected values
*/
export function toggleOtherOption(
selectedValues: string[],
isOtherSelected: boolean,
): string[] {
if (isOtherSelected) {
// Remove other option
return selectedValues.filter((v) => !v.startsWith('other'));
} else {
// Add other option with empty value
return [...selectedValues.filter((v) => !v.startsWith('other')), 'other: '];
}
}
/**
* Handles toggling a regular (non-other) option in a multi-select form
* @param selectedValues Current array of selected values
* @param optionValue The option value to toggle
* @param isOtherSelected Whether "other" is currently selected
* @param otherSelection The current "other" selection string
* @returns Updated array of selected values
*/
export function toggleRegularOption(
selectedValues: string[],
optionValue: string,
isOtherSelected: boolean,
otherSelection?: string,
): string[] {
if (selectedValues.includes(optionValue)) {
// Remove the option
return selectedValues.filter((v) => v !== optionValue);
} else {
// Add the option, preserving "other" if it exists
return [
...selectedValues.filter((v) => !v.startsWith('other')),
optionValue,
...(isOtherSelected && otherSelection ? [otherSelection] : []),
];
}
}
/**
* Updates the text value for an "other" selection
* @param selectedValues Current array of selected values
* @param text The new text value for the "other" option
* @returns Updated array of selected values
*/
export function updateOtherText(
selectedValues: string[],
text: string,
): string[] {
const newValues = selectedValues.filter((v) => !v.startsWith('other'));
return [...newValues, `other: ${text}`];
}
@@ -36,14 +36,17 @@ export default function GetStarted() {
<div className="container mx-auto px-4 lg:px-8 max-w-4xl">
<div className="flex flex-col justify-center space-y-4">
<div className="flex flex-row justify-between mt-10">
<h1 className="text-3xl font-bold">Quickstart</h1>
<h1 className="text-3xl font-bold">
{/* TODO: we should give the production environment a different treatment */}
Setup your {currTenant.environment} environment
</h1>
<a href="/">
<Button variant="outline">Skip Quickstart</Button>
</a>
</div>
<p className="text-gray-600 dark:text-gray-300 text-sm">
Get started by deploying your first worker.
Get started by running your first worker on your local machine.
</p>
<Steps className="mt-6">
@@ -26,6 +26,8 @@ export const DefaultOnboardingAuth: React.FC<{
});
if (generatedToken) {
const token = 'export HATCHET_CLIENT_TOKEN="' + generatedToken + '"';
return (
<div>
<p className="mb-4 text-muted-foreground">
@@ -41,17 +43,20 @@ export const DefaultOnboardingAuth: React.FC<{
language="plaintext"
className="text-sm"
wrapLines={false}
code={'export HATCHET_CLIENT_TOKEN="' + generatedToken + '"'}
code={token}
copy
/>
</div>
<Button
onClick={() => tokenGenerated()}
variant="outline"
onClick={() => {
navigator.clipboard.writeText(token);
tokenGenerated();
}}
variant="default"
className="mt-2"
>
Continue
Copy and Continue
</Button>
</div>
);
+18 -3
View File
@@ -162,6 +162,13 @@ const (
StepRunStatusSUCCEEDED StepRunStatus = "SUCCEEDED"
)
// Defines values for TenantEnvironment.
const (
Development TenantEnvironment = "development"
Local TenantEnvironment = "local"
Production TenantEnvironment = "production"
)
// Defines values for TenantMemberRole.
const (
ADMIN TenantMemberRole = "ADMIN"
@@ -511,11 +518,15 @@ type CreateTenantInviteRequest struct {
// CreateTenantRequest defines model for CreateTenantRequest.
type CreateTenantRequest struct {
EngineVersion *TenantVersion `json:"engineVersion,omitempty"`
EngineVersion *TenantVersion `json:"engineVersion,omitempty"`
Environment *TenantEnvironment `json:"environment,omitempty"`
// Name The name of the tenant.
Name string `json:"name" validate:"required"`
// OnboardingData Additional onboarding data to store with the tenant.
OnboardingData *map[string]interface{} `json:"onboardingData,omitempty"`
// Slug The slug of the tenant.
Slug string `json:"slug" validate:"required,hatchetName"`
UiVersion *TenantUIVersion `json:"uiVersion,omitempty"`
@@ -1007,8 +1018,9 @@ type Tenant struct {
AlertMemberEmails *bool `json:"alertMemberEmails,omitempty"`
// AnalyticsOptOut Whether the tenant has opted out of analytics.
AnalyticsOptOut *bool `json:"analyticsOptOut,omitempty"`
Metadata APIResourceMeta `json:"metadata"`
AnalyticsOptOut *bool `json:"analyticsOptOut,omitempty"`
Environment *TenantEnvironment `json:"environment,omitempty"`
Metadata APIResourceMeta `json:"metadata"`
// Name The name of the tenant.
Name string `json:"name"`
@@ -1054,6 +1066,9 @@ type TenantAlertingSettings struct {
Metadata APIResourceMeta `json:"metadata"`
}
// TenantEnvironment defines model for TenantEnvironment.
type TenantEnvironment string
// TenantInvite defines model for TenantInvite.
type TenantInvite struct {
// Email The email of the user to invite.
+45
View File
@@ -688,6 +688,49 @@ func (ns NullStickyStrategy) Value() (driver.Value, error) {
return string(ns.StickyStrategy), nil
}
type TenantEnvironment string
const (
TenantEnvironmentLocal TenantEnvironment = "local"
TenantEnvironmentDevelopment TenantEnvironment = "development"
TenantEnvironmentProduction TenantEnvironment = "production"
)
func (e *TenantEnvironment) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = TenantEnvironment(s)
case string:
*e = TenantEnvironment(s)
default:
return fmt.Errorf("unsupported scan type for TenantEnvironment: %T", src)
}
return nil
}
type NullTenantEnvironment struct {
TenantEnvironment TenantEnvironment `json:"TenantEnvironment"`
Valid bool `json:"valid"` // Valid is true if TenantEnvironment is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullTenantEnvironment) Scan(value interface{}) error {
if value == nil {
ns.TenantEnvironment, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.TenantEnvironment.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullTenantEnvironment) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.TenantEnvironment), nil
}
type TenantMajorEngineVersion string
const (
@@ -1666,6 +1709,8 @@ type Tenant struct {
DataRetentionPeriod string `json:"dataRetentionPeriod"`
SchedulerPartitionId pgtype.Text `json:"schedulerPartitionId"`
CanUpgradeV1 bool `json:"canUpgradeV1"`
OnboardingData []byte `json:"onboardingData"`
Environment NullTenantEnvironment `json:"environment"`
}
type TenantAlertEmailGroup struct {
+6 -3
View File
@@ -7,7 +7,7 @@ WITH active_controller_partitions AS (
WHERE
"lastHeartbeat" > NOW() - INTERVAL '1 minute'
)
INSERT INTO "Tenant" ("id", "name", "slug", "controllerPartitionId", "dataRetentionPeriod", "version", "uiVersion")
INSERT INTO "Tenant" ("id", "name", "slug", "controllerPartitionId", "dataRetentionPeriod", "version", "uiVersion", "onboardingData", "environment")
VALUES (
sqlc.arg('id')::uuid,
sqlc.arg('name')::text,
@@ -23,7 +23,9 @@ VALUES (
),
COALESCE(sqlc.narg('dataRetentionPeriod')::text, '720h'),
COALESCE(sqlc.narg('version')::"TenantMajorEngineVersion", 'V0'),
COALESCE(sqlc.narg('uiVersion')::"TenantMajorUIVersion", 'V0')
COALESCE(sqlc.narg('uiVersion')::"TenantMajorUIVersion", 'V0'),
sqlc.narg('onboardingData')::jsonb,
sqlc.narg('environment')::"TenantEnvironment"
)
RETURNING *;
@@ -604,7 +606,8 @@ SELECT
t."alertMemberEmails" as "alertMemberEmails",
t."analyticsOptOut" as "analyticsOptOut",
t."version" as "tenantVersion",
t."uiVersion" as "tenantUiVersion"
t."uiVersion" as "tenantUiVersion",
t."environment" as "tenantEnvironment"
FROM
"TenantMember" tm
JOIN
+39 -12
View File
@@ -83,7 +83,7 @@ WITH active_controller_partitions AS (
WHERE
"lastHeartbeat" > NOW() - INTERVAL '1 minute'
)
INSERT INTO "Tenant" ("id", "name", "slug", "controllerPartitionId", "dataRetentionPeriod", "version", "uiVersion")
INSERT INTO "Tenant" ("id", "name", "slug", "controllerPartitionId", "dataRetentionPeriod", "version", "uiVersion", "onboardingData", "environment")
VALUES (
$1::uuid,
$2::text,
@@ -99,9 +99,11 @@ VALUES (
),
COALESCE($4::text, '720h'),
COALESCE($5::"TenantMajorEngineVersion", 'V0'),
COALESCE($6::"TenantMajorUIVersion", 'V0')
COALESCE($6::"TenantMajorUIVersion", 'V0'),
$7::jsonb,
$8::"TenantEnvironment"
)
RETURNING id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1"
RETURNING id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1", "onboardingData", environment
`
type CreateTenantParams struct {
@@ -111,6 +113,8 @@ type CreateTenantParams struct {
DataRetentionPeriod pgtype.Text `json:"dataRetentionPeriod"`
Version NullTenantMajorEngineVersion `json:"version"`
UiVersion NullTenantMajorUIVersion `json:"uiVersion"`
OnboardingData []byte `json:"onboardingData"`
Environment NullTenantEnvironment `json:"environment"`
}
func (q *Queries) CreateTenant(ctx context.Context, db DBTX, arg CreateTenantParams) (*Tenant, error) {
@@ -121,6 +125,8 @@ func (q *Queries) CreateTenant(ctx context.Context, db DBTX, arg CreateTenantPar
arg.DataRetentionPeriod,
arg.Version,
arg.UiVersion,
arg.OnboardingData,
arg.Environment,
)
var i Tenant
err := row.Scan(
@@ -139,6 +145,8 @@ func (q *Queries) CreateTenant(ctx context.Context, db DBTX, arg CreateTenantPar
&i.DataRetentionPeriod,
&i.SchedulerPartitionId,
&i.CanUpgradeV1,
&i.OnboardingData,
&i.Environment,
)
return &i, err
}
@@ -378,7 +386,7 @@ func (q *Queries) GetEmailGroups(ctx context.Context, db DBTX, tenantid pgtype.U
const getInternalTenantForController = `-- name: GetInternalTenantForController :one
SELECT
id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1"
id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1", "onboardingData", environment
FROM
"Tenant" as tenants
WHERE
@@ -405,6 +413,8 @@ func (q *Queries) GetInternalTenantForController(ctx context.Context, db DBTX, c
&i.DataRetentionPeriod,
&i.SchedulerPartitionId,
&i.CanUpgradeV1,
&i.OnboardingData,
&i.Environment,
)
return &i, err
}
@@ -530,7 +540,7 @@ func (q *Queries) GetTenantAlertingSettings(ctx context.Context, db DBTX, tenant
const getTenantByID = `-- name: GetTenantByID :one
SELECT
id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1"
id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1", "onboardingData", environment
FROM
"Tenant" as tenants
WHERE
@@ -556,13 +566,15 @@ func (q *Queries) GetTenantByID(ctx context.Context, db DBTX, id pgtype.UUID) (*
&i.DataRetentionPeriod,
&i.SchedulerPartitionId,
&i.CanUpgradeV1,
&i.OnboardingData,
&i.Environment,
)
return &i, err
}
const getTenantBySlug = `-- name: GetTenantBySlug :one
SELECT
id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1"
id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1", "onboardingData", environment
FROM
"Tenant" as tenants
WHERE
@@ -588,6 +600,8 @@ func (q *Queries) GetTenantBySlug(ctx context.Context, db DBTX, slug string) (*T
&i.DataRetentionPeriod,
&i.SchedulerPartitionId,
&i.CanUpgradeV1,
&i.OnboardingData,
&i.Environment,
)
return &i, err
}
@@ -885,7 +899,7 @@ func (q *Queries) ListTenantMembers(ctx context.Context, db DBTX, tenantid pgtyp
const listTenants = `-- name: ListTenants :many
SELECT
id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1"
id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1", "onboardingData", environment
FROM
"Tenant" as tenants
`
@@ -915,6 +929,8 @@ func (q *Queries) ListTenants(ctx context.Context, db DBTX) ([]*Tenant, error) {
&i.DataRetentionPeriod,
&i.SchedulerPartitionId,
&i.CanUpgradeV1,
&i.OnboardingData,
&i.Environment,
); err != nil {
return nil, err
}
@@ -928,7 +944,7 @@ func (q *Queries) ListTenants(ctx context.Context, db DBTX) ([]*Tenant, error) {
const listTenantsByControllerPartitionId = `-- name: ListTenantsByControllerPartitionId :many
SELECT
id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1"
id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1", "onboardingData", environment
FROM
"Tenant" as tenants
WHERE
@@ -966,6 +982,8 @@ func (q *Queries) ListTenantsByControllerPartitionId(ctx context.Context, db DBT
&i.DataRetentionPeriod,
&i.SchedulerPartitionId,
&i.CanUpgradeV1,
&i.OnboardingData,
&i.Environment,
); err != nil {
return nil, err
}
@@ -979,7 +997,7 @@ func (q *Queries) ListTenantsByControllerPartitionId(ctx context.Context, db DBT
const listTenantsBySchedulerPartitionId = `-- name: ListTenantsBySchedulerPartitionId :many
SELECT
id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1"
id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1", "onboardingData", environment
FROM
"Tenant" as tenants
WHERE
@@ -1017,6 +1035,8 @@ func (q *Queries) ListTenantsBySchedulerPartitionId(ctx context.Context, db DBTX
&i.DataRetentionPeriod,
&i.SchedulerPartitionId,
&i.CanUpgradeV1,
&i.OnboardingData,
&i.Environment,
); err != nil {
return nil, err
}
@@ -1030,7 +1050,7 @@ func (q *Queries) ListTenantsBySchedulerPartitionId(ctx context.Context, db DBTX
const listTenantsByTenantWorkerPartitionId = `-- name: ListTenantsByTenantWorkerPartitionId :many
SELECT
id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1"
id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1", "onboardingData", environment
FROM
"Tenant" as tenants
WHERE
@@ -1068,6 +1088,8 @@ func (q *Queries) ListTenantsByTenantWorkerPartitionId(ctx context.Context, db D
&i.DataRetentionPeriod,
&i.SchedulerPartitionId,
&i.CanUpgradeV1,
&i.OnboardingData,
&i.Environment,
); err != nil {
return nil, err
}
@@ -1092,7 +1114,8 @@ SELECT
t."alertMemberEmails" as "alertMemberEmails",
t."analyticsOptOut" as "analyticsOptOut",
t."version" as "tenantVersion",
t."uiVersion" as "tenantUiVersion"
t."uiVersion" as "tenantUiVersion",
t."environment" as "tenantEnvironment"
FROM
"TenantMember" tm
JOIN
@@ -1121,6 +1144,7 @@ type PopulateTenantMembersRow struct {
AnalyticsOptOut bool `json:"analyticsOptOut"`
TenantVersion TenantMajorEngineVersion `json:"tenantVersion"`
TenantUiVersion TenantMajorUIVersion `json:"tenantUiVersion"`
TenantEnvironment NullTenantEnvironment `json:"tenantEnvironment"`
}
func (q *Queries) PopulateTenantMembers(ctx context.Context, db DBTX, ids []pgtype.UUID) ([]*PopulateTenantMembersRow, error) {
@@ -1150,6 +1174,7 @@ func (q *Queries) PopulateTenantMembers(ctx context.Context, db DBTX, ids []pgty
&i.AnalyticsOptOut,
&i.TenantVersion,
&i.TenantUiVersion,
&i.TenantEnvironment,
); err != nil {
return nil, err
}
@@ -1440,7 +1465,7 @@ SET
"uiVersion" = COALESCE($5::"TenantMajorUIVersion", "uiVersion")
WHERE
"id" = $6::uuid
RETURNING id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1"
RETURNING id, "createdAt", "updatedAt", "deletedAt", version, "uiVersion", name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1", "onboardingData", environment
`
type UpdateTenantParams struct {
@@ -1478,6 +1503,8 @@ func (q *Queries) UpdateTenant(ctx context.Context, db DBTX, arg UpdateTenantPar
&i.DataRetentionPeriod,
&i.SchedulerPartitionId,
&i.CanUpgradeV1,
&i.OnboardingData,
&i.Environment,
)
return &i, err
}
+24
View File
@@ -71,6 +71,28 @@ func (r *tenantAPIRepository) CreateTenant(ctx context.Context, opts *repository
defer sqlchelpers.DeferRollback(context.Background(), r.l, tx.Rollback)
var onboardingDataBytes []byte
if opts.OnboardingData != nil {
onboardingDataBytes, err = json.Marshal(opts.OnboardingData)
if err != nil {
return nil, fmt.Errorf("failed to marshal onboarding data: %w", err)
}
}
var environment dbsqlc.NullTenantEnvironment
if opts.Environment != nil {
environment = dbsqlc.NullTenantEnvironment{
TenantEnvironment: dbsqlc.TenantEnvironment(*opts.Environment),
Valid: true,
}
} else {
// Default to development environment if none is specified
environment = dbsqlc.NullTenantEnvironment{
TenantEnvironment: dbsqlc.TenantEnvironmentDevelopment,
Valid: true,
}
}
createTenant, err := r.queries.CreateTenant(context.Background(), tx, dbsqlc.CreateTenantParams{
ID: sqlchelpers.UUIDFromStr(tenantId),
Slug: opts.Slug,
@@ -84,6 +106,8 @@ func (r *tenantAPIRepository) CreateTenant(ctx context.Context, opts *repository
TenantMajorUIVersion: uiVersion,
Valid: true,
},
OnboardingData: onboardingDataBytes,
Environment: environment,
})
if err != nil {
+6
View File
@@ -22,6 +22,12 @@ type CreateTenantOpts struct {
UIVersion *dbsqlc.TenantMajorUIVersion `validate:"omitempty"`
EngineVersion *dbsqlc.TenantMajorEngineVersion `validate:"omitempty"`
// (optional) the tenant environment type
Environment *string `validate:"omitempty,oneof=local development production"`
// (optional) additional onboarding data
OnboardingData map[string]interface{} `validate:"omitempty"`
}
type UpdateTenantOpts struct {
+45
View File
@@ -688,6 +688,49 @@ func (ns NullStickyStrategy) Value() (driver.Value, error) {
return string(ns.StickyStrategy), nil
}
type TenantEnvironment string
const (
TenantEnvironmentLocal TenantEnvironment = "local"
TenantEnvironmentDevelopment TenantEnvironment = "development"
TenantEnvironmentProduction TenantEnvironment = "production"
)
func (e *TenantEnvironment) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = TenantEnvironment(s)
case string:
*e = TenantEnvironment(s)
default:
return fmt.Errorf("unsupported scan type for TenantEnvironment: %T", src)
}
return nil
}
type NullTenantEnvironment struct {
TenantEnvironment TenantEnvironment `json:"TenantEnvironment"`
Valid bool `json:"valid"` // Valid is true if TenantEnvironment is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullTenantEnvironment) Scan(value interface{}) error {
if value == nil {
ns.TenantEnvironment, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.TenantEnvironment.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullTenantEnvironment) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.TenantEnvironment), nil
}
type TenantMajorEngineVersion string
const (
@@ -2507,6 +2550,8 @@ type Tenant struct {
DataRetentionPeriod string `json:"dataRetentionPeriod"`
SchedulerPartitionId pgtype.Text `json:"schedulerPartitionId"`
CanUpgradeV1 bool `json:"canUpgradeV1"`
OnboardingData []byte `json:"onboardingData"`
Environment NullTenantEnvironment `json:"environment"`
}
type TenantAlertEmailGroup struct {
+6
View File
@@ -107,6 +107,10 @@ CREATE TYPE "StickyStrategy" AS ENUM ('SOFT', 'HARD');
-- CreateEnum
CREATE TYPE "TenantMemberRole" AS ENUM ('OWNER', 'ADMIN', 'MEMBER');
-- CreateEnum
-- IMPORTANT: keep values in sync with api-contracts/openapi/components/schemas/tenant.yaml#TenantEnvironment
CREATE TYPE "TenantEnvironment" AS ENUM ('local', 'development', 'production');
-- CreateEnum
CREATE TYPE "TenantResourceLimitAlertType" AS ENUM ('Alarm', 'Exhausted');
@@ -614,6 +618,8 @@ CREATE TABLE "Tenant" (
"dataRetentionPeriod" TEXT NOT NULL DEFAULT '720h',
"schedulerPartitionId" TEXT,
"canUpgradeV1" BOOLEAN NOT NULL DEFAULT true,
"onboardingData" JSONB,
"environment" "TenantEnvironment",
CONSTRAINT "Tenant_pkey" PRIMARY KEY ("id")
);