Feat v1 UI tweaks (#1344)

* fix: drop uncached loader

* feat: upgrade modal

* add beta

* hacky feature flag

* fix: build

* refetch interval

* 5s

* stop flashing on load

* lint

* fix: map

* fix: last redir

* nil check

* small styling and wording things, change default canUpgrade -> true

* switch link to github discussion

---------

Co-authored-by: Alexander Belanger <alexander@hatchet.run>
This commit is contained in:
Gabe Ruttner
2025-03-15 09:23:32 -04:00
committed by GitHub
parent 5c647e247e
commit 3670b94fc4
16 changed files with 341 additions and 194 deletions

View File

@@ -21,6 +21,22 @@ func (t *TenantService) TenantUpdate(ctx echo.Context, request gen.TenantUpdateR
return gen.TenantUpdate400JSONResponse(*apiErrors), nil return gen.TenantUpdate400JSONResponse(*apiErrors), nil
} }
// check if the tenant version is being changed
if t.config.Runtime.PreventTenantVersionUpgrade &&
request.Body.Version != nil &&
*request.Body.Version == "V1" &&
!tenant.CanUpgradeV1 {
code := uint64(403)
message := "Tenant version upgrade is not enabled for this tenant"
return gen.TenantUpdate403JSONResponse(
gen.APIError{
Code: &code,
Description: message,
},
), nil
}
// construct the database query // construct the database query
updateOpts := &repository.UpdateTenantOpts{} updateOpts := &repository.UpdateTenantOpts{}

View File

@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE "Tenant" ADD COLUMN IF NOT EXISTS "canUpgradeV1" BOOLEAN NOT NULL DEFAULT true;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE "Tenant" DROP COLUMN IF EXISTS "canUpgradeV1";
-- +goose StatementEnd

View File

