Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/config/getInstanceClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,16 @@ interface InstanceClient {
}

export function getInstanceClient(
{ id = OverallAppSignIn, operationsUrl, port, secure, forceFabricConnect }: InstanceClient & {
{
id = OverallAppSignIn,
operationsUrl,
port,
secure,
forceFabricConnect,
disableFabricConnect,
}: InstanceClient & {
forceFabricConnect?: boolean;
disableFabricConnect?: boolean;
} = {},
) {
let baseURL = operationsUrl || authStore.getOperationsUrl(id);
Expand All @@ -30,7 +38,7 @@ export function getInstanceClient(
}
}

const fabricConnect = forceFabricConnect || authStore.checkForFabricConnect(id);
const fabricConnect = !disableFabricConnect && (forceFabricConnect || authStore.checkForFabricConnect(id));
if (fabricConnect) {
if (id.startsWith('clu-')) {
baseURL = apiClient.defaults.baseURL + `/Cluster/${id}/operation`;
Expand Down
52 changes: 33 additions & 19 deletions src/config/useInstanceClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,49 @@ import { OverallAppSignIn } from '@/features/auth/store/authStore';
import { useParams } from '@tanstack/react-router';
import { useMemo } from 'react';

export function useInstanceClient(operationsUrl?: string | null, port?: number, secure?: boolean) {
interface UseParams {
operationsUrl?: string | null;
port?: number;
secure?: boolean;
disableFabricConnect?: boolean;
instanceId?: string;
clusterId?: string;
}

export function useInstanceClient(
params: UseParams = {},
) {
const { instanceId, clusterId }: { instanceId?: string; clusterId?: string } = useParams({ strict: false });
const id = isLocalStudio ? OverallAppSignIn : instanceId ?? clusterId;
return getInstanceClient({ id, operationsUrl, port, secure });
const id = isLocalStudio ? OverallAppSignIn : params.instanceId ?? instanceId ?? params.clusterId ?? clusterId;
return getInstanceClient({ id, ...params });
}

export function useInstanceClientParams(
operationsUrl?: string | null,
port?: number,
secure?: boolean,
params: UseParams = {},
): InstanceClientConfig & InstanceTypeConfig {
const { instanceId, clusterId }: { instanceId?: string; clusterId?: string } = useParams({ strict: false });
const id = isLocalStudio ? OverallAppSignIn : instanceId ?? clusterId;
const id = isLocalStudio ? OverallAppSignIn : params.instanceId ?? instanceId ?? params.clusterId ?? clusterId;
return {
instanceClient: getInstanceClient({ id, operationsUrl, port, secure }),
instanceClient: getInstanceClient({ id, ...params }),
entityType: (isLocalStudio || instanceId) ? 'instance' : 'cluster',
};
}

export function useInstanceClientIdParams(
operationsUrl?: string | null,
port?: number,
secure?: boolean,
params: UseParams = {},
): InstanceClientIdConfig & InstanceTypeConfig {
const params: { instanceId?: string; clusterId?: string } = useParams({ strict: false });
return useMemo(() => getInstanceClientIdFromParams({ ...params, operationsUrl, port, secure }), [
params,
operationsUrl,
port,
secure,
]);
const { instanceId, clusterId }: { instanceId?: string; clusterId?: string } = useParams({ strict: false });
return useMemo(
() => getInstanceClientIdFromParams({ instanceId, clusterId, ...params }),
[
params.instanceId ?? instanceId,
params.clusterId ?? clusterId,
params.operationsUrl,
params.port,
params.secure,
params.disableFabricConnect,
],
);
}

export function getInstanceClientIdFromParams({
Expand All @@ -44,19 +56,21 @@ export function getInstanceClientIdFromParams({
operationsUrl,
port,
secure,
disableFabricConnect,
}: {
instanceId?: string;
clusterId?: string;
operationsUrl?: string | null;
port?: number;
secure?: boolean;
disableFabricConnect?: boolean;
}): InstanceClientIdConfig & InstanceTypeConfig {
const id = isLocalStudio ? OverallAppSignIn : instanceId ?? clusterId;
if (!id) {
throw new Error('id could not be automatically calculated in useInstanceClientIdParams');
}
return {
instanceClient: getInstanceClient({ id, operationsUrl, port, secure }),
instanceClient: getInstanceClient({ id, operationsUrl, port, secure, disableFabricConnect }),
entityId: id,
entityType: (isLocalStudio || instanceId) ? 'instance' : 'cluster',
};
Expand Down
2 changes: 1 addition & 1 deletion src/features/auth/ClusterInstanceSignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function ClusterInstanceSignIn() {
return null;
}, [cluster, instance]);

const instanceParams = useInstanceClientIdParams(operationsUrl);
const instanceParams = useInstanceClientIdParams({ operationsUrl });
const warnAboutLocalDeviceAccess = useMemo(
() => operationsUrl?.includes('localhost') || operationsUrl?.includes('127.0.0.1'),
[operationsUrl],
Expand Down
2 changes: 1 addition & 1 deletion src/features/cluster/FinishSetup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function FinishSetup() {

const navigate = useNavigate();
const operationsUrl = useMemo(() => getOperationsUrlForCluster(cluster), [cluster]);
const instanceClient = useInstanceClient(operationsUrl);
const instanceClient = useInstanceClient({ operationsUrl });

const { redirect } = useSearch({ strict: false });
const router = useRouter();
Expand Down
2 changes: 1 addition & 1 deletion src/features/cluster/InstanceLogInCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function InstanceLogInCell(
) {
const { user: instanceUser, isLoading: instanceAuthIsLoading } = useInstanceAuth(instance.id);
const operationsUrl = useMemo(() => getOperationsUrlForInstance(instance), [instance]);
const instanceClient = useInstanceClient(operationsUrl);
const instanceClient = useInstanceClient({ operationsUrl });
const { update } = useOrganizationClusterInstancePermissions();
const isFabricConnect = authStore.checkForFabricConnect(instance.id);

Expand Down
107 changes: 107 additions & 0 deletions src/features/cluster/InstanceStatusCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useInstanceClientIdParams } from '@/config/useInstanceClient';
import { useOrganizationClusterInstancePermissions } from '@/hooks/usePermissions';
import { Instance } from '@/integrations/api/api.patch';
import { getStatusQueryOptions } from '@/integrations/api/instance/status/getStatus';
import { useSetStatus } from '@/integrations/api/instance/status/setStatus';
import { getOperationsUrlForInstance } from '@/lib/urls/getOperationsUrlForInstance';
import { useQuery } from '@tanstack/react-query';
import { LoaderCircleIcon, ShieldCheckIcon, ShieldXIcon } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';

const DEFAULT_200_MSG = 'Status endpoint is reporting for duty.';
const DEFAULT_404_MSG = 'Status endpoint is reporting downtime.';

export function InstanceStatusCell(
{ instance }: { readonly instance: Instance },
) {
const operationsUrl = useMemo(() => getOperationsUrlForInstance(instance), [instance]);
const instanceParams = useInstanceClientIdParams({ operationsUrl, instanceId: instance.id });
const { update: canManage } = useOrganizationClusterInstancePermissions();
const { mutate: setStatus, isPending: isSettingStatus } = useSetStatus();

// We want to spread the initial requests across 5 seconds.
const [randomOffset] = useState(() => Math.floor(Math.random() * 5_000));
const [ready, setReady] = useState(false);

useEffect(() => {
const timer = setTimeout(() => setReady(true), randomOffset);
return () => clearTimeout(timer);
}, [randomOffset]);

const { data: statusResponse, isLoading, isFetching } = useQuery(getStatusQueryOptions(instanceParams, ready));

const isAvailable = statusResponse?.systemStatus?.[0]?.status === 'Available';
const isUnavailable = statusResponse?.systemStatus?.[0]?.status === 'Unavailable';
const statusMessage = typeof statusResponse?.data === 'string'
? statusResponse.data
: (isAvailable ? DEFAULT_200_MSG : (isUnavailable ? DEFAULT_404_MSG : 'Unknown status'));
Comment on lines +34 to +40
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how we check if the instance is available or not, based on the first systemStatus.


return (
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center">
{isLoading || !ready || (isFetching && !statusResponse)
? <LoaderCircleIcon className="animate-spin size-5 text-muted-foreground" />
: (
<Badge
variant={isAvailable ? 'success' : isUnavailable ? 'destructive' : 'default'}
className="size-4 rounded-full p-0"
>
<span className="sr-only">{isAvailable ? 'Online' : 'Offline'}</span>
</Badge>
)}
</div>
</TooltipTrigger>
<TooltipContent>
{isFetching && statusResponse ? 'Refreshing... ' : ''}
{statusMessage}
</TooltipContent>
</Tooltip>

{canManage && (
<div className="flex gap-1">
{isAvailable && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="destructiveGhost"
size="icon"
className="size-7"
onClick={() => setStatus({ ...instanceParams, id: 'availability', status: 'Unavailable' })}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how we set it to be unavailable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await server.operation({ operation: 'set_status', id: 'availability', status: 'Unavailable' });

Yeah looks right to me.

disabled={isSettingStatus}
>
{isSettingStatus
? <LoaderCircleIcon className="animate-spin size-4" />
: <ShieldXIcon className="size-4" />}
</Button>
</TooltipTrigger>
<TooltipContent>Bring out of rotation</TooltipContent>
</Tooltip>
)}
{isUnavailable && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="size-7"
onClick={() => setStatus({ ...instanceParams, id: 'availability', status: 'Available' })}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this is how we set it to be available.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that looks right.

await server.operation({ operation: 'set_status', id: 'availability', status: 'Available' });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thanks! Experimentally it works too. :)

disabled={isSettingStatus}
>
{isSettingStatus
? <LoaderCircleIcon className="animate-spin size-4" />
: <ShieldCheckIcon className="size-4" />}
</Button>
</TooltipTrigger>
<TooltipContent>Bring back into rotation</TooltipContent>
</Tooltip>
)}
</div>
)}
</div>
);
}
14 changes: 10 additions & 4 deletions src/features/cluster/Instances.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ColumnDef } from '@tanstack/react-table';
import { useMemo } from 'react';
import { EmptyCluster } from './EmptyCluster';
import { InstanceLogInCell } from './InstanceLogInCell';
import { InstanceStatusCell } from './InstanceStatusCell';
import { getClusterInfoQueryOptions } from './queries/getClusterInfoQuery';

export function Instances() {
Expand All @@ -34,7 +35,7 @@ export function Instances() {
size: 1,
minSize: 1,
cell: (cell) => (
<div className="flex justify-end">
<div className="flex justify-end gap-2 items-center">
<InstanceLogInCell isSelfManaged={isSelfManaged} instance={cell.row.original} />
</div>
),
Expand All @@ -56,14 +57,19 @@ export function Instances() {
size: 90,
header: 'Name',
},
!isSelfManaged && {
{
accessorKey: 'status',
header: 'Status',
size: 1,
minSize: 1,
cell: (cell) => {
const status = cell.getValue() as string;
return <Badge variant={renderBadgeStatusVariant(status)}>{capitalizeWords(status)}</Badge>;
return (
<div className="flex items-center gap-2">
<InstanceStatusCell instance={cell.row.original} />
{status ? <Badge variant={renderBadgeStatusVariant(status)}>{capitalizeWords(status)}</Badge> : null}
</div>
);
},
},
!isSelfManaged && {
Expand Down Expand Up @@ -128,7 +134,7 @@ export function Instances() {
return (
<>
<SubNavMenu />
<div className="mt-32 px-4 pt-4 md:px-12 min-h-[calc(100vh-theme(spacing.32))]">
<div className="mt-32 px-4 pt-4 md:px-12 min-h-[calc(100vh-(--spacing(32)))]">
<Card className="p-0 mt-4 min-h-96">
<CardContent className="p-0 min-h-96">
{clusterIsLoading
Expand Down
2 changes: 1 addition & 1 deletion src/features/clusters/components/ClusterCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function ClusterCard({ cluster }: { cluster: Cluster }) {
const router = useRouter();
const queryClient = useQueryClient();
const operationsUrl = useMemo(() => getOperationsUrlForCluster(cluster), [cluster]);
const instanceClient = useInstanceClient(operationsUrl);
const instanceClient = useInstanceClient({ operationsUrl });
const auth = useInstanceAuth(cluster.id);
const [, setSavedClusterState] = useLocalStorage<unknown | null>(LocalStorageKeys.SavedClusterState, null);

Expand Down
2 changes: 1 addition & 1 deletion src/integrations/api/instance/status/getStatus.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { InstanceClientIdConfig } from '@/config/instanceClientConfig';
import { queryOptions } from '@tanstack/react-query';

interface SystemStatus {
export interface SystemStatus {
id: 'availability' | 'maintenance' | 'primary' | string;
status: 'Available' | 'Unavailable' | string;
__updatedtime__: number;
Expand Down
28 changes: 28 additions & 0 deletions src/integrations/api/instance/status/setStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { InstanceClientIdConfig } from '@/config/instanceClientConfig';
import { SystemStatus } from '@/integrations/api/instance/status/getStatus';
import { ReplicatedResponse } from '@/integrations/api/replication';
import { queryClient } from '@/react-query/queryClient';
import { useMutation } from '@tanstack/react-query';

interface SetConfigurationParams extends Pick<SystemStatus, 'id' | 'status'>, InstanceClientIdConfig {
}

async function setStatus({
instanceClient,
id,
status,
}: SetConfigurationParams): Promise<ReplicatedResponse> {
const { data } = await instanceClient.post('/', {
operation: 'set_status',
id,
status,
});
return data;
}

export function useSetStatus() {
return useMutation({
mutationFn: setStatus,
onSuccess: (_data, variables) => queryClient.invalidateQueries({ queryKey: [variables.entityId, 'get_status'] }),
});
}
12 changes: 12 additions & 0 deletions src/lib/urls/getRestUrlForInstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Instance } from '@/integrations/api/api.patch';

export function getRestUrlForInstance(
instance: Pick<Instance, 'instanceFqdn'>,
): string {
let fqdn = instance.instanceFqdn;
if (!fqdn.match(/^https?:\/\//i)) {
fqdn = `https://${fqdn}`;
}
const url = new URL(fqdn);
return url.toString();
}