mirror of
https://github.com/hatchet-dev/hatchet.git
synced 2025-12-20 08:10:26 -06:00
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:
@@ -21,6 +21,22 @@ func (t *TenantService) TenantUpdate(ctx echo.Context, request gen.TenantUpdateR
|
||||
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
|
||||
updateOpts := &repository.UpdateTenantOpts{}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -140,9 +140,8 @@ export function useTenant(): TenantContext {
|
||||
return;
|
||||
}
|
||||
|
||||
setLastRedirected(tenant?.slug);
|
||||
|
||||
if (tenant?.version == TenantVersion.V0 && pathname.startsWith('/v1')) {
|
||||
setLastRedirected(tenant?.slug);
|
||||
return navigate({
|
||||
pathname: pathname.replace('/v1', ''),
|
||||
search: params.toString(),
|
||||
@@ -150,6 +149,7 @@ export function useTenant(): TenantContext {
|
||||
}
|
||||
|
||||
if (tenant?.version == TenantVersion.V1 && !pathname.startsWith('/v1')) {
|
||||
setLastRedirected(tenant?.slug);
|
||||
return navigate({
|
||||
pathname: '/v1' + pathname,
|
||||
search: params.toString(),
|
||||
|
||||
@@ -1,121 +1,99 @@
|
||||
import MainNav from '@/components/molecules/nav-bar/nav-bar';
|
||||
import {
|
||||
LoaderFunctionArgs,
|
||||
Outlet,
|
||||
redirect,
|
||||
useLoaderData,
|
||||
} from 'react-router-dom';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import api, { queries } from '@/lib/api';
|
||||
import queryClient from '@/query-client';
|
||||
import { useContextFromParent } from '@/lib/outlet';
|
||||
import { Loading } from '@/components/ui/loading.tsx';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import SupportChat from '@/components/molecules/support-chat';
|
||||
import AnalyticsProvider from '@/components/molecules/analytics-provider';
|
||||
import { useState } from 'react';
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useContextFromParent } from '@/lib/outlet';
|
||||
|
||||
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({
|
||||
...queries.user.listTenantMemberships,
|
||||
});
|
||||
|
||||
const { user, memberships } = useLoaderData() as Awaited<
|
||||
ReturnType<typeof loader>
|
||||
>;
|
||||
|
||||
const ctx = useContextFromParent({
|
||||
user,
|
||||
memberships: listMembershipsQuery.data?.rows || memberships,
|
||||
user: userQuery.data,
|
||||
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 (
|
||||
<AnalyticsProvider user={user}>
|
||||
<SupportChat user={user}>
|
||||
<AnalyticsProvider user={userQuery.data}>
|
||||
<SupportChat user={userQuery.data}>
|
||||
<div className="flex flex-row flex-1 w-full h-full">
|
||||
<MainNav user={user} setHasBanner={setHasBanner} />
|
||||
<MainNav user={userQuery.data} setHasBanner={setHasBanner} />
|
||||
<div
|
||||
className={`${hasHasBanner ? 'pt-28' : 'pt-16'} flex-grow overflow-y-auto overflow-x-hidden`}
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { TenantContextType } from '@/lib/outlet';
|
||||
import { useState } from 'react';
|
||||
import { useLocation, useNavigate, useOutletContext } from 'react-router-dom';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { useApiError } from '@/lib/hooks';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import api, {
|
||||
@@ -16,8 +16,15 @@ import { Label } from '@radix-ui/react-label';
|
||||
import { Spinner } from '@/components/ui/loading';
|
||||
import { capitalize } from '@/lib/utils';
|
||||
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() {
|
||||
const { tenant } = useOutletContext<TenantContextType>();
|
||||
|
||||
@@ -40,57 +47,128 @@ export default function TenantSettings() {
|
||||
|
||||
const TenantVersionSwitcher = () => {
|
||||
const { tenant } = useOutletContext<TenantContextType>();
|
||||
const selectedVersion = tenant.version;
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
||||
const [upgradeRestrictedError, setUpgradeRestrictedError] =
|
||||
useState<boolean>(false);
|
||||
const { handleApiError } = useApiError({});
|
||||
|
||||
const { mutate: updateTenant, isPending } = useMutation({
|
||||
mutationKey: ['tenant:update'],
|
||||
mutationFn: async (data: UpdateTenantRequest) => {
|
||||
setUpgradeRestrictedError(false);
|
||||
await api.tenantUpdate(tenant.metadata.id, data);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
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 (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<h2 className="text-xl font-semibold leading-tight text-foreground">
|
||||
Tenant Version
|
||||
</h2>
|
||||
<RadioGroup
|
||||
disabled={isPending}
|
||||
value={selectedVersion}
|
||||
onValueChange={(value) => {
|
||||
updateTenant({
|
||||
version: value as TenantVersion,
|
||||
});
|
||||
<>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<h2 className="text-xl font-semibold leading-tight text-foreground">
|
||||
Tenant Version
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Upgrade your tenant to v1 to access new features and improvements. v1
|
||||
is currently in beta.
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => setShowUpgradeModal(true)}
|
||||
disabled={isPending}
|
||||
className="w-fit"
|
||||
>
|
||||
{isPending ? <Spinner /> : null}
|
||||
Upgrade to v1 (beta)
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
if (value === 'V1' && !pathname.includes('v1')) {
|
||||
navigate('/v1' + pathname);
|
||||
} else if (value === 'V0' && pathname.includes('v1')) {
|
||||
navigate(pathname.replace('/v1', ''));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tenantVersions.map((version) => (
|
||||
<div key={version} className="flex items-center space-x-2">
|
||||
<RadioGroupItem value={version} id={version.toLowerCase()} />
|
||||
<Label htmlFor={version.toLowerCase()}>{version}</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<Dialog open={showUpgradeModal} onOpenChange={setShowUpgradeModal}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Upgrade to v1 (beta)</DialogTitle>
|
||||
</DialogHeader>
|
||||
{!upgradeRestrictedError && (
|
||||
<div className="space-y-4 py-4">
|
||||
<p className="text-sm">Upgrading your tenant to v1 will:</p>
|
||||
<ul className="list-disc list-inside text-sm space-y-2">
|
||||
<li>Enable new v1 features and improvements</li>
|
||||
<li>Redirect you to the v1 interface</li>
|
||||
</ul>
|
||||
<Alert variant="warn">
|
||||
<AlertTitle>Warning</AlertTitle>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Button } from '@/components/v1/ui/button';
|
||||
import { Separator } from '@/components/v1/ui/separator';
|
||||
import { TenantContextType } from '@/lib/outlet';
|
||||
import { useState } from 'react';
|
||||
import { useLocation, useNavigate, useOutletContext } from 'react-router-dom';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { useApiError } from '@/lib/hooks';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import api, {
|
||||
@@ -16,7 +16,14 @@ import { Label } from '@radix-ui/react-label';
|
||||
import { Spinner } from '@/components/v1/ui/loading';
|
||||
import { capitalize } from '@/lib/utils';
|
||||
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() {
|
||||
const { tenant } = useOutletContext<TenantContextType>();
|
||||
@@ -40,10 +47,8 @@ export default function TenantSettings() {
|
||||
|
||||
const TenantVersionSwitcher = () => {
|
||||
const { tenant } = useOutletContext<TenantContextType>();
|
||||
const selectedVersion = tenant.version;
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const [showDowngradeModal, setShowDowngradeModal] = useState(false);
|
||||
|
||||
const { handleApiError } = useApiError({});
|
||||
|
||||
@@ -56,41 +61,85 @@ const TenantVersionSwitcher = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queries.user.listTenantMemberships.queryKey,
|
||||
});
|
||||
|
||||
window.location.reload();
|
||||
},
|
||||
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 (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<h2 className="text-xl font-semibold leading-tight text-foreground">
|
||||
Tenant Version
|
||||
</h2>
|
||||
<RadioGroup
|
||||
disabled={isPending}
|
||||
value={selectedVersion}
|
||||
onValueChange={(value) => {
|
||||
updateTenant({
|
||||
version: value as TenantVersion,
|
||||
});
|
||||
<>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<h2 className="text-xl font-semibold leading-tight text-foreground">
|
||||
Tenant Version
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
You can downgrade your tenant to v0 if needed. Please help us improve
|
||||
v1 by reporting any bugs in our{' '}
|
||||
<a
|
||||
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')) {
|
||||
navigate('/v1' + pathname);
|
||||
} else if (value === 'V0' && pathname.includes('v1')) {
|
||||
navigate(pathname.replace('/v1', ''));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tenantVersions.map((version) => (
|
||||
<div key={version} className="flex items-center space-x-2">
|
||||
<RadioGroupItem value={version} id={version.toLowerCase()} />
|
||||
<Label htmlFor={version.toLowerCase()}>{version}</Label>
|
||||
<Dialog open={showDowngradeModal} onOpenChange={setShowDowngradeModal}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Downgrade to v0</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<Alert variant="warn">
|
||||
<AlertTitle>Warning</AlertTitle>
|
||||
<AlertDescription>
|
||||
Downgrading to v0 will remove access to v1 features and may
|
||||
affect your existing workflows. This action should only be taken
|
||||
if absolutely necessary.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DataTable } from '@/components/v1/molecules/data-table/data-table.tsx';
|
||||
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 { useQuery } from '@tanstack/react-query';
|
||||
import invariant from 'tiny-invariant';
|
||||
@@ -69,7 +69,7 @@ export function TaskRunsTable({
|
||||
createdAfter: createdAfterProp,
|
||||
initColumnVisibility = {},
|
||||
filterVisibility = {},
|
||||
refetchInterval = 100000,
|
||||
refetchInterval = 5000,
|
||||
showMetrics = false,
|
||||
showCounts = true,
|
||||
disableTaskRunPagination = false,
|
||||
@@ -106,6 +106,7 @@ export function TaskRunsTable({
|
||||
selectedRuns,
|
||||
numPages,
|
||||
isLoading: isTaskRunsLoading,
|
||||
isFetching: isTaskRunsFetching,
|
||||
refetch: refetchTaskRuns,
|
||||
getRowId,
|
||||
} = useTaskRuns({
|
||||
@@ -120,6 +121,7 @@ export function TaskRunsTable({
|
||||
metrics,
|
||||
tenantMetrics,
|
||||
isLoading: isMetricsLoading,
|
||||
isFetching: isMetricsFetching,
|
||||
refetch: refetchMetrics,
|
||||
} = useMetrics({
|
||||
workflow,
|
||||
@@ -153,7 +155,12 @@ export function TaskRunsTable({
|
||||
const hasTaskFiltersSelected = Object.values(v1TaskFilters).some(
|
||||
(filter) => !!filter,
|
||||
);
|
||||
const isLoading = isTaskRunsLoading || isMetricsLoading;
|
||||
|
||||
const hasLoaded = useMemo(() => {
|
||||
return !isTaskRunsLoading && !isMetricsLoading;
|
||||
}, [isTaskRunsLoading, isMetricsLoading]);
|
||||
|
||||
const isFetching = !hasLoaded && (isTaskRunsFetching || isMetricsFetching);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -197,7 +204,7 @@ export function TaskRunsTable({
|
||||
code={JSON.stringify(tenantMetrics || '{}', null, 2)}
|
||||
/>
|
||||
)}
|
||||
{isMetricsLoading && <Skeleton className="w-full h-36" />}
|
||||
{isMetricsLoading && 'Loading...'}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
@@ -319,7 +326,7 @@ export function TaskRunsTable({
|
||||
)}
|
||||
<DataTable
|
||||
emptyState={<>No workflow runs found with the given filters.</>}
|
||||
isLoading={isLoading}
|
||||
isLoading={isFetching}
|
||||
columns={columns(cf.setAdditionalMetadata, onTaskRunIdClick)}
|
||||
columnVisibility={columnVisibility}
|
||||
setColumnVisibility={setColumnVisibility}
|
||||
|
||||
@@ -38,11 +38,8 @@ export const useMetrics = ({
|
||||
const tenantMetrics = tenantMetricsQuery.data?.queues || {};
|
||||
|
||||
return {
|
||||
isLoading:
|
||||
metricsQuery.isLoading ||
|
||||
metricsQuery.isFetching ||
|
||||
tenantMetricsQuery.isLoading ||
|
||||
tenantMetricsQuery.isFetching,
|
||||
isLoading: metricsQuery.isLoading || tenantMetricsQuery.isLoading,
|
||||
isFetching: metricsQuery.isFetching || tenantMetricsQuery.isFetching,
|
||||
tenantMetrics,
|
||||
metrics,
|
||||
refetch: () => {
|
||||
|
||||
@@ -10,8 +10,8 @@ export const usePagination = () => {
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex: Number(searchParams.get(pageSizeParamName)) || 0,
|
||||
pageSize: Number(searchParams.get(pageIndexParamName)) || 50,
|
||||
pageIndex: Number(searchParams.get(pageIndexParamName)) || 0,
|
||||
pageSize: Number(searchParams.get(pageSizeParamName)) || 50,
|
||||
}),
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
@@ -94,6 +94,7 @@ export const useTaskRuns = ({
|
||||
refetch: listTasksQuery.refetch,
|
||||
isLoading: listTasksQuery.isLoading,
|
||||
isError: listTasksQuery.isError,
|
||||
isFetching: listTasksQuery.isFetching,
|
||||
getRowId,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -52,7 +52,6 @@ export const routes: RouteObject[] = [
|
||||
lazy: async () =>
|
||||
import('./pages/onboarding/verify-email').then((res) => {
|
||||
return {
|
||||
loader: res.loader,
|
||||
Component: res.default,
|
||||
};
|
||||
}),
|
||||
@@ -62,7 +61,6 @@ export const routes: RouteObject[] = [
|
||||
lazy: async () =>
|
||||
import('./pages/authenticated').then((res) => {
|
||||
return {
|
||||
loader: res.loader,
|
||||
Component: res.default,
|
||||
};
|
||||
}),
|
||||
@@ -100,7 +98,6 @@ export const routes: RouteObject[] = [
|
||||
lazy: async () =>
|
||||
import('./pages/onboarding/invites').then((res) => {
|
||||
return {
|
||||
loader: res.loader,
|
||||
Component: res.default,
|
||||
};
|
||||
}),
|
||||
@@ -343,7 +340,6 @@ export const routes: RouteObject[] = [
|
||||
lazy: async () =>
|
||||
import('./pages/authenticated').then((res) => {
|
||||
return {
|
||||
loader: res.loader,
|
||||
Component: res.default,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -181,6 +181,9 @@ type ConfigFileRuntime struct {
|
||||
QueueStepRunBuffer buffer.ConfigFileBuffer `mapstructure:"queueStepRunBuffer" json:"queueStepRunBuffer,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 {
|
||||
@@ -526,6 +529,7 @@ func BindAllEnv(v *viper.Viper) {
|
||||
_ = v.BindEnv("runtime.bufferCreateWorkflowRuns", "SERVER_BUFFER_CREATE_WORKFLOW_RUNS")
|
||||
_ = v.BindEnv("runtime.disableTenantPubs", "SERVER_DISABLE_TENANT_PUBS")
|
||||
_ = v.BindEnv("runtime.maxInternalRetryCount", "SERVER_MAX_INTERNAL_RETRY_COUNT")
|
||||
_ = v.BindEnv("runtime.preventTenantVersionUpgrade", "SERVER_PREVENT_TENANT_VERSION_UPGRADE")
|
||||
|
||||
// security check options
|
||||
_ = v.BindEnv("securityCheck.enabled", "SERVER_SECURITY_CHECK_ENABLED")
|
||||
|
||||
@@ -1619,6 +1619,7 @@ type Tenant struct {
|
||||
WorkerPartitionId pgtype.Text `json:"workerPartitionId"`
|
||||
DataRetentionPeriod string `json:"dataRetentionPeriod"`
|
||||
SchedulerPartitionId pgtype.Text `json:"schedulerPartitionId"`
|
||||
CanUpgradeV1 bool `json:"canUpgradeV1"`
|
||||
}
|
||||
|
||||
type TenantAlertEmailGroup struct {
|
||||
|
||||
@@ -99,7 +99,7 @@ VALUES (
|
||||
),
|
||||
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 {
|
||||
@@ -131,6 +131,7 @@ func (q *Queries) CreateTenant(ctx context.Context, db DBTX, arg CreateTenantPar
|
||||
&i.WorkerPartitionId,
|
||||
&i.DataRetentionPeriod,
|
||||
&i.SchedulerPartitionId,
|
||||
&i.CanUpgradeV1,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
@@ -370,7 +371,7 @@ func (q *Queries) GetEmailGroups(ctx context.Context, db DBTX, tenantid pgtype.U
|
||||
|
||||
const getInternalTenantForController = `-- name: GetInternalTenantForController :one
|
||||
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
|
||||
"Tenant" as tenants
|
||||
WHERE
|
||||
@@ -395,6 +396,7 @@ func (q *Queries) GetInternalTenantForController(ctx context.Context, db DBTX, c
|
||||
&i.WorkerPartitionId,
|
||||
&i.DataRetentionPeriod,
|
||||
&i.SchedulerPartitionId,
|
||||
&i.CanUpgradeV1,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
@@ -520,7 +522,7 @@ func (q *Queries) GetTenantAlertingSettings(ctx context.Context, db DBTX, tenant
|
||||
|
||||
const getTenantByID = `-- name: GetTenantByID :one
|
||||
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
|
||||
"Tenant" as tenants
|
||||
WHERE
|
||||
@@ -544,13 +546,14 @@ func (q *Queries) GetTenantByID(ctx context.Context, db DBTX, id pgtype.UUID) (*
|
||||
&i.WorkerPartitionId,
|
||||
&i.DataRetentionPeriod,
|
||||
&i.SchedulerPartitionId,
|
||||
&i.CanUpgradeV1,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
const getTenantBySlug = `-- name: GetTenantBySlug :one
|
||||
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
|
||||
"Tenant" as tenants
|
||||
WHERE
|
||||
@@ -574,6 +577,7 @@ func (q *Queries) GetTenantBySlug(ctx context.Context, db DBTX, slug string) (*T
|
||||
&i.WorkerPartitionId,
|
||||
&i.DataRetentionPeriod,
|
||||
&i.SchedulerPartitionId,
|
||||
&i.CanUpgradeV1,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
@@ -871,7 +875,7 @@ func (q *Queries) ListTenantMembers(ctx context.Context, db DBTX, tenantid pgtyp
|
||||
|
||||
const listTenants = `-- name: ListTenants :many
|
||||
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
|
||||
"Tenant" as tenants
|
||||
`
|
||||
@@ -899,6 +903,7 @@ func (q *Queries) ListTenants(ctx context.Context, db DBTX) ([]*Tenant, error) {
|
||||
&i.WorkerPartitionId,
|
||||
&i.DataRetentionPeriod,
|
||||
&i.SchedulerPartitionId,
|
||||
&i.CanUpgradeV1,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -912,7 +917,7 @@ func (q *Queries) ListTenants(ctx context.Context, db DBTX) ([]*Tenant, error) {
|
||||
|
||||
const listTenantsByControllerPartitionId = `-- name: ListTenantsByControllerPartitionId :many
|
||||
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
|
||||
"Tenant" as tenants
|
||||
WHERE
|
||||
@@ -948,6 +953,7 @@ func (q *Queries) ListTenantsByControllerPartitionId(ctx context.Context, db DBT
|
||||
&i.WorkerPartitionId,
|
||||
&i.DataRetentionPeriod,
|
||||
&i.SchedulerPartitionId,
|
||||
&i.CanUpgradeV1,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -961,7 +967,7 @@ func (q *Queries) ListTenantsByControllerPartitionId(ctx context.Context, db DBT
|
||||
|
||||
const listTenantsBySchedulerPartitionId = `-- name: ListTenantsBySchedulerPartitionId :many
|
||||
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
|
||||
"Tenant" as tenants
|
||||
WHERE
|
||||
@@ -997,6 +1003,7 @@ func (q *Queries) ListTenantsBySchedulerPartitionId(ctx context.Context, db DBTX
|
||||
&i.WorkerPartitionId,
|
||||
&i.DataRetentionPeriod,
|
||||
&i.SchedulerPartitionId,
|
||||
&i.CanUpgradeV1,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1010,7 +1017,7 @@ func (q *Queries) ListTenantsBySchedulerPartitionId(ctx context.Context, db DBTX
|
||||
|
||||
const listTenantsByTenantWorkerPartitionId = `-- name: ListTenantsByTenantWorkerPartitionId :many
|
||||
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
|
||||
"Tenant" as tenants
|
||||
WHERE
|
||||
@@ -1046,6 +1053,7 @@ func (q *Queries) ListTenantsByTenantWorkerPartitionId(ctx context.Context, db D
|
||||
&i.WorkerPartitionId,
|
||||
&i.DataRetentionPeriod,
|
||||
&i.SchedulerPartitionId,
|
||||
&i.CanUpgradeV1,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1414,7 +1422,7 @@ SET
|
||||
"version" = COALESCE($4::"TenantMajorEngineVersion", "version")
|
||||
WHERE
|
||||
"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 {
|
||||
@@ -1448,6 +1456,7 @@ func (q *Queries) UpdateTenant(ctx context.Context, db DBTX, arg UpdateTenantPar
|
||||
&i.WorkerPartitionId,
|
||||
&i.DataRetentionPeriod,
|
||||
&i.SchedulerPartitionId,
|
||||
&i.CanUpgradeV1,
|
||||
)
|
||||
return &i, err
|
||||
}
|
||||
|
||||
@@ -2199,6 +2199,7 @@ type Tenant struct {
|
||||
WorkerPartitionId pgtype.Text `json:"workerPartitionId"`
|
||||
DataRetentionPeriod string `json:"dataRetentionPeriod"`
|
||||
SchedulerPartitionId pgtype.Text `json:"schedulerPartitionId"`
|
||||
CanUpgradeV1 bool `json:"canUpgradeV1"`
|
||||
}
|
||||
|
||||
type TenantAlertEmailGroup struct {
|
||||
|
||||
@@ -604,6 +604,7 @@ CREATE TABLE "Tenant" (
|
||||
"workerPartitionId" TEXT,
|
||||
"dataRetentionPeriod" TEXT NOT NULL DEFAULT '720h',
|
||||
"schedulerPartitionId" TEXT,
|
||||
"canUpgradeV1" BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
CONSTRAINT "Tenant_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user