@@ -140,9 +140,8 @@ export function useTenant(): TenantContext {
return; return;
} }
setLastRedirected(tenant?.slug);
if (tenant?.version == TenantVersion.V0 && pathname.startsWith('/v1')) { if (tenant?.version == TenantVersion.V0 && pathname.startsWith('/v1')) {
setLastRedirected(tenant?.slug);
return navigate({ return navigate({
pathname: pathname.replace('/v1', ''), pathname: pathname.replace('/v1', ''),
search: params.toString(), search: params.toString(),
@@ -150,6 +149,7 @@ export function useTenant(): TenantContext {
} }
if (tenant?.version == TenantVersion.V1 && !pathname.startsWith('/v1')) { if (tenant?.version == TenantVersion.V1 && !pathname.startsWith('/v1')) {
setLastRedirected(tenant?.slug);
return navigate({ return navigate({
pathname: '/v1' + pathname, pathname: '/v1' + pathname,
search: params.toString(), search: params.toString(),

View File

@@ -1,121 +1,99 @@
import MainNav from '@/components/molecules/nav-bar/nav-bar'; import MainNav from '@/components/molecules/nav-bar/nav-bar';
import { import { Outlet } from 'react-router-dom';
LoaderFunctionArgs,
Outlet,
redirect,
useLoaderData,
} from 'react-router-dom';
import api, { queries } from '@/lib/api'; import api, { queries } from '@/lib/api';
import queryClient from '@/query-client';
import { useContextFromParent } from '@/lib/outlet';
import { Loading } from '@/components/ui/loading.tsx'; import { Loading } from '@/components/ui/loading.tsx';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import SupportChat from '@/components/molecules/support-chat'; import SupportChat from '@/components/molecules/support-chat';
import AnalyticsProvider from '@/components/molecules/analytics-provider'; import AnalyticsProvider from '@/components/molecules/analytics-provider';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { useContextFromParent } from '@/lib/outlet';
const authMiddleware = async (currentUrl: string) => {
try {
const user = await queryClient.fetchQuery({
queryKey: ['user:get:current'],
queryFn: async () => {
const res = await api.userGetCurrent();
return res.data;
},
});
if (
!user.emailVerified &&
!currentUrl.includes('/onboarding/verify-email')
) {
throw redirect('/onboarding/verify-email');
}
return user;
} catch (error) {
if (error instanceof Response) {
throw error;
} else if (
!currentUrl.includes('/auth/login') &&
!currentUrl.includes('/auth/register')
) {
throw redirect('/auth/login');
}
}
};
const invitesRedirector = async (currentUrl: string) => {
try {
const res = await api.userListTenantInvites();
const invites = res.data.rows || [];
if (invites.length > 0 && !currentUrl.includes('/onboarding/invites')) {
throw redirect('/onboarding/invites');
}
return;
} catch (error) {
if (error instanceof Response) {
throw error;
}
}
};
const membershipsPopulator = async (currentUrl: string) => {
try {
const res = await api.tenantMembershipsList();
const memberships = res.data;
if (memberships.rows?.length === 0 && !currentUrl.includes('/onboarding')) {
throw redirect('/onboarding/create-tenant');
}
return res.data.rows;
} catch (error) {
if (error instanceof Response) {
throw error;
}
}
};
export async function loader({ request }: LoaderFunctionArgs) {
const user = await authMiddleware(request.url);
await invitesRedirector(request.url);
const memberships = await membershipsPopulator(request.url);
return {
user,
memberships,
};
}
export default function Authenticated() { export default function Authenticated() {
const [hasHasBanner, setHasBanner] = useState(false);
const userQuery = useQuery({
queryKey: ['user:get:current'],
queryFn: async () => {
const res = await api.userGetCurrent();
return res.data;
},
});
const invitesQuery = useQuery({
queryKey: ['user:list-tenant-invites'],
queryFn: async () => {
const res = await api.userListTenantInvites();
return res.data.rows || [];
},
});
const listMembershipsQuery = useQuery({ const listMembershipsQuery = useQuery({
...queries.user.listTenantMemberships, ...queries.user.listTenantMemberships,
}); });
const { user, memberships } = useLoaderData() as Awaited<
ReturnType<typeof loader>
>;
const ctx = useContextFromParent({ const ctx = useContextFromParent({
user, user: userQuery.data,
memberships: listMembershipsQuery.data?.rows || memberships, memberships: listMembershipsQuery.data?.rows,
}); });
const [hasHasBanner, setHasBanner] = useState(false); useEffect(() => {
const currentUrl = window.location.pathname;
if (!user || !memberships) { if (
userQuery.data &&
!userQuery.data.emailVerified &&
!currentUrl.includes('/onboarding/verify-email')
) {
window.location.href = '/onboarding/verify-email';
return;
}
if (
invitesQuery.data?.length &&
invitesQuery.data.length > 0 &&
!currentUrl.includes('/onboarding/invites')
) {
window.location.href = '/onboarding/invites';
return;
}
if (
listMembershipsQuery.data?.rows?.length === 0 &&
!currentUrl.includes('/onboarding')
) {
window.location.href = '/onboarding/create-tenant';
return;
}
}, [userQuery.data, invitesQuery.data, listMembershipsQuery.data]);
if (
userQuery.isLoading ||
invitesQuery.isLoading ||
listMembershipsQuery.isLoading
) {
return <Loading />;
}
if (userQuery.error) {
const currentUrl = window.location.pathname;
if (
!currentUrl.includes('/auth/login') &&
!currentUrl.includes('/auth/register')
) {
window.location.href = '/auth/login';
return null;
}
}
if (!userQuery.data || !listMembershipsQuery.data?.rows) {
return <Loading />; return <Loading />;
} }
return ( return (
<AnalyticsProvider user={user}> <AnalyticsProvider user={userQuery.data}>
<SupportChat user={user}> <SupportChat user={userQuery.data}>
<div className="flex flex-row flex-1 w-full h-full"> <div className="flex flex-row flex-1 w-full h-full">
<MainNav user={user} setHasBanner={setHasBanner} /> <MainNav user={userQuery.data} setHasBanner={setHasBanner} />
<div <div
className={`${hasHasBanner ? 'pt-28' : 'pt-16'} flex-grow overflow-y-auto overflow-x-hidden`} className={`${hasHasBanner ? 'pt-28' : 'pt-16'} flex-grow overflow-y-auto overflow-x-hidden`}
> >

View File

@@ -2,7 +2,7 @@ import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { TenantContextType } from '@/lib/outlet'; import { TenantContextType } from '@/lib/outlet';
import { useState } from 'react'; import { useState } from 'react';
import { useLocation, useNavigate, useOutletContext } from 'react-router-dom'; import { useOutletContext } from 'react-router-dom';
import { useApiError } from '@/lib/hooks'; import { useApiError } from '@/lib/hooks';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import api, { import api, {
@@ -16,8 +16,15 @@ import { Label } from '@radix-ui/react-label';
import { Spinner } from '@/components/ui/loading'; import { Spinner } from '@/components/ui/loading';
import { capitalize } from '@/lib/utils'; import { capitalize } from '@/lib/utils';
import { UpdateTenantForm } from './components/update-tenant-form'; import { UpdateTenantForm } from './components/update-tenant-form';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { AxiosError } from 'axios';
export default function TenantSettings() { export default function TenantSettings() {
const { tenant } = useOutletContext<TenantContextType>(); const { tenant } = useOutletContext<TenantContextType>();
@@ -40,57 +47,128 @@ export default function TenantSettings() {
const TenantVersionSwitcher = () => { const TenantVersionSwitcher = () => {
const { tenant } = useOutletContext<TenantContextType>(); const { tenant } = useOutletContext<TenantContextType>();
const selectedVersion = tenant.version;
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const navigate = useNavigate(); const [showUpgradeModal, setShowUpgradeModal] = useState(false);
const { pathname } = useLocation(); const [upgradeRestrictedError, setUpgradeRestrictedError] =
useState<boolean>(false);
const { handleApiError } = useApiError({}); const { handleApiError } = useApiError({});
const { mutate: updateTenant, isPending } = useMutation({ const { mutate: updateTenant, isPending } = useMutation({
mutationKey: ['tenant:update'], mutationKey: ['tenant:update'],
mutationFn: async (data: UpdateTenantRequest) => { mutationFn: async (data: UpdateTenantRequest) => {
setUpgradeRestrictedError(false);
await api.tenantUpdate(tenant.metadata.id, data); await api.tenantUpdate(tenant.metadata.id, data);
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: queries.user.listTenantMemberships.queryKey, queryKey: queries.user.listTenantMemberships.queryKey,
}); });
window.location.reload();
},
onError: (error: AxiosError) => {
if (error.response?.status === 403) {
setUpgradeRestrictedError(true);
} else {
setShowUpgradeModal(false);
handleApiError(error);
}
}, },
onError: handleApiError,
}); });
const tenantVersions = Object.keys(TenantVersion) as Array<
keyof typeof TenantVersion // Only show for V0 tenants
>; if (tenant.version === TenantVersion.V1) {
return null;
}
return ( return (
<div className="flex flex-col gap-y-2"> <>
<h2 className="text-xl font-semibold leading-tight text-foreground"> <div className="flex flex-col gap-y-4">
Tenant Version <h2 className="text-xl font-semibold leading-tight text-foreground">
</h2> Tenant Version
<RadioGroup </h2>
disabled={isPending} <p className="text-sm text-muted-foreground">
value={selectedVersion} Upgrade your tenant to v1 to access new features and improvements. v1
onValueChange={(value) => { is currently in beta.
updateTenant({ </p>
version: value as TenantVersion, <Button
}); onClick={() => setShowUpgradeModal(true)}
disabled={isPending}
className="w-fit"
>
{isPending ? <Spinner /> : null}
Upgrade to v1 (beta)
</Button>
</div>
if (value === 'V1' && !pathname.includes('v1')) { <Dialog open={showUpgradeModal} onOpenChange={setShowUpgradeModal}>
navigate('/v1' + pathname); <DialogContent className="sm:max-w-[425px]">
} else if (value === 'V0' && pathname.includes('v1')) { <DialogHeader>
navigate(pathname.replace('/v1', '')); <DialogTitle>Upgrade to v1 (beta)</DialogTitle>
} </DialogHeader>
}} {!upgradeRestrictedError && (
> <div className="space-y-4 py-4">
{tenantVersions.map((version) => ( <p className="text-sm">Upgrading your tenant to v1 will:</p>
<div key={version} className="flex items-center space-x-2"> <ul className="list-disc list-inside text-sm space-y-2">
<RadioGroupItem value={version} id={version.toLowerCase()} /> <li>Enable new v1 features and improvements</li>
<Label htmlFor={version.toLowerCase()}>{version}</Label> <li>Redirect you to the v1 interface</li>
</div> </ul>
))} <Alert variant="warn">
</RadioGroup> <AlertTitle>Warning</AlertTitle>
</div> <AlertDescription>
This upgrade will not automatically migrate your existing
workflows or in-progress runs. To ensure zero downtime during
the upgrade, please follow our migration guide which includes
steps for parallel operation of v0 and v1 environments.
</AlertDescription>
</Alert>
<p className="text-sm">
Please read our{' '}
<a
href="https://github.com/hatchet-dev/hatchet/discussions/1348"
target="_blank"
rel="noopener noreferrer"
className="text-indigo-400 hover:underline"
>
v1 preview announcement
</a>{' '}
before proceeding.
</p>
</div>
)}
{upgradeRestrictedError && (
<Alert variant="warn">
<AlertDescription>
Tenant version upgrade has been restricted for this tenant.
Please contact us to request upgrade referencing tenant id:{' '}
{tenant.metadata.id}
</AlertDescription>
</Alert>
)}
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowUpgradeModal(false)}
disabled={isPending}
>
Cancel
</Button>
<Button
onClick={() => {
updateTenant({
version: TenantVersion.V1,
});
}}
disabled={isPending}
>
{isPending ? <Spinner /> : null}
Confirm Upgrade
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
); );
}; };

View File

@@ -2,7 +2,7 @@ import { Button } from '@/components/v1/ui/button';
import { Separator } from '@/components/v1/ui/separator'; import { Separator } from '@/components/v1/ui/separator';
import { TenantContextType } from '@/lib/outlet'; import { TenantContextType } from '@/lib/outlet';
import { useState } from 'react'; import { useState } from 'react';
import { useLocation, useNavigate, useOutletContext } from 'react-router-dom'; import { useOutletContext } from 'react-router-dom';
import { useApiError } from '@/lib/hooks'; import { useApiError } from '@/lib/hooks';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import api, { import api, {
@@ -16,7 +16,14 @@ import { Label } from '@radix-ui/react-label';
import { Spinner } from '@/components/v1/ui/loading'; import { Spinner } from '@/components/v1/ui/loading';
import { capitalize } from '@/lib/utils'; import { capitalize } from '@/lib/utils';
import { UpdateTenantForm } from './components/update-tenant-form'; import { UpdateTenantForm } from './components/update-tenant-form';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/v1/ui/dialog';
import { Alert, AlertDescription, AlertTitle } from '@/components/v1/ui/alert';
export default function TenantSettings() { export default function TenantSettings() {
const { tenant } = useOutletContext<TenantContextType>(); const { tenant } = useOutletContext<TenantContextType>();
@@ -40,10 +47,8 @@ export default function TenantSettings() {
const TenantVersionSwitcher = () => { const TenantVersionSwitcher = () => {
const { tenant } = useOutletContext<TenantContextType>(); const { tenant } = useOutletContext<TenantContextType>();
const selectedVersion = tenant.version;
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const navigate = useNavigate(); const [showDowngradeModal, setShowDowngradeModal] = useState(false);
const { pathname } = useLocation();
const { handleApiError } = useApiError({}); const { handleApiError } = useApiError({});
@@ -56,41 +61,85 @@ const TenantVersionSwitcher = () => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: queries.user.listTenantMemberships.queryKey, queryKey: queries.user.listTenantMemberships.queryKey,
}); });
window.location.reload();
}, },
onError: handleApiError, onError: handleApiError,
}); });
const tenantVersions = Object.keys(TenantVersion) as Array<
keyof typeof TenantVersion // Only show for V1 tenants
>; if (tenant.version === TenantVersion.V0) {
return null;
}
return ( return (
<div className="flex flex-col gap-y-2"> <>
<h2 className="text-xl font-semibold leading-tight text-foreground"> <div className="flex flex-col gap-y-2">
Tenant Version <h2 className="text-xl font-semibold leading-tight text-foreground">
</h2> Tenant Version
<RadioGroup </h2>
disabled={isPending} <p className="text-sm text-muted-foreground">
value={selectedVersion} You can downgrade your tenant to v0 if needed. Please help us improve
onValueChange={(value) => { v1 by reporting any bugs in our{' '}
updateTenant({ <a
version: value as TenantVersion, href="https://github.com/hatchet-dev/hatchet/issues"
}); target="_blank"
rel="noopener noreferrer"
className="text-indigo-400 hover:underline"
>
Github issues.
</a>
</p>
<Button
onClick={() => setShowDowngradeModal(true)}
disabled={isPending}
variant="destructive"
className="w-fit"
>
{isPending ? <Spinner /> : null}
Downgrade to v0
</Button>
</div>
if (value === 'V1' && !pathname.includes('v1')) { <Dialog open={showDowngradeModal} onOpenChange={setShowDowngradeModal}>
navigate('/v1' + pathname); <DialogContent className="sm:max-w-[425px]">
} else if (value === 'V0' && pathname.includes('v1')) { <DialogHeader>
navigate(pathname.replace('/v1', '')); <DialogTitle>Downgrade to v0</DialogTitle>
} </DialogHeader>
}} <div className="space-y-4 py-4">
> <Alert variant="warn">
{tenantVersions.map((version) => ( <AlertTitle>Warning</AlertTitle>
<div key={version} className="flex items-center space-x-2"> <AlertDescription>
<RadioGroupItem value={version} id={version.toLowerCase()} /> Downgrading to v0 will remove access to v1 features and may
<Label htmlFor={version.toLowerCase()}>{version}</Label> affect your existing workflows. This action should only be taken
if absolutely necessary.
</AlertDescription>
</Alert>
</div> </div>
))} <DialogFooter>
</RadioGroup> <Button
</div> variant="outline"
onClick={() => setShowDowngradeModal(false)}
disabled={isPending}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={() => {
updateTenant({
version: TenantVersion.V0,
});
}}
disabled={isPending}
>
{isPending ? <Spinner /> : null}
Confirm Downgrade
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
); );
}; };

View File

@@ -1,6 +1,6 @@
import { DataTable } from '@/components/v1/molecules/data-table/data-table.tsx'; import { DataTable } from '@/components/v1/molecules/data-table/data-table.tsx';
import { columns } from './v1/task-runs-columns'; import { columns } from './v1/task-runs-columns';
import { useCallback, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { RowSelectionState, VisibilityState } from '@tanstack/react-table'; import { RowSelectionState, VisibilityState } from '@tanstack/react-table';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import invariant from 'tiny-invariant'; import invariant from 'tiny-invariant';
@@ -69,7 +69,7 @@ export function TaskRunsTable({
createdAfter: createdAfterProp, createdAfter: createdAfterProp,
initColumnVisibility = {}, initColumnVisibility = {},
filterVisibility = {}, filterVisibility = {},
refetchInterval = 100000, refetchInterval = 5000,
showMetrics = false, showMetrics = false,
showCounts = true, showCounts = true,
disableTaskRunPagination = false, disableTaskRunPagination = false,
@@ -106,6 +106,7 @@ export function TaskRunsTable({
selectedRuns, selectedRuns,
numPages, numPages,
isLoading: isTaskRunsLoading, isLoading: isTaskRunsLoading,
isFetching: isTaskRunsFetching,
refetch: refetchTaskRuns, refetch: refetchTaskRuns,
getRowId, getRowId,
} = useTaskRuns({ } = useTaskRuns({
@@ -120,6 +121,7 @@ export function TaskRunsTable({
metrics, metrics,
tenantMetrics, tenantMetrics,
isLoading: isMetricsLoading, isLoading: isMetricsLoading,
isFetching: isMetricsFetching,
refetch: refetchMetrics, refetch: refetchMetrics,
} = useMetrics({ } = useMetrics({
workflow, workflow,
@@ -153,7 +155,12 @@ export function TaskRunsTable({
const hasTaskFiltersSelected = Object.values(v1TaskFilters).some( const hasTaskFiltersSelected = Object.values(v1TaskFilters).some(
(filter) => !!filter, (filter) => !!filter,
); );
const isLoading = isTaskRunsLoading || isMetricsLoading;
const hasLoaded = useMemo(() => {
return !isTaskRunsLoading && !isMetricsLoading;
}, [isTaskRunsLoading, isMetricsLoading]);
const isFetching = !hasLoaded && (isTaskRunsFetching || isMetricsFetching);
return ( return (
<> <>
@@ -197,7 +204,7 @@ export function TaskRunsTable({
code={JSON.stringify(tenantMetrics || '{}', null, 2)} code={JSON.stringify(tenantMetrics || '{}', null, 2)}
/> />
)} )}
{isMetricsLoading && <Skeleton className="w-full h-36" />} {isMetricsLoading && 'Loading...'}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
)} )}
@@ -319,7 +326,7 @@ export function TaskRunsTable({
)} )}
<DataTable <DataTable
emptyState={<>No workflow runs found with the given filters.</>} emptyState={<>No workflow runs found with the given filters.</>}
isLoading={isLoading} isLoading={isFetching}
columns={columns(cf.setAdditionalMetadata, onTaskRunIdClick)} columns={columns(cf.setAdditionalMetadata, onTaskRunIdClick)}
columnVisibility={columnVisibility} columnVisibility={columnVisibility}
setColumnVisibility={setColumnVisibility} setColumnVisibility={setColumnVisibility}

View File

@@ -38,11 +38,8 @@ export const useMetrics = ({
const tenantMetrics = tenantMetricsQuery.data?.queues || {}; const tenantMetrics = tenantMetricsQuery.data?.queues || {};
return { return {
isLoading: isLoading: metricsQuery.isLoading || tenantMetricsQuery.isLoading,
metricsQuery.isLoading || isFetching: metricsQuery.isFetching || tenantMetricsQuery.isFetching,
metricsQuery.isFetching ||
tenantMetricsQuery.isLoading ||
tenantMetricsQuery.isFetching,
tenantMetrics, tenantMetrics,
metrics, metrics,
refetch: () => { refetch: () => {

View File

@@ -10,8 +10,8 @@ export const usePagination = () => {
const pagination = useMemo( const pagination = useMemo(
() => ({ () => ({
pageIndex: Number(searchParams.get(pageSizeParamName)) || 0, pageIndex: Number(searchParams.get(pageIndexParamName)) || 0,
pageSize: Number(searchParams.get(pageIndexParamName)) || 50, pageSize: Number(searchParams.get(pageSizeParamName)) || 50,
}), }),
[searchParams], [searchParams],
); );

View File

@@ -94,6 +94,7 @@ export const useTaskRuns = ({
refetch: listTasksQuery.refetch, refetch: listTasksQuery.refetch,
isLoading: listTasksQuery.isLoading, isLoading: listTasksQuery.isLoading,
isError: listTasksQuery.isError, isError: listTasksQuery.isError,
isFetching: listTasksQuery.isFetching,
getRowId, getRowId,
}; };
}; };

View File

@@ -52,7 +52,6 @@ export const routes: RouteObject[] = [
lazy: async () => lazy: async () =>
import('./pages/onboarding/verify-email').then((res) => { import('./pages/onboarding/verify-email').then((res) => {
return { return {
loader: res.loader,
Component: res.default, Component: res.default,
}; };
}), }),
@@ -62,7 +61,6 @@ export const routes: RouteObject[] = [
lazy: async () => lazy: async () =>
import('./pages/authenticated').then((res) => { import('./pages/authenticated').then((res) => {
return { return {
loader: res.loader,
Component: res.default, Component: res.default,
}; };
}), }),
@@ -100,7 +98,6 @@ export const routes: RouteObject[] = [
lazy: async () => lazy: async () =>
import('./pages/onboarding/invites').then((res) => { import('./pages/onboarding/invites').then((res) => {
return { return {
loader: res.loader,
Component: res.default, Component: res.default,
}; };
}), }),
@@ -343,7 +340,6 @@ export const routes: RouteObject[] = [
lazy: async () => lazy: async () =>
import('./pages/authenticated').then((res) => { import('./pages/authenticated').then((res) => {
return { return {
loader: res.loader,
Component: res.default, Component: res.default,
}; };
}), }),

View File

@@ -181,6 +181,9 @@ type ConfigFileRuntime struct {
QueueStepRunBuffer buffer.ConfigFileBuffer `mapstructure:"queueStepRunBuffer" json:"queueStepRunBuffer,omitempty"` QueueStepRunBuffer buffer.ConfigFileBuffer `mapstructure:"queueStepRunBuffer" json:"queueStepRunBuffer,omitempty"`
Monitoring ConfigFileMonitoring `mapstructure:"monitoring" json:"monitoring,omitempty"` Monitoring ConfigFileMonitoring `mapstructure:"monitoring" json:"monitoring,omitempty"`
// PreventTenantVersionUpgrade controls whether the server prevents tenant version upgrades
PreventTenantVersionUpgrade bool `mapstructure:"preventTenantVersionUpgrade" json:"preventTenantVersionUpgrade,omitempty" default:"false"`
} }
type InternalClientTLSConfigFile struct { type InternalClientTLSConfigFile struct {
@@ -526,6 +529,7 @@ func BindAllEnv(v *viper.Viper) {
_ = v.BindEnv("runtime.bufferCreateWorkflowRuns", "SERVER_BUFFER_CREATE_WORKFLOW_RUNS") _ = v.BindEnv("runtime.bufferCreateWorkflowRuns", "SERVER_BUFFER_CREATE_WORKFLOW_RUNS")
_ = v.BindEnv("runtime.disableTenantPubs", "SERVER_DISABLE_TENANT_PUBS") _ = v.BindEnv("runtime.disableTenantPubs", "SERVER_DISABLE_TENANT_PUBS")
_ = v.BindEnv("runtime.maxInternalRetryCount", "SERVER_MAX_INTERNAL_RETRY_COUNT") _ = v.BindEnv("runtime.maxInternalRetryCount", "SERVER_MAX_INTERNAL_RETRY_COUNT")
_ = v.BindEnv("runtime.preventTenantVersionUpgrade", "SERVER_PREVENT_TENANT_VERSION_UPGRADE")
// security check options // security check options
_ = v.BindEnv("securityCheck.enabled", "SERVER_SECURITY_CHECK_ENABLED") _ = v.BindEnv("securityCheck.enabled", "SERVER_SECURITY_CHECK_ENABLED")

View File

@@ -1619,6 +1619,7 @@ type Tenant struct {
WorkerPartitionId pgtype.Text `json:"workerPartitionId"` WorkerPartitionId pgtype.Text `json:"workerPartitionId"`
DataRetentionPeriod string `json:"dataRetentionPeriod"` DataRetentionPeriod string `json:"dataRetentionPeriod"`
SchedulerPartitionId pgtype.Text `json:"schedulerPartitionId"` SchedulerPartitionId pgtype.Text `json:"schedulerPartitionId"`
CanUpgradeV1 bool `json:"canUpgradeV1"`
} }
type TenantAlertEmailGroup struct { type TenantAlertEmailGroup struct {

View File

@@ -99,7 +99,7 @@ VALUES (
), ),
COALESCE($4::text, '720h') COALESCE($4::text, '720h')
) )
RETURNING id, "createdAt", "updatedAt", "deletedAt", version, name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId" RETURNING id, "createdAt", "updatedAt", "deletedAt", version, name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1"
` `
type CreateTenantParams struct { type CreateTenantParams struct {
@@ -131,6 +131,7 @@ func (q *Queries) CreateTenant(ctx context.Context, db DBTX, arg CreateTenantPar
&i.WorkerPartitionId, &i.WorkerPartitionId,
&i.DataRetentionPeriod, &i.DataRetentionPeriod,
&i.SchedulerPartitionId, &i.SchedulerPartitionId,
&i.CanUpgradeV1,
) )
return &i, err return &i, err
} }
@@ -370,7 +371,7 @@ func (q *Queries) GetEmailGroups(ctx context.Context, db DBTX, tenantid pgtype.U
const getInternalTenantForController = `-- name: GetInternalTenantForController :one const getInternalTenantForController = `-- name: GetInternalTenantForController :one
SELECT SELECT
id, "createdAt", "updatedAt", "deletedAt", version, name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId" id, "createdAt", "updatedAt", "deletedAt", version, name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1"
FROM FROM
"Tenant" as tenants "Tenant" as tenants
WHERE WHERE
@@ -395,6 +396,7 @@ func (q *Queries) GetInternalTenantForController(ctx context.Context, db DBTX, c
&i.WorkerPartitionId, &i.WorkerPartitionId,
&i.DataRetentionPeriod, &i.DataRetentionPeriod,
&i.SchedulerPartitionId, &i.SchedulerPartitionId,
&i.CanUpgradeV1,
) )
return &i, err return &i, err
} }
@@ -520,7 +522,7 @@ func (q *Queries) GetTenantAlertingSettings(ctx context.Context, db DBTX, tenant
const getTenantByID = `-- name: GetTenantByID :one const getTenantByID = `-- name: GetTenantByID :one
SELECT SELECT
id, "createdAt", "updatedAt", "deletedAt", version, name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId" id, "createdAt", "updatedAt", "deletedAt", version, name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1"
FROM FROM
"Tenant" as tenants "Tenant" as tenants
WHERE WHERE
@@ -544,13 +546,14 @@ func (q *Queries) GetTenantByID(ctx context.Context, db DBTX, id pgtype.UUID) (*
&i.WorkerPartitionId, &i.WorkerPartitionId,
&i.DataRetentionPeriod, &i.DataRetentionPeriod,
&i.SchedulerPartitionId, &i.SchedulerPartitionId,
&i.CanUpgradeV1,
) )
return &i, err return &i, err
} }
const getTenantBySlug = `-- name: GetTenantBySlug :one const getTenantBySlug = `-- name: GetTenantBySlug :one
SELECT SELECT
id, "createdAt", "updatedAt", "deletedAt", version, name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId" id, "createdAt", "updatedAt", "deletedAt", version, name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1"
FROM FROM
"Tenant" as tenants "Tenant" as tenants
WHERE WHERE
@@ -574,6 +577,7 @@ func (q *Queries) GetTenantBySlug(ctx context.Context, db DBTX, slug string) (*T
&i.WorkerPartitionId, &i.WorkerPartitionId,
&i.DataRetentionPeriod, &i.DataRetentionPeriod,
&i.SchedulerPartitionId, &i.SchedulerPartitionId,
&i.CanUpgradeV1,
) )
return &i, err return &i, err
} }
@@ -871,7 +875,7 @@ func (q *Queries) ListTenantMembers(ctx context.Context, db DBTX, tenantid pgtyp
const listTenants = `-- name: ListTenants :many const listTenants = `-- name: ListTenants :many
SELECT SELECT
id, "createdAt", "updatedAt", "deletedAt", version, name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId" id, "createdAt", "updatedAt", "deletedAt", version, name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1"
FROM FROM
"Tenant" as tenants "Tenant" as tenants
` `
@@ -899,6 +903,7 @@ func (q *Queries) ListTenants(ctx context.Context, db DBTX) ([]*Tenant, error) {
&i.WorkerPartitionId, &i.WorkerPartitionId,
&i.DataRetentionPeriod, &i.DataRetentionPeriod,
&i.SchedulerPartitionId, &i.SchedulerPartitionId,
&i.CanUpgradeV1,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -912,7 +917,7 @@ func (q *Queries) ListTenants(ctx context.Context, db DBTX) ([]*Tenant, error) {
const listTenantsByControllerPartitionId = `-- name: ListTenantsByControllerPartitionId :many const listTenantsByControllerPartitionId = `-- name: ListTenantsByControllerPartitionId :many
SELECT SELECT
id, "createdAt", "updatedAt", "deletedAt", version, name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId" id, "createdAt", "updatedAt", "deletedAt", version, name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1"
FROM FROM
"Tenant" as tenants "Tenant" as tenants
WHERE WHERE
@@ -948,6 +953,7 @@ func (q *Queries) ListTenantsByControllerPartitionId(ctx context.Context, db DBT
&i.WorkerPartitionId, &i.WorkerPartitionId,
&i.DataRetentionPeriod, &i.DataRetentionPeriod,
&i.SchedulerPartitionId, &i.SchedulerPartitionId,
&i.CanUpgradeV1,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -961,7 +967,7 @@ func (q *Queries) ListTenantsByControllerPartitionId(ctx context.Context, db DBT
const listTenantsBySchedulerPartitionId = `-- name: ListTenantsBySchedulerPartitionId :many const listTenantsBySchedulerPartitionId = `-- name: ListTenantsBySchedulerPartitionId :many
SELECT SELECT
id, "createdAt", "updatedAt", "deletedAt", version, name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId" id, "createdAt", "updatedAt", "deletedAt", version, name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1"
FROM FROM
"Tenant" as tenants "Tenant" as tenants
WHERE WHERE
@@ -997,6 +1003,7 @@ func (q *Queries) ListTenantsBySchedulerPartitionId(ctx context.Context, db DBTX
&i.WorkerPartitionId, &i.WorkerPartitionId,
&i.DataRetentionPeriod, &i.DataRetentionPeriod,
&i.SchedulerPartitionId, &i.SchedulerPartitionId,
&i.CanUpgradeV1,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -1010,7 +1017,7 @@ func (q *Queries) ListTenantsBySchedulerPartitionId(ctx context.Context, db DBTX
const listTenantsByTenantWorkerPartitionId = `-- name: ListTenantsByTenantWorkerPartitionId :many const listTenantsByTenantWorkerPartitionId = `-- name: ListTenantsByTenantWorkerPartitionId :many
SELECT SELECT
id, "createdAt", "updatedAt", "deletedAt", version, name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId" id, "createdAt", "updatedAt", "deletedAt", version, name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1"
FROM FROM
"Tenant" as tenants "Tenant" as tenants
WHERE WHERE
@@ -1046,6 +1053,7 @@ func (q *Queries) ListTenantsByTenantWorkerPartitionId(ctx context.Context, db D
&i.WorkerPartitionId, &i.WorkerPartitionId,
&i.DataRetentionPeriod, &i.DataRetentionPeriod,
&i.SchedulerPartitionId, &i.SchedulerPartitionId,
&i.CanUpgradeV1,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -1414,7 +1422,7 @@ SET
"version" = COALESCE($4::"TenantMajorEngineVersion", "version") "version" = COALESCE($4::"TenantMajorEngineVersion", "version")
WHERE WHERE
"id" = $5::uuid "id" = $5::uuid
RETURNING id, "createdAt", "updatedAt", "deletedAt", version, name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId" RETURNING id, "createdAt", "updatedAt", "deletedAt", version, name, slug, "analyticsOptOut", "alertMemberEmails", "controllerPartitionId", "workerPartitionId", "dataRetentionPeriod", "schedulerPartitionId", "canUpgradeV1"
` `
type UpdateTenantParams struct { type UpdateTenantParams struct {
@@ -1448,6 +1456,7 @@ func (q *Queries) UpdateTenant(ctx context.Context, db DBTX, arg UpdateTenantPar
&i.WorkerPartitionId, &i.WorkerPartitionId,
&i.DataRetentionPeriod, &i.DataRetentionPeriod,
&i.SchedulerPartitionId, &i.SchedulerPartitionId,
&i.CanUpgradeV1,
) )
return &i, err return &i, err
} }

View File

@@ -2199,6 +2199,7 @@ type Tenant struct {
WorkerPartitionId pgtype.Text `json:"workerPartitionId"` WorkerPartitionId pgtype.Text `json:"workerPartitionId"`
DataRetentionPeriod string `json:"dataRetentionPeriod"` DataRetentionPeriod string `json:"dataRetentionPeriod"`
SchedulerPartitionId pgtype.Text `json:"schedulerPartitionId"` SchedulerPartitionId pgtype.Text `json:"schedulerPartitionId"`
CanUpgradeV1 bool `json:"canUpgradeV1"`
} }
type TenantAlertEmailGroup struct { type TenantAlertEmailGroup struct {

View File

@@ -604,6 +604,7 @@ CREATE TABLE "Tenant" (
"workerPartitionId" TEXT, "workerPartitionId" TEXT,
"dataRetentionPeriod" TEXT NOT NULL DEFAULT '720h', "dataRetentionPeriod" TEXT NOT NULL DEFAULT '720h',
"schedulerPartitionId" TEXT, "schedulerPartitionId" TEXT,
"canUpgradeV1" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "Tenant_pkey" PRIMARY KEY ("id") CONSTRAINT "Tenant_pkey" PRIMARY KEY ("id")
); );