mirror of
https://github.com/hatchet-dev/hatchet.git
synced 2025-12-16 22:35:11 -06:00
fix: frontend bugs relating to redirects/tenants (#25)
* fix: frontend bugs - Adds a query param `tenant` to application routes - Allows setting an environment variable `SERVER_AUTH_SET_EMAIL_VERIFIED` to true which automatically sets the email to verified for new signups, since most local installations won't have an email verification mechanism - When a user is logged in, navigating to `/auth/login` or `/auth/register` will redirect to the application via the no-auth.tsx middleware - When there are no events found, the backend will no longer respond with a `500`-level error, and will return 0 rows instead
This commit is contained in:
@@ -36,6 +36,7 @@ SERVER_URL=https://app.dev.hatchet-tools.com
|
||||
SERVER_AUTH_COOKIE_SECRETS="$(randstring 16) $(randstring 16)"
|
||||
SERVER_AUTH_COOKIE_DOMAIN=app.dev.hatchet-tools.com
|
||||
SERVER_AUTH_COOKIE_INSECURE=false
|
||||
SERVER_AUTH_SET_EMAIL_VERIFIED=true
|
||||
EOF
|
||||
```
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ func (u *UserService) UserCreate(ctx echo.Context, request gen.UserCreateRequest
|
||||
|
||||
createOpts := &repository.CreateUserOpts{
|
||||
Email: string(request.Body.Email),
|
||||
EmailVerified: repository.BoolPtr(false),
|
||||
EmailVerified: repository.BoolPtr(u.config.Auth.SetEmailVerified),
|
||||
Name: repository.StringPtr(request.Body.Name),
|
||||
Password: *hashedPw,
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ export const queries = createQueryKeyStore({
|
||||
queryKey: ['user:get'],
|
||||
queryFn: async () => (await api.userGetCurrent()).data,
|
||||
},
|
||||
listTenantMemberships: {
|
||||
queryKey: ['tenant-memberships:list'],
|
||||
queryFn: async () => (await api.tenantMembershipsList()).data,
|
||||
},
|
||||
},
|
||||
workflows: {
|
||||
list: (tenant: string) => ({
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { atom } from 'jotai';
|
||||
import { Tenant } from './api';
|
||||
import { atom, useAtom } from 'jotai';
|
||||
import { Tenant, queries } from './api';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
const getInitialValue = <T>(key: string): T | undefined => {
|
||||
const item = localStorage.getItem(key);
|
||||
@@ -11,14 +14,110 @@ const getInitialValue = <T>(key: string): T | undefined => {
|
||||
return;
|
||||
};
|
||||
|
||||
const currTenantKey = 'currTenant';
|
||||
const lastTenantKey = 'lastTenant';
|
||||
|
||||
const currTenantAtomInit = atom(getInitialValue<Tenant>(currTenantKey));
|
||||
const lastTenantAtomInit = atom(getInitialValue<Tenant>(lastTenantKey));
|
||||
|
||||
export const currTenantAtom = atom(
|
||||
(get) => get(currTenantAtomInit),
|
||||
export const lastTenantAtom = atom(
|
||||
(get) => get(lastTenantAtomInit),
|
||||
(_get, set, newVal: Tenant) => {
|
||||
set(currTenantAtomInit, newVal);
|
||||
localStorage.setItem(currTenantKey, JSON.stringify(newVal));
|
||||
set(lastTenantAtomInit, newVal);
|
||||
localStorage.setItem(lastTenantKey, JSON.stringify(newVal));
|
||||
},
|
||||
);
|
||||
|
||||
// search param sets the tenant, the last tenant set is used if the search param is empty,
|
||||
// otherwise the first membership is used
|
||||
export function useTenantContext(): [
|
||||
Tenant | undefined,
|
||||
(tenant: Tenant) => void,
|
||||
] {
|
||||
const [lastTenant, setLastTenant] = useAtom(lastTenantAtom);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [currTenant, setCurrTenant] = useState<Tenant>();
|
||||
|
||||
const listMembershipsQuery = useQuery({
|
||||
...queries.user.listTenantMemberships,
|
||||
});
|
||||
|
||||
const memberships = useMemo(() => {
|
||||
return listMembershipsQuery.data?.rows || [];
|
||||
}, [listMembershipsQuery]);
|
||||
|
||||
const computedCurrTenant = useMemo(() => {
|
||||
const findTenant = (tenantId: string) => {
|
||||
return memberships?.find((m) => m.tenant?.metadata.id === tenantId)
|
||||
?.tenant;
|
||||
};
|
||||
|
||||
const currTenantId = searchParams.get('tenant') || undefined;
|
||||
|
||||
if (currTenantId) {
|
||||
const tenant = findTenant(currTenantId);
|
||||
|
||||
if (tenant) {
|
||||
return tenant;
|
||||
}
|
||||
}
|
||||
|
||||
const lastTenantId = lastTenant?.metadata.id || undefined;
|
||||
|
||||
if (lastTenantId) {
|
||||
const tenant = findTenant(lastTenantId);
|
||||
|
||||
if (tenant) {
|
||||
return tenant;
|
||||
}
|
||||
}
|
||||
|
||||
const firstMembershipTenant = memberships?.[0]?.tenant;
|
||||
|
||||
return firstMembershipTenant;
|
||||
}, [memberships, lastTenant?.metadata.id, searchParams]);
|
||||
|
||||
// sets the current tenant if the search param changes
|
||||
useEffect(() => {
|
||||
if (searchParams.get('tenant') !== currTenant?.metadata.id) {
|
||||
const newTenant = memberships?.find(
|
||||
(m) => m.tenant?.metadata.id === searchParams.get('tenant'),
|
||||
)?.tenant;
|
||||
|
||||
if (newTenant) {
|
||||
setCurrTenant(newTenant);
|
||||
} else if (computedCurrTenant?.metadata.id) {
|
||||
const newSearchParams = new URLSearchParams(searchParams);
|
||||
newSearchParams.set('tenant', computedCurrTenant?.metadata.id);
|
||||
setSearchParams(newSearchParams);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
searchParams,
|
||||
currTenant,
|
||||
setCurrTenant,
|
||||
memberships,
|
||||
computedCurrTenant,
|
||||
setSearchParams,
|
||||
]);
|
||||
|
||||
// sets the current tenant to the initial tenant
|
||||
useEffect(() => {
|
||||
if (!currTenant && computedCurrTenant) {
|
||||
setCurrTenant(computedCurrTenant);
|
||||
}
|
||||
}, [computedCurrTenant, currTenant, setCurrTenant]);
|
||||
|
||||
// keeps the current tenant in sync with the last tenant
|
||||
useEffect(() => {
|
||||
if (currTenant && lastTenant?.metadata.id !== currTenant?.metadata.id) {
|
||||
setLastTenant(currTenant);
|
||||
}
|
||||
}, [lastTenant, currTenant, setLastTenant]);
|
||||
|
||||
const setTenant = (tenant: Tenant) => {
|
||||
const newSearchParams = new URLSearchParams(searchParams);
|
||||
newSearchParams.set('tenant', tenant.metadata.id);
|
||||
setSearchParams(newSearchParams);
|
||||
};
|
||||
|
||||
return [currTenant || computedCurrTenant, setTenant];
|
||||
}
|
||||
|
||||
38
frontend/app/src/pages/auth/no-auth.tsx
Normal file
38
frontend/app/src/pages/auth/no-auth.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { redirect } from 'react-router-dom';
|
||||
import api from '@/lib/api';
|
||||
import queryClient from '@/query-client';
|
||||
import { AxiosError, isAxiosError } from 'axios';
|
||||
|
||||
const noAuthMiddleware = async () => {
|
||||
try {
|
||||
const user = await queryClient.fetchQuery({
|
||||
queryKey: ['user:get:current'],
|
||||
queryFn: async () => {
|
||||
const res = await api.userGetCurrent();
|
||||
|
||||
return res.data;
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
throw redirect('/');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Response) {
|
||||
throw error;
|
||||
} else if (isAxiosError(error)) {
|
||||
const axiosErr = error as AxiosError;
|
||||
|
||||
if (axiosErr.response?.status === 403) {
|
||||
return;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export async function loader() {
|
||||
await noAuthMiddleware();
|
||||
return null;
|
||||
}
|
||||
@@ -4,10 +4,11 @@ import {
|
||||
redirect,
|
||||
useLoaderData,
|
||||
} from 'react-router-dom';
|
||||
import api 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 { useQuery } from '@tanstack/react-query';
|
||||
|
||||
const authMiddleware = async (currentUrl: string) => {
|
||||
try {
|
||||
@@ -37,12 +38,12 @@ const authMiddleware = async (currentUrl: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const membershipsPopulator = async () => {
|
||||
const membershipsPopulator = async (currentUrl: string) => {
|
||||
const res = await api.tenantMembershipsList();
|
||||
|
||||
const memberships = res.data;
|
||||
|
||||
if (memberships.rows?.length === 0) {
|
||||
if (memberships.rows?.length === 0 && !currentUrl.includes('/onboarding')) {
|
||||
throw redirect('/onboarding/create-tenant');
|
||||
}
|
||||
|
||||
@@ -51,7 +52,7 @@ const membershipsPopulator = async () => {
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const user = await authMiddleware(request.url);
|
||||
const memberships = await membershipsPopulator();
|
||||
const memberships = await membershipsPopulator(request.url);
|
||||
return {
|
||||
user,
|
||||
memberships,
|
||||
@@ -59,13 +60,17 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
}
|
||||
|
||||
export default function Auth() {
|
||||
const listMembershipsQuery = useQuery({
|
||||
...queries.user.listTenantMemberships,
|
||||
});
|
||||
|
||||
const { user, memberships } = useLoaderData() as Awaited<
|
||||
ReturnType<typeof loader>
|
||||
>;
|
||||
|
||||
const ctx = useContextFromParent({
|
||||
user,
|
||||
memberships,
|
||||
memberships: listMembershipsQuery.data?.rows || memberships,
|
||||
});
|
||||
|
||||
if (!user || !memberships) {
|
||||
|
||||
@@ -12,11 +12,11 @@ import {
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { currTenantAtom } from '@/lib/atoms';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useAtom } from 'jotai';
|
||||
import invariant from 'tiny-invariant';
|
||||
import { DataTable } from '@/components/molecules/data-table/data-table';
|
||||
import { TenantContextType } from '@/lib/outlet';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
|
||||
export const columns = ({
|
||||
onRowClick,
|
||||
@@ -100,7 +100,7 @@ export const columns = ({
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
function WorkflowRunSummary({ event }: { event: Event }) {
|
||||
const [tenant] = useAtom(currTenantAtom);
|
||||
const { tenant } = useOutletContext<TenantContextType>();
|
||||
invariant(tenant);
|
||||
|
||||
const [hoverCardOpen, setPopoverOpen] = useState<
|
||||
|
||||
@@ -18,8 +18,6 @@ import api, {
|
||||
queries,
|
||||
} from '@/lib/api';
|
||||
import invariant from 'tiny-invariant';
|
||||
import { useAtom } from 'jotai';
|
||||
import { currTenantAtom } from '@/lib/atoms';
|
||||
import { FilterOption } from '@/components/molecules/data-table/data-table-toolbar';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -30,7 +28,7 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { relativeDate } from '@/lib/utils';
|
||||
import { Code } from '@/components/ui/code';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useOutletContext, useSearchParams } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
@@ -38,6 +36,7 @@ import {
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { useApiError } from '@/lib/hooks';
|
||||
import { Loading } from '@/components/ui/loading.tsx';
|
||||
import { TenantContextType } from '@/lib/outlet';
|
||||
|
||||
export default function Events() {
|
||||
return (
|
||||
@@ -55,7 +54,7 @@ export default function Events() {
|
||||
|
||||
function EventsTable() {
|
||||
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
|
||||
const [tenant] = useAtom(currTenantAtom);
|
||||
const { tenant } = useOutletContext<TenantContextType>();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [rotate, setRotate] = useState(false);
|
||||
const { handleApiError } = useApiError({});
|
||||
@@ -65,16 +64,20 @@ function EventsTable() {
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedEvent &&
|
||||
(!searchParams.get('eventId') ||
|
||||
searchParams.get('eventId') !== selectedEvent.metadata.id)
|
||||
(!searchParams.get('event') ||
|
||||
searchParams.get('event') !== selectedEvent.metadata.id)
|
||||
) {
|
||||
setSearchParams({ eventId: selectedEvent.metadata.id });
|
||||
const newSearchParams = new URLSearchParams(searchParams);
|
||||
newSearchParams.set('event', selectedEvent.metadata.id);
|
||||
setSearchParams(newSearchParams);
|
||||
} else if (
|
||||
!selectedEvent &&
|
||||
searchParams.get('eventId') &&
|
||||
searchParams.get('eventId') !== ''
|
||||
searchParams.get('event') &&
|
||||
searchParams.get('event') !== ''
|
||||
) {
|
||||
setSearchParams({});
|
||||
const newSearchParams = new URLSearchParams(searchParams);
|
||||
newSearchParams.delete('event');
|
||||
setSearchParams(newSearchParams);
|
||||
}
|
||||
}, [selectedEvent, searchParams, setSearchParams]);
|
||||
|
||||
@@ -296,7 +299,7 @@ function EventDataSection({ event }: { event: Event }) {
|
||||
}
|
||||
|
||||
function EventWorkflowRunsList({ event }: { event: Event }) {
|
||||
const [tenant] = useAtom(currTenantAtom);
|
||||
const { tenant } = useOutletContext<TenantContextType>();
|
||||
invariant(tenant);
|
||||
|
||||
const listWorkflowRunsQuery = useQuery({
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
} from '@/components/ui/command';
|
||||
|
||||
import { Link, Outlet, useNavigate, useOutletContext } from 'react-router-dom';
|
||||
import api, { TenantMember, User } from '@/lib/api';
|
||||
import api, { Tenant, TenantMember, User } from '@/lib/api';
|
||||
import { useApiError } from '@/lib/hooks';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { CaretSortIcon, PlusCircledIcon } from '@radix-ui/react-icons';
|
||||
@@ -40,45 +40,38 @@ import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
} from '@radix-ui/react-popover';
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
MembershipsContextType,
|
||||
UserContextType,
|
||||
useContextFromParent,
|
||||
} from '@/lib/outlet';
|
||||
import { useAtom } from 'jotai';
|
||||
import { currTenantAtom } from '@/lib/atoms';
|
||||
import { useTenantContext } from '@/lib/atoms';
|
||||
import { Loading, Spinner } from '@/components/ui/loading.tsx';
|
||||
|
||||
function Main() {
|
||||
const { user, memberships } = useOutletContext<
|
||||
UserContextType & MembershipsContextType
|
||||
>();
|
||||
const [tenant, setTenant] = useAtom(currTenantAtom);
|
||||
const ctx = useOutletContext<UserContextType & MembershipsContextType>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!tenant && memberships && memberships.length > 0) {
|
||||
const tenant = memberships[0].tenant;
|
||||
invariant(tenant);
|
||||
setTenant(tenant);
|
||||
}
|
||||
}, [tenant, memberships, setTenant]);
|
||||
const { user, memberships } = ctx;
|
||||
|
||||
const ctx = useContextFromParent({
|
||||
const [currTenant] = useTenantContext();
|
||||
|
||||
const childCtx = useContextFromParent({
|
||||
user,
|
||||
memberships,
|
||||
tenant: currTenant,
|
||||
});
|
||||
|
||||
if (!user || !memberships) {
|
||||
if (!user || !memberships || !currTenant) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row flex-1 w-full h-full">
|
||||
<MainNav user={user} />
|
||||
<Sidebar memberships={memberships} />
|
||||
<Sidebar memberships={memberships} currTenant={currTenant} />
|
||||
<div className="pt-12 flex-grow">
|
||||
<Outlet context={ctx} />
|
||||
<Outlet context={childCtx} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -88,9 +81,10 @@ export default Main;
|
||||
|
||||
interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
memberships: TenantMember[];
|
||||
currTenant: Tenant;
|
||||
}
|
||||
|
||||
function Sidebar({ className, memberships }: SidebarProps) {
|
||||
function Sidebar({ className, memberships, currTenant }: SidebarProps) {
|
||||
return (
|
||||
<div className={cn('h-full border-r max-w-xs', className)}>
|
||||
<div className="flex flex-col justify-between items-start space-y-4 px-4 py-4 h-full">
|
||||
@@ -139,7 +133,7 @@ function Sidebar({ className, memberships }: SidebarProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<TenantSwitcher memberships={memberships} />
|
||||
<TenantSwitcher memberships={memberships} currTenant={currTenant} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -189,22 +183,6 @@ function MainNav({ user }: MainNavProps) {
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{/* <DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
Profile
|
||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
Billing
|
||||
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
Settings
|
||||
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>New Team</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator /> */}
|
||||
<DropdownMenuItem onClick={() => logoutMutation.mutate()}>
|
||||
Log out
|
||||
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
||||
@@ -220,10 +198,15 @@ function MainNav({ user }: MainNavProps) {
|
||||
interface TenantSwitcherProps {
|
||||
className?: string;
|
||||
memberships: TenantMember[];
|
||||
currTenant: Tenant;
|
||||
}
|
||||
|
||||
function TenantSwitcher({ className, memberships }: TenantSwitcherProps) {
|
||||
const [currTenant, setTenant] = useAtom(currTenantAtom);
|
||||
function TenantSwitcher({
|
||||
className,
|
||||
memberships,
|
||||
currTenant,
|
||||
}: TenantSwitcherProps) {
|
||||
const setCurrTenant = useTenantContext()[1];
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
if (!currTenant) {
|
||||
@@ -254,7 +237,7 @@ function TenantSwitcher({ className, memberships }: TenantSwitcherProps) {
|
||||
key={membership.metadata.id}
|
||||
onSelect={() => {
|
||||
invariant(membership.tenant);
|
||||
setTenant(membership.tenant);
|
||||
setCurrTenant(membership.tenant);
|
||||
setOpen(false);
|
||||
}}
|
||||
value={membership.tenant?.slug}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { queries } from '@/lib/api';
|
||||
import { currTenantAtom } from '@/lib/atoms';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useOutletContext, useParams } from 'react-router-dom';
|
||||
import invariant from 'tiny-invariant';
|
||||
import { relativeDate } from '@/lib/utils';
|
||||
import { ServerStackIcon } from '@heroicons/react/24/outline';
|
||||
@@ -11,9 +9,10 @@ import { Button } from '@/components/ui/button';
|
||||
import { DataTable } from '@/components/molecules/data-table/data-table';
|
||||
import { columns } from './components/step-runs-columns';
|
||||
import { Loading } from '@/components/ui/loading.tsx';
|
||||
import { TenantContextType } from '@/lib/outlet';
|
||||
|
||||
export default function ExpandedWorkflowRun() {
|
||||
const [tenant] = useAtom(currTenantAtom);
|
||||
const { tenant } = useOutletContext<TenantContextType>();
|
||||
invariant(tenant);
|
||||
|
||||
const params = useParams();
|
||||
|
||||
@@ -2,15 +2,14 @@ import { Separator } from '@/components/ui/separator';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { queries } from '@/lib/api';
|
||||
import invariant from 'tiny-invariant';
|
||||
import { useAtom } from 'jotai';
|
||||
import { currTenantAtom } from '@/lib/atoms';
|
||||
import { relativeDate } from '@/lib/utils';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useOutletContext } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Loading } from '@/components/ui/loading.tsx';
|
||||
import { TenantContextType } from '@/lib/outlet';
|
||||
|
||||
export default function Workers() {
|
||||
const [tenant] = useAtom(currTenantAtom);
|
||||
const { tenant } = useOutletContext<TenantContextType>();
|
||||
invariant(tenant);
|
||||
|
||||
const listWorkersQuery = useQuery({
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { JobRun, StepRun, StepRunStatus, queries, Event } from '@/lib/api';
|
||||
import CronPrettifier from 'cronstrue';
|
||||
import { currTenantAtom } from '@/lib/atoms';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useAtom } from 'jotai';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { Link, useOutletContext, useParams } from 'react-router-dom';
|
||||
import invariant from 'tiny-invariant';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { relativeDate } from '@/lib/utils';
|
||||
@@ -22,11 +20,12 @@ import { ColumnDef } from '@tanstack/react-table';
|
||||
import { useState } from 'react';
|
||||
import { Code } from '@/components/ui/code';
|
||||
import { Loading } from '@/components/ui/loading.tsx';
|
||||
import { TenantContextType } from '@/lib/outlet';
|
||||
|
||||
export default function ExpandedWorkflowRun() {
|
||||
const [expandedStepRuns, setExpandedStepRuns] = useState<string[]>([]);
|
||||
|
||||
const [tenant] = useAtom(currTenantAtom);
|
||||
const { tenant } = useOutletContext<TenantContextType>();
|
||||
invariant(tenant);
|
||||
|
||||
const params = useParams();
|
||||
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
} from '@tanstack/react-table';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import invariant from 'tiny-invariant';
|
||||
import { useAtom } from 'jotai';
|
||||
import { currTenantAtom } from '@/lib/atoms';
|
||||
import { queries } from '@/lib/api';
|
||||
import { Loading } from '@/components/ui/loading.tsx';
|
||||
import { TenantContextType } from '@/lib/outlet';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
|
||||
export default function WorkflowRuns() {
|
||||
return (
|
||||
@@ -29,7 +29,7 @@ export default function WorkflowRuns() {
|
||||
}
|
||||
|
||||
function WorkflowRunsTable() {
|
||||
const [tenant] = useAtom(currTenantAtom);
|
||||
const { tenant } = useOutletContext<TenantContextType>();
|
||||
invariant(tenant);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { DataTable } from '@/components/molecules/data-table/data-table';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import api, { Workflow, queries } from '@/lib/api';
|
||||
import { currTenantAtom } from '@/lib/atoms';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { isAxiosError } from 'axios';
|
||||
import { useAtom } from 'jotai';
|
||||
import {
|
||||
LoaderFunctionArgs,
|
||||
redirect,
|
||||
useLoaderData,
|
||||
useOutletContext,
|
||||
useParams,
|
||||
} from 'react-router-dom';
|
||||
import invariant from 'tiny-invariant';
|
||||
@@ -19,6 +18,7 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { relativeDate } from '@/lib/utils';
|
||||
import { Square3Stack3DIcon } from '@heroicons/react/24/outline';
|
||||
import { Loading } from '@/components/ui/loading.tsx';
|
||||
import { TenantContextType } from '@/lib/outlet';
|
||||
|
||||
export async function loader({
|
||||
params,
|
||||
@@ -97,7 +97,7 @@ export default function ExpandedWorkflow() {
|
||||
}
|
||||
|
||||
function WorkflowDefinition() {
|
||||
const [tenant] = useAtom(currTenantAtom);
|
||||
const { tenant } = useOutletContext<TenantContextType>();
|
||||
invariant(tenant);
|
||||
|
||||
const params = useParams();
|
||||
@@ -126,7 +126,7 @@ function WorkflowDefinition() {
|
||||
}
|
||||
|
||||
function RecentRunsList() {
|
||||
const [tenant] = useAtom(currTenantAtom);
|
||||
const { tenant } = useOutletContext<TenantContextType>();
|
||||
invariant(tenant);
|
||||
|
||||
const params = useParams();
|
||||
|
||||
@@ -3,12 +3,12 @@ import WorkflowList from './components/workflow-list';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { queries } from '@/lib/api';
|
||||
import invariant from 'tiny-invariant';
|
||||
import { useAtom } from 'jotai';
|
||||
import { currTenantAtom } from '@/lib/atoms';
|
||||
import { Loading } from '@/components/ui/loading.tsx';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { TenantContextType } from '@/lib/outlet';
|
||||
|
||||
export default function Workflows() {
|
||||
const [tenant] = useAtom(currTenantAtom);
|
||||
const { tenant } = useOutletContext<TenantContextType>();
|
||||
invariant(tenant);
|
||||
|
||||
const listWorkflowsQuery = useQuery({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import api, { CreateTenantRequest } from '@/lib/api';
|
||||
import api, { CreateTenantRequest, queries } from '@/lib/api';
|
||||
import { useApiError } from '@/lib/hooks';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { TenantCreateForm } from './components/tenant-create-form';
|
||||
@@ -12,12 +12,17 @@ export default function CreateTenant() {
|
||||
setFieldErrors: setFieldErrors,
|
||||
});
|
||||
|
||||
const listMembershipsQuery = useQuery({
|
||||
...queries.user.listTenantMemberships,
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationKey: ['user:update:login'],
|
||||
mutationFn: async (data: CreateTenantRequest) => {
|
||||
await api.tenantCreate(data);
|
||||
},
|
||||
onSuccess: () => {
|
||||
onSuccess: async () => {
|
||||
await listMembershipsQuery.refetch();
|
||||
navigate('/');
|
||||
},
|
||||
onError: handleApiError,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { FC } from 'react';
|
||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
||||
import {
|
||||
createBrowserRouter,
|
||||
redirect,
|
||||
RouterProvider,
|
||||
} from 'react-router-dom';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -12,22 +16,33 @@ const routes = [
|
||||
}),
|
||||
children: [
|
||||
{
|
||||
path: '/auth/login',
|
||||
path: '/auth',
|
||||
lazy: async () =>
|
||||
import('./pages/auth/login').then((res) => {
|
||||
import('./pages/auth/no-auth').then((res) => {
|
||||
return {
|
||||
Component: res.default,
|
||||
};
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/auth/register',
|
||||
lazy: async () =>
|
||||
import('./pages/auth/register').then((res) => {
|
||||
return {
|
||||
Component: res.default,
|
||||
loader: res.loader,
|
||||
};
|
||||
}),
|
||||
children: [
|
||||
{
|
||||
path: '/auth/login',
|
||||
lazy: async () =>
|
||||
import('./pages/auth/login').then((res) => {
|
||||
return {
|
||||
Component: res.default,
|
||||
};
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/auth/register',
|
||||
lazy: async () =>
|
||||
import('./pages/auth/register').then((res) => {
|
||||
return {
|
||||
Component: res.default,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
@@ -39,6 +54,16 @@ const routes = [
|
||||
};
|
||||
}),
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
lazy: async () => {
|
||||
return {
|
||||
loader: function () {
|
||||
return redirect('/events');
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/onboarding/create-tenant',
|
||||
lazy: async () =>
|
||||
|
||||
@@ -21,7 +21,7 @@ type SeedConfigFile struct {
|
||||
AdminPassword string `mapstructure:"adminPassword" json:"adminPassword,omitempty" default:"Admin123!!"`
|
||||
AdminName string `mapstructure:"adminName" json:"adminName,omitempty" default:"Admin"`
|
||||
|
||||
DefaultTenantName string `mapstructure:"defaultTenantName" json:"defaultTenantName,omitempty" default:"default"`
|
||||
DefaultTenantName string `mapstructure:"defaultTenantName" json:"defaultTenantName,omitempty" default:"Default"`
|
||||
DefaultTenantSlug string `mapstructure:"defaultTenantSlug" json:"defaultTenantSlug,omitempty" default:"default"`
|
||||
DefaultTenantID string `mapstructure:"defaultTenantId" json:"defaultTenantId,omitempty" default:"707d0855-80ab-4e1f-a156-f1c4546cbf52"`
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@ type ConfigFileAuth struct {
|
||||
// Hatchet instance
|
||||
BasicAuthEnabled bool `mapstructure:"basicAuthEnabled" json:"basicAuthEnabled,omitempty" default:"true"`
|
||||
|
||||
// SetEmailVerified controls whether the user's email is automatically set to verified
|
||||
SetEmailVerified bool `mapstructure:"setEmailVerified" json:"setEmailVerified,omitempty" default:"false"`
|
||||
|
||||
// Configuration options for the cookie
|
||||
Cookie ConfigFileAuthCookie `mapstructure:"cookie" json:"cookie,omitempty"`
|
||||
}
|
||||
@@ -111,6 +114,7 @@ func BindAllEnv(v *viper.Viper) {
|
||||
// auth options
|
||||
v.BindEnv("auth.restrictedEmailDomains", "SERVER_AUTH_RESTRICTED_EMAIL_DOMAINS")
|
||||
v.BindEnv("auth.basicAuthEnabled", "SERVER_AUTH_BASIC_AUTH_ENABLED")
|
||||
v.BindEnv("auth.setEmailVerified", "SERVER_AUTH_SET_EMAIL_VERIFIED")
|
||||
v.BindEnv("auth.cookie.name", "SERVER_AUTH_COOKIE_NAME")
|
||||
v.BindEnv("auth.cookie.domain", "SERVER_AUTH_COOKIE_DOMAIN")
|
||||
v.BindEnv("auth.cookie.secrets", "SERVER_AUTH_COOKIE_SECRETS")
|
||||
|
||||
@@ -2,11 +2,14 @@ package prisma
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hatchet-dev/hatchet/internal/repository"
|
||||
"github.com/hatchet-dev/hatchet/internal/repository/prisma/db"
|
||||
"github.com/hatchet-dev/hatchet/internal/repository/prisma/dbsqlc"
|
||||
"github.com/hatchet-dev/hatchet/internal/validator"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
@@ -87,19 +90,27 @@ func (r *eventRepository) ListEvents(tenantId string, opts *repository.ListEvent
|
||||
events, err := r.queries.ListEvents(context.Background(), tx, queryParams)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
events = make([]*dbsqlc.ListEventsRow, 0)
|
||||
} else {
|
||||
return nil, fmt.Errorf("could not list events: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
count, err := r.queries.CountEvents(context.Background(), tx, countParams)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
count = 0
|
||||
} else {
|
||||
return nil, fmt.Errorf("could not count events: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.Commit(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("could not commit transaction: %w", err)
|
||||
}
|
||||
|
||||
res.Rows = events
|
||||
|
||||
Reference in New Issue
Block a user