mirror of
https://github.com/hatchet-dev/hatchet.git
synced 2026-05-24 04:18:38 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
Generated
+383
-316
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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+209
-98
@@ -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">
|
||||
|
||||
+9
-4
@@ -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
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user