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: 11 additions & 1 deletion apps/mobile/src/features/connection/CloudEnvironmentRows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
resolveEnvironmentMachineKind,
} from "@t3tools/contracts";
import { useAtomValue } from "@effect/atom-react";
import { useCallback, useState } from "react";
import { type ReactNode, useCallback, useState } from "react";
import {
ActivityIndicator,
Pressable,
Expand All @@ -28,6 +28,7 @@ import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-typ
import { serverEnvironment } from "../../state/server";
import { availableCloudEnvironmentPresentation } from "../cloud/cloudEnvironmentPresentation";
import { hasCloudPublicConfig } from "../cloud/publicConfig";
import { ConnectionHostStorage } from "./ConnectionHostStorage";
import { ConnectionStatusDot } from "./ConnectionStatusDot";
import { type RelayEnvironmentView, useConnectionController } from "./useConnectionController";

Expand Down Expand Up @@ -225,6 +226,13 @@ function ConnectedCloudEnvironmentRow(props: {
errorExpanded={props.errorExpanded}
label={props.environment.environmentLabel}
machine={resolveEnvironmentMachineKind(serverConfig)}
storageIndicator={
<ConnectionHostStorage
environmentId={props.environment.environmentId}
environmentLabel={props.environment.environmentLabel}
connected={props.environment.connectionState === "connected"}
/>
}
onValueChange={(enabled) => {
if (enabled) {
props.onConnect();
Expand Down Expand Up @@ -286,6 +294,7 @@ function CloudEnvironmentRowShell(props: {
readonly onToggleError: () => void;
readonly onValueChange: (enabled: boolean) => void;
readonly statusText?: string;
readonly storageIndicator?: ReactNode;
readonly value: boolean;
}) {
const isRetrying =
Expand Down Expand Up @@ -406,6 +415,7 @@ function CloudEnvironmentRowShell(props: {
) : null}
</StatusContainer>
</View>
{props.storageIndicator}
<ThemedSwitch
disabled={props.disabled}
onValueChange={props.onValueChange}
Expand Down
139 changes: 74 additions & 65 deletions apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";
import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-types";
import { serverEnvironment } from "../../state/server";
import { ConnectionStatusDot } from "./ConnectionStatusDot";
import { ConnectionHostStorage } from "./ConnectionHostStorage";

function connectionStatusLabel(environment: ConnectedEnvironmentSummary): string | null {
return connectionStatusText({
Expand Down Expand Up @@ -65,76 +66,84 @@ export function ConnectionEnvironmentRow(props: {

return (
<Animated.View layout={LinearTransition.duration(250)} className="bg-card">
<Pressable
className="flex-row items-center gap-3 px-4 py-3.5 active:opacity-70"
onPress={props.onToggle}
>
<ConnectionStatusDot
state={props.environment.connectionState}
pulse={isRetrying}
size={8}
/>
<View className="flex-row items-center pr-2">
<Pressable
className="min-w-0 flex-1 flex-row items-center gap-3 px-4 py-3.5 active:opacity-70"
onPress={props.onToggle}
>
<ConnectionStatusDot
state={props.environment.connectionState}
pulse={isRetrying}
size={8}
/>

<View className="flex-1 gap-0.5">
<View className="flex-row items-center gap-1.5">
<EnvironmentMachineSymbol
kind={resolveEnvironmentMachineKind(serverConfig)}
size={14}
tintColorClassName="accent-foreground-muted"
/>
<Text
className="min-w-0 flex-shrink text-base font-t3-bold leading-snug text-foreground"
numberOfLines={1}
>
{props.environment.environmentLabel}
<View className="flex-1 gap-0.5">
<View className="flex-row items-center gap-1.5">
<EnvironmentMachineSymbol
kind={resolveEnvironmentMachineKind(serverConfig)}
size={14}
tintColorClassName="accent-foreground-muted"
/>
<Text
className="min-w-0 flex-shrink text-base font-t3-bold leading-snug text-foreground"
numberOfLines={1}
>
{props.environment.environmentLabel}
</Text>
</View>
<Text className="text-xs text-foreground-muted" numberOfLines={1}>
{props.environment.displayUrl}
</Text>
{statusLabel ? (
<Text
className={cn(
"text-xs",
hasConnectionFailure ? "text-danger-foreground" : "text-foreground-muted",
)}
numberOfLines={props.expanded ? undefined : 1}
selectable={props.expanded}
>
{statusLabel}
{statusTraceId ? (
<>
{" Trace ID: "}
<Text
accessibilityHint="Copies the trace ID"
accessibilityRole="button"
className="underline decoration-dotted"
onLongPress={(event) => {
event.stopPropagation();
copyTextWithHaptic(statusTraceId, { target: "connection-trace-id" });
}}
onPress={(event) => {
event.stopPropagation();
}}
>
{statusTraceId}
</Text>
</>
) : null}
</Text>
) : null}
</View>
<Text className="text-xs text-foreground-muted" numberOfLines={1}>
{props.environment.displayUrl}
</Text>
{statusLabel ? (
<Text
className={cn(
"text-xs",
hasConnectionFailure ? "text-danger-foreground" : "text-foreground-muted",
)}
numberOfLines={props.expanded ? undefined : 1}
selectable={props.expanded}
>
{statusLabel}
{statusTraceId ? (
<>
{" Trace ID: "}
<Text
accessibilityHint="Copies the trace ID"
accessibilityRole="button"
className="underline decoration-dotted"
onLongPress={(event) => {
event.stopPropagation();
copyTextWithHaptic(statusTraceId, { target: "connection-trace-id" });
}}
onPress={(event) => {
event.stopPropagation();
}}
>
{statusTraceId}
</Text>
</>
) : null}
</Text>
) : null}
</View>

<SymbolView
name="chevron.down"
size={12}
tintColorClassName={"accent-icon-subtle"}
type="monochrome"
style={{
transform: [{ rotate: props.expanded ? "180deg" : "0deg" }],
}}
<SymbolView
name="chevron.down"
size={12}
tintColorClassName={"accent-icon-subtle"}
type="monochrome"
style={{
transform: [{ rotate: props.expanded ? "180deg" : "0deg" }],
}}
/>
</Pressable>

<ConnectionHostStorage
environmentId={props.environment.environmentId}
environmentLabel={props.environment.environmentLabel}
connected={props.environment.connectionState === "connected"}
/>
Comment thread
adamblumoff marked this conversation as resolved.
</Pressable>
</View>

{props.expanded ? (
<Animated.View
Expand Down
168 changes: 168 additions & 0 deletions apps/mobile/src/features/connection/ConnectionHostStorage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { useAtomRefresh, useAtomValue } from "@effect/atom-react";
import { useIsFocused } from "@react-navigation/native";
import { getHostStoragePresentation } from "@t3tools/client-runtime/host-storage";
import type { EnvironmentId } from "@t3tools/contracts";
import { useState } from "react";
import { Modal, Pressable, View } from "react-native";
import Svg, { Circle } from "react-native-svg";
import { withUniwind } from "uniwind";

import { SymbolView } from "../../components/AppSymbol";
import { AppText as Text } from "../../components/AppText";
import { serverEnvironment } from "../../state/server";

const ThemedSvg = withUniwind(Svg);

function HostStorageView(props: {
readonly environmentLabel: string;
readonly presentation: ReturnType<typeof getHostStoragePresentation>;
readonly onRefresh?: () => void;
}) {
const { presentation } = props;
const loading = presentation.status === "loading";
const [open, setOpen] = useState(false);
const usedPercent =
presentation.status === "available" ? 100 - presentation.availablePercent : null;

return (
<>
<Pressable
accessibilityRole="button"
accessibilityLabel={`${props.environmentLabel} disk space${presentation.status === "available" ? `: ${presentation.label}` : ""}`}
accessibilityHint="Show disk capacity"
onPress={() => setOpen(true)}
className="h-11 w-11 items-center justify-center rounded-full active:bg-subtle"
>
<ThemedSvg
width={20}
height={20}
viewBox="0 0 24 24"
colorClassName="accent-foreground-muted"
>
<Circle
cx={12}
cy={12}
r={9}
fill="none"
stroke="currentColor"
strokeOpacity={0.2}
strokeWidth={3}
/>
{usedPercent !== null ? (
<Circle
cx={12}
cy={12}
r={9}
fill="none"
stroke="currentColor"
strokeWidth={3}
strokeDasharray={[(2 * Math.PI * 9 * usedPercent) / 100, 2 * Math.PI * 9]}
rotation={-90}
origin="12, 12"
/>
) : null}
</ThemedSvg>
</Pressable>
<Modal visible={open} transparent animationType="fade" onRequestClose={() => setOpen(false)}>
<Pressable
accessible={false}
focusable={false}
className="flex-1 items-center justify-center bg-backdrop px-8"
onPress={() => setOpen(false)}
>
<Pressable
accessible={false}
focusable={false}
accessibilityViewIsModal
className="w-full max-w-sm gap-3 rounded-2xl bg-card p-4"
onPress={(event) => event.stopPropagation()}
>
<View className="flex-row items-center gap-2">
<View className="min-w-0 flex-1 gap-0.5">
<Text className="text-xs text-foreground-muted">Disk containing T3 data</Text>
<Text className="text-xs text-foreground" selectable>
{presentation.status === "available"
? presentation.label
: loading
? "Checking storage…"
: "Storage unavailable"}
</Text>
</View>
{props.onRefresh ? (
<Pressable
accessibilityRole="button"
accessibilityLabel={`Refresh disk storage for ${props.environmentLabel}`}
accessibilityState={{ disabled: loading, busy: loading }}
disabled={loading}
onPress={props.onRefresh}
className="h-11 w-11 items-center justify-center rounded-xl active:bg-subtle disabled:opacity-40"
>
<SymbolView
name="arrow.clockwise"
size={14}
tintColorClassName="accent-icon-subtle"
type="monochrome"
/>
</Pressable>
) : null}
</View>
{presentation.status === "available" ? (
<Text className="text-2xs text-foreground-muted">
{Math.round(100 - presentation.availablePercent)}% used or reserved{"\n"}
Checked at {new Date(presentation.sampledAt).toLocaleTimeString()}
</Text>
) : null}
<Text className="text-xs text-foreground-muted">
Other drives may have different space available.
</Text>
<Pressable
accessibilityRole="button"
onPress={() => setOpen(false)}
className="min-h-11 items-center justify-center rounded-xl bg-subtle"
>
<Text className="text-sm text-foreground">Done</Text>
</Pressable>
</Pressable>
</Pressable>
</Modal>
</>
);
}

function ConnectedHostStorage(props: {
readonly environmentId: EnvironmentId;
readonly environmentLabel: string;
}) {
const query = serverEnvironment.hostStorage({ environmentId: props.environmentId, input: {} });
const result = useAtomValue(query);
const refresh = useAtomRefresh(query);

return (
<HostStorageView
environmentLabel={props.environmentLabel}
presentation={getHostStoragePresentation(result)}
onRefresh={refresh}
/>
);
}

export function ConnectionHostStorage(props: {
readonly environmentId: EnvironmentId;
readonly environmentLabel: string;
readonly connected: boolean;
}) {
const isFocused = useIsFocused();
if (!isFocused) return null;

return props.connected ? (
<ConnectedHostStorage
environmentId={props.environmentId}
environmentLabel={props.environmentLabel}
/>
) : (
<HostStorageView
environmentLabel={props.environmentLabel}
presentation={{ status: "unavailable" }}
/>
);
}
1 change: 1 addition & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const RPC_REQUIRED_SCOPES = {
[WS_METHODS.serverGetTraceDiagnostics]: AuthOrchestrationReadScope,
[WS_METHODS.serverGetProcessDiagnostics]: AuthOrchestrationReadScope,
[WS_METHODS.serverGetHostResources]: AuthOrchestrationReadScope,
[WS_METHODS.serverGetHostStorage]: AuthOrchestrationReadScope,
[WS_METHODS.serverGetProcessResourceHistory]: AuthOrchestrationReadScope,
[WS_METHODS.serverGetResourceTelemetryHistory]: AuthOrchestrationReadScope,
[WS_METHODS.serverRetryResourceTelemetry]: AuthOrchestrationOperateScope,
Expand Down
Loading
Loading