Extract PrBadge component and refactor workspace hover card
This commit is contained in:
@@ -27,6 +27,7 @@ import { type GestureType } from "react-native-gesture-handler";
|
|||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
import {
|
import {
|
||||||
Archive,
|
Archive,
|
||||||
|
ArrowUpRight,
|
||||||
CircleAlert,
|
CircleAlert,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
@@ -112,11 +113,6 @@ const DEFAULT_STATUS_DOT_SIZE = 7;
|
|||||||
const EMPHASIZED_STATUS_DOT_SIZE = 9;
|
const EMPHASIZED_STATUS_DOT_SIZE = 9;
|
||||||
const DEFAULT_STATUS_DOT_OFFSET = 0;
|
const DEFAULT_STATUS_DOT_OFFSET = 0;
|
||||||
const EMPHASIZED_STATUS_DOT_OFFSET = -1;
|
const EMPHASIZED_STATUS_DOT_OFFSET = -1;
|
||||||
const GITHUB_PR_STATE_LABELS: Record<PrHint["state"], string> = {
|
|
||||||
open: "Open",
|
|
||||||
merged: "Merged",
|
|
||||||
closed: "Closed",
|
|
||||||
};
|
|
||||||
|
|
||||||
interface SidebarWorkspaceListProps {
|
interface SidebarWorkspaceListProps {
|
||||||
projects: SidebarProjectEntry[];
|
projects: SidebarProjectEntry[];
|
||||||
@@ -178,7 +174,7 @@ interface WorkspaceRowInnerProps {
|
|||||||
archiveShortcutKeys?: ShortcutKey[][] | null;
|
archiveShortcutKeys?: ShortcutKey[][] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorkspacePrBadge({ hint }: { hint: PrHint }) {
|
export function PrBadge({ hint }: { hint: PrHint }) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const activeColor = isHovered ? theme.colors.foreground : theme.colors.foregroundMuted;
|
const activeColor = isHovered ? theme.colors.foreground : theme.colors.foregroundMuted;
|
||||||
@@ -198,32 +194,45 @@ function WorkspacePrBadge({ hint }: { hint: PrHint }) {
|
|||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityRole="link"
|
accessibilityRole="link"
|
||||||
accessibilityLabel={`${hint.state} pull request #${hint.number}`}
|
accessibilityLabel={`Pull request #${hint.number}`}
|
||||||
hitSlop={4}
|
hitSlop={4}
|
||||||
onPressIn={handlePressIn}
|
onPressIn={handlePressIn}
|
||||||
onPress={handlePress}
|
onPress={handlePress}
|
||||||
onPointerEnter={() => setIsHovered(true)}
|
onPointerEnter={() => setIsHovered(true)}
|
||||||
onPointerLeave={() => setIsHovered(false)}
|
onPointerLeave={() => setIsHovered(false)}
|
||||||
style={({ pressed }) => [
|
style={({ pressed }) => [
|
||||||
styles.workspacePrBadge,
|
prBadgeStyles.badge,
|
||||||
pressed && styles.workspacePrBadgePressed,
|
pressed && prBadgeStyles.badgePressed,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<GitPullRequest size={12} color={activeColor} />
|
<GitPullRequest size={12} color={activeColor} />
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[prBadgeStyles.text, { color: activeColor }]}
|
||||||
styles.workspacePrBadgeText,
|
|
||||||
{ color: activeColor },
|
|
||||||
]}
|
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
>
|
>
|
||||||
#{hint.number} · {GITHUB_PR_STATE_LABELS[hint.state]}
|
#{hint.number}
|
||||||
</Text>
|
</Text>
|
||||||
{isHovered && <ExternalLink size={10} color={activeColor} />}
|
<ArrowUpRight size={10} color={activeColor} style={{ opacity: isHovered ? 1 : 0 }} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prBadgeStyles = StyleSheet.create((theme) => ({
|
||||||
|
badge: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 2,
|
||||||
|
},
|
||||||
|
badgePressed: {
|
||||||
|
opacity: 0.82,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
fontWeight: theme.fontWeight.normal,
|
||||||
|
lineHeight: 14,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
function WorkspaceStatusIndicator({
|
function WorkspaceStatusIndicator({
|
||||||
bucket,
|
bucket,
|
||||||
@@ -1042,8 +1051,8 @@ function WorkspaceRowInner({
|
|||||||
</View>
|
</View>
|
||||||
{prHint ? (
|
{prHint ? (
|
||||||
<View style={styles.workspacePrBadgeRow}>
|
<View style={styles.workspacePrBadgeRow}>
|
||||||
<WorkspacePrBadge hint={prHint} />
|
<CheckStatusIndicator status={prHint.checksStatus ?? "none"} size={12} />
|
||||||
<CheckStatusIndicator status={prHint.checksStatus ?? "none"} size={11} />
|
<PrBadge hint={prHint} />
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@@ -2414,23 +2423,9 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
workspacePrBadgeRow: {
|
workspacePrBadgeRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: theme.spacing[1],
|
gap: theme.spacing[2],
|
||||||
paddingLeft: WORKSPACE_STATUS_DOT_WIDTH + theme.spacing[2],
|
paddingLeft: WORKSPACE_STATUS_DOT_WIDTH + theme.spacing[2],
|
||||||
},
|
},
|
||||||
workspacePrBadge: {
|
|
||||||
alignSelf: "flex-start",
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing[1],
|
|
||||||
},
|
|
||||||
workspacePrBadgePressed: {
|
|
||||||
opacity: 0.82,
|
|
||||||
},
|
|
||||||
workspacePrBadgeText: {
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
fontWeight: theme.fontWeight.normal,
|
|
||||||
lineHeight: 14,
|
|
||||||
},
|
|
||||||
workspaceCreatingText: {
|
workspaceCreatingText: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useMutation } from "@tanstack/react-query";
|
|||||||
import { Dimensions, Platform, Text, View } from "react-native";
|
import { Dimensions, Platform, Text, View } from "react-native";
|
||||||
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
|
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
|
||||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { Check, ExternalLink, GitPullRequest, LoaderCircle, Minus, Play, X } from "lucide-react-native";
|
import { Check, ExternalLink, LoaderCircle, Minus, Play, X } from "lucide-react-native";
|
||||||
import { Pressable } from "react-native";
|
import { Pressable } from "react-native";
|
||||||
import { Portal } from "@gorhom/portal";
|
import { Portal } from "@gorhom/portal";
|
||||||
import { useBottomSheetModalInternal } from "@gorhom/bottom-sheet";
|
import { useBottomSheetModalInternal } from "@gorhom/bottom-sheet";
|
||||||
@@ -19,6 +19,7 @@ import type { PrHint } from "@/hooks/use-checkout-pr-status-query";
|
|||||||
import { useToast } from "@/contexts/toast-context";
|
import { useToast } from "@/contexts/toast-context";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import { openExternalUrl } from "@/utils/open-external-url";
|
import { openExternalUrl } from "@/utils/open-external-url";
|
||||||
|
import { PrBadge } from "@/components/sidebar-workspace-list";
|
||||||
|
|
||||||
interface Rect {
|
interface Rect {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -191,12 +192,6 @@ function WorkspaceHoverCardDesktop({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const GITHUB_PR_STATE_LABELS: Record<PrHint["state"], string> = {
|
|
||||||
open: "Open",
|
|
||||||
merged: "Merged",
|
|
||||||
closed: "Closed",
|
|
||||||
};
|
|
||||||
|
|
||||||
function getServiceHealthColor(input: {
|
function getServiceHealthColor(input: {
|
||||||
health: SidebarWorkspaceEntry["services"][number]["health"];
|
health: SidebarWorkspaceEntry["services"][number]["health"];
|
||||||
theme: ReturnType<typeof useUnistyles>["theme"];
|
theme: ReturnType<typeof useUnistyles>["theme"];
|
||||||
@@ -223,84 +218,93 @@ function getServiceHealthLabel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getCheckStatusColor(input: {
|
||||||
|
status: string;
|
||||||
|
theme: ReturnType<typeof useUnistyles>["theme"];
|
||||||
|
}): string {
|
||||||
|
if (input.status === "success") return input.theme.colors.palette.green[500];
|
||||||
|
if (input.status === "failure") return input.theme.colors.palette.red[500];
|
||||||
|
if (input.status === "pending") return input.theme.colors.palette.amber[500];
|
||||||
|
return input.theme.colors.foregroundMuted;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCheckStatusIcon(status: string): typeof Check {
|
||||||
|
if (status === "success") return Check;
|
||||||
|
if (status === "failure") return X;
|
||||||
|
return Minus;
|
||||||
|
}
|
||||||
|
|
||||||
export function CheckStatusIndicator({
|
export function CheckStatusIndicator({
|
||||||
status,
|
status,
|
||||||
size = 14,
|
size = 12,
|
||||||
}: {
|
}: {
|
||||||
status: string;
|
status: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
}): ReactElement | null {
|
}): ReactElement | null {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const iconSize = Math.round(size * 0.6);
|
|
||||||
|
|
||||||
if (!status || status === "none") return null;
|
if (!status || status === "none") return null;
|
||||||
|
|
||||||
if (status === "pending") {
|
const color = getCheckStatusColor({ status, theme });
|
||||||
return (
|
const IconComponent = getCheckStatusIcon(status);
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: size,
|
|
||||||
height: size,
|
|
||||||
borderRadius: size / 2,
|
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: theme.colors.palette.amber[500],
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === "success") {
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: size,
|
|
||||||
height: size,
|
|
||||||
borderRadius: size / 2,
|
|
||||||
backgroundColor: "rgba(34,197,94,0.15)",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Check size={iconSize} color={theme.colors.palette.green[500]} strokeWidth={3} />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === "failure") {
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
width: size,
|
|
||||||
height: size,
|
|
||||||
borderRadius: size / 2,
|
|
||||||
backgroundColor: "rgba(239,68,68,0.15)",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<X size={iconSize} color={theme.colors.palette.red[500]} strokeWidth={3} />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// skipped / cancelled / unknown
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
borderRadius: size / 2,
|
borderRadius: size / 2,
|
||||||
backgroundColor: "rgba(128,128,128,0.15)",
|
borderWidth: 1,
|
||||||
|
borderColor: color,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Minus size={iconSize} color={theme.colors.foregroundMuted} strokeWidth={3} />
|
<IconComponent size={Math.round(size * 0.5)} color={color} strokeWidth={3} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ChecksSummary({ checks }: { checks: Array<{ status: string }> }): ReactElement {
|
||||||
|
const { theme } = useUnistyles();
|
||||||
|
const counts: Record<string, number> = {};
|
||||||
|
for (const check of checks) {
|
||||||
|
const bucket = check.status === "success" ? "success" : check.status === "failure" ? "failure" : "pending";
|
||||||
|
counts[bucket] = (counts[bucket] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buckets: Array<{ status: string; count: number }> = [];
|
||||||
|
if (counts.failure) buckets.push({ status: "failure", count: counts.failure });
|
||||||
|
if (counts.success) buckets.push({ status: "success", count: counts.success });
|
||||||
|
if (counts.pending) buckets.push({ status: "pending", count: counts.pending });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{buckets.map((bucket) => {
|
||||||
|
const color = getCheckStatusColor({ status: bucket.status, theme });
|
||||||
|
return (
|
||||||
|
<View key={bucket.status} style={checksSummaryStyles.item}>
|
||||||
|
<Text style={[checksSummaryStyles.count, { color }]}>{bucket.count}</Text>
|
||||||
|
<CheckStatusIndicator status={bucket.status} size={12} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const checksSummaryStyles = StyleSheet.create((theme) => ({
|
||||||
|
item: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 3,
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
fontWeight: theme.fontWeight.medium,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
function WorkspaceHoverCardContent({
|
function WorkspaceHoverCardContent({
|
||||||
workspace,
|
workspace,
|
||||||
prHint,
|
prHint,
|
||||||
@@ -406,160 +410,136 @@ function WorkspaceHoverCardContent({
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{prHint || workspace.diffStat ? (
|
{prHint || workspace.diffStat ? (
|
||||||
<Pressable
|
<View style={styles.cardMetaRow}>
|
||||||
style={styles.cardMetaRow}
|
|
||||||
onPress={prHint ? () => void openExternalUrl(prHint.url) : undefined}
|
|
||||||
disabled={!prHint}
|
|
||||||
>
|
|
||||||
{prHint ? (
|
|
||||||
<>
|
|
||||||
<GitPullRequest size={12} color={theme.colors.foregroundMuted} />
|
|
||||||
<Text style={styles.prBadgeText} numberOfLines={1}>
|
|
||||||
#{prHint.number} · {GITHUB_PR_STATE_LABELS[prHint.state]}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{workspace.diffStat ? (
|
{workspace.diffStat ? (
|
||||||
<>
|
<View style={styles.diffStatRow}>
|
||||||
<Text style={styles.diffStatAdditions}>+{workspace.diffStat.additions}</Text>
|
<Text style={styles.diffStatAdditions}>+{workspace.diffStat.additions}</Text>
|
||||||
<Text style={styles.diffStatDeletions}>-{workspace.diffStat.deletions}</Text>
|
<Text style={styles.diffStatDeletions}>-{workspace.diffStat.deletions}</Text>
|
||||||
</>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</Pressable>
|
{prHint ? <PrBadge hint={prHint} /> : null}
|
||||||
) : null}
|
|
||||||
{prHint?.checks && prHint.checks.length > 0 ? (
|
|
||||||
<>
|
|
||||||
<View style={styles.separator} />
|
|
||||||
<Text style={styles.sectionLabel}>Checks</Text>
|
|
||||||
<View style={styles.checksList}>
|
|
||||||
{prHint.checks.map((check) => (
|
|
||||||
<Pressable
|
|
||||||
key={check.name}
|
|
||||||
style={({ hovered }) => [
|
|
||||||
styles.serviceRow,
|
|
||||||
hovered && check.url && styles.serviceRowHovered,
|
|
||||||
]}
|
|
||||||
onPress={check.url ? () => void openExternalUrl(check.url!) : undefined}
|
|
||||||
disabled={!check.url}
|
|
||||||
>
|
|
||||||
<CheckStatusIndicator status={check.status} size={14} />
|
|
||||||
<Text
|
|
||||||
style={styles.checkName}
|
|
||||||
numberOfLines={1}
|
|
||||||
>
|
|
||||||
{check.name}
|
|
||||||
</Text>
|
|
||||||
{check.url ? (
|
|
||||||
<ExternalLink size={11} color={theme.colors.foregroundMuted} />
|
|
||||||
) : null}
|
|
||||||
</Pressable>
|
|
||||||
))}
|
|
||||||
</View>
|
</View>
|
||||||
</>
|
|
||||||
) : null}
|
) : null}
|
||||||
{workspace.services.length > 0 ? (
|
{workspace.services.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<View style={styles.separator} />
|
<View style={styles.separator} />
|
||||||
<Text style={styles.sectionLabel}>Services</Text>
|
<Text style={styles.sectionLabel}>Services</Text>
|
||||||
<View style={styles.serviceList} testID="hover-card-service-list">
|
<View style={styles.sectionList} testID="hover-card-service-list">
|
||||||
{workspace.services.map((service) => (
|
{workspace.services.map((service) => {
|
||||||
<Pressable
|
const isRunning = service.lifecycle === "running";
|
||||||
key={service.hostname}
|
const isLinkable = isRunning && !!service.url;
|
||||||
accessibilityRole={service.lifecycle === "running" && service.url ? "link" : undefined}
|
return (
|
||||||
accessibilityLabel={`${service.serviceName} service`}
|
<Pressable
|
||||||
testID={`hover-card-service-${service.serviceName}`}
|
key={service.hostname}
|
||||||
style={({ hovered }) => [
|
accessibilityRole={isLinkable ? "link" : undefined}
|
||||||
styles.serviceRow,
|
accessibilityLabel={`${service.serviceName} service — ${isRunning ? getServiceHealthLabel(service.health) : "Stopped"}`}
|
||||||
hovered &&
|
testID={`hover-card-service-${service.serviceName}`}
|
||||||
service.lifecycle === "running" &&
|
style={({ hovered }) => [
|
||||||
service.url &&
|
styles.listRow,
|
||||||
styles.serviceRowHovered,
|
hovered && isLinkable && styles.listRowHovered,
|
||||||
]}
|
|
||||||
onPress={() => {
|
|
||||||
if (service.lifecycle === "running" && service.url) {
|
|
||||||
void openExternalUrl(service.url);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.serviceName,
|
|
||||||
{
|
|
||||||
color:
|
|
||||||
service.lifecycle === "running"
|
|
||||||
? theme.colors.foreground
|
|
||||||
: theme.colors.foregroundMuted,
|
|
||||||
},
|
|
||||||
]}
|
]}
|
||||||
numberOfLines={1}
|
onPress={isLinkable ? () => void openExternalUrl(service.url!) : undefined}
|
||||||
|
disabled={!isLinkable}
|
||||||
>
|
>
|
||||||
{service.serviceName}
|
{({ hovered }) => (
|
||||||
</Text>
|
<>
|
||||||
<View style={styles.serviceMeta}>
|
<View
|
||||||
<Text
|
testID={`hover-card-service-health-${service.serviceName}`}
|
||||||
testID={`hover-card-service-status-${service.serviceName}`}
|
style={[
|
||||||
accessibilityLabel={service.lifecycle === "running" ? "Running" : "Stopped"}
|
styles.statusDot,
|
||||||
style={styles.serviceLifecycleText}
|
{
|
||||||
>
|
backgroundColor: isRunning
|
||||||
{service.lifecycle === "running" ? "Running" : "Stopped"}
|
? getServiceHealthColor({ health: service.health, theme })
|
||||||
</Text>
|
: theme.colors.foregroundMuted,
|
||||||
<View
|
},
|
||||||
testID={`hover-card-service-health-${service.serviceName}`}
|
]}
|
||||||
accessibilityLabel={getServiceHealthLabel(service.health)}
|
|
||||||
style={[
|
|
||||||
styles.statusDot,
|
|
||||||
{
|
|
||||||
backgroundColor: getServiceHealthColor({
|
|
||||||
health: service.health,
|
|
||||||
theme,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Text style={styles.serviceHealthText}>{getServiceHealthLabel(service.health)}</Text>
|
|
||||||
</View>
|
|
||||||
{service.lifecycle === "running" && service.url ? (
|
|
||||||
<Text style={styles.serviceUrl} numberOfLines={1}>
|
|
||||||
{service.url.replace(/^https?:\/\//, "")}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<View style={styles.serviceUrlSpacer} />
|
|
||||||
)}
|
|
||||||
{service.lifecycle === "running" && service.url ? (
|
|
||||||
<ExternalLink size={11} color={theme.colors.foregroundMuted} />
|
|
||||||
) : (
|
|
||||||
<Pressable
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={`Start ${service.serviceName} service`}
|
|
||||||
testID={`hover-card-service-start-${service.serviceName}`}
|
|
||||||
style={({ hovered, pressed }) => [
|
|
||||||
styles.startServiceButton,
|
|
||||||
(hovered || pressed) && styles.startServiceButtonHovered,
|
|
||||||
]}
|
|
||||||
disabled={startServiceMutation.isPending}
|
|
||||||
onPress={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
startServiceMutation.mutate(service.serviceName);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{startServiceMutation.isPending &&
|
|
||||||
startServiceMutation.variables === service.serviceName ? (
|
|
||||||
<LoaderCircle
|
|
||||||
size={12}
|
|
||||||
color={theme.colors.foregroundMuted}
|
|
||||||
style={styles.startServiceSpinner}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
<Text
|
||||||
<Play size={12} color={theme.colors.foregroundMuted} fill="transparent" />
|
style={[
|
||||||
)}
|
styles.listRowLabel,
|
||||||
</Pressable>
|
{
|
||||||
)}
|
color: isRunning
|
||||||
</Pressable>
|
? theme.colors.foreground
|
||||||
))}
|
: theme.colors.foregroundMuted,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{service.serviceName}
|
||||||
|
</Text>
|
||||||
|
{isRunning && service.url ? (
|
||||||
|
<Text style={styles.listRowSecondary} numberOfLines={1}>
|
||||||
|
{service.url.replace(/^https?:\/\//, "")}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<View style={styles.listRowSpacer} />
|
||||||
|
)}
|
||||||
|
{isRunning ? (
|
||||||
|
service.url ? (
|
||||||
|
<ExternalLink
|
||||||
|
size={12}
|
||||||
|
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
) : (
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={`Start ${service.serviceName} service`}
|
||||||
|
testID={`hover-card-service-start-${service.serviceName}`}
|
||||||
|
hitSlop={4}
|
||||||
|
disabled={startServiceMutation.isPending}
|
||||||
|
onPress={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
startServiceMutation.mutate(service.serviceName);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ hovered: actionHovered }) =>
|
||||||
|
startServiceMutation.isPending &&
|
||||||
|
startServiceMutation.variables === service.serviceName ? (
|
||||||
|
<LoaderCircle size={12} color={theme.colors.foregroundMuted} />
|
||||||
|
) : (
|
||||||
|
<Play
|
||||||
|
size={12}
|
||||||
|
color={actionHovered ? theme.colors.foreground : theme.colors.foregroundMuted}
|
||||||
|
fill="transparent"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
{prHint?.checks && prHint.checks.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<View style={styles.separator} />
|
||||||
|
<Pressable
|
||||||
|
style={({ hovered }) => [
|
||||||
|
styles.checksSummaryRow,
|
||||||
|
hovered && styles.listRowHovered,
|
||||||
|
]}
|
||||||
|
onPress={() => void openExternalUrl(`${prHint.url}/checks`)}
|
||||||
|
>
|
||||||
|
{({ hovered }) => (
|
||||||
|
<>
|
||||||
|
<Text style={styles.checksSummaryLabel}>Checks</Text>
|
||||||
|
<View style={styles.checksSummaryCounts}>
|
||||||
|
<ChecksSummary checks={prHint.checks!} />
|
||||||
|
</View>
|
||||||
|
<ExternalLink
|
||||||
|
size={12}
|
||||||
|
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
</Portal>
|
</Portal>
|
||||||
@@ -609,6 +589,11 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
paddingHorizontal: theme.spacing[3],
|
paddingHorizontal: theme.spacing[3],
|
||||||
paddingBottom: theme.spacing[2],
|
paddingBottom: theme.spacing[2],
|
||||||
},
|
},
|
||||||
|
diffStatRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
diffStatAdditions: {
|
diffStatAdditions: {
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
fontWeight: theme.fontWeight.normal,
|
fontWeight: theme.fontWeight.normal,
|
||||||
@@ -619,76 +604,10 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
fontWeight: theme.fontWeight.normal,
|
fontWeight: theme.fontWeight.normal,
|
||||||
color: theme.colors.palette.red[500],
|
color: theme.colors.palette.red[500],
|
||||||
},
|
},
|
||||||
prBadgeText: {
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
color: theme.colors.foregroundMuted,
|
|
||||||
},
|
|
||||||
separator: {
|
separator: {
|
||||||
height: 1,
|
height: 1,
|
||||||
backgroundColor: theme.colors.border,
|
backgroundColor: theme.colors.border,
|
||||||
},
|
},
|
||||||
serviceList: {
|
|
||||||
paddingTop: theme.spacing[1],
|
|
||||||
},
|
|
||||||
serviceRow: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing[2],
|
|
||||||
paddingHorizontal: theme.spacing[3],
|
|
||||||
paddingVertical: theme.spacing[2],
|
|
||||||
minHeight: 32,
|
|
||||||
},
|
|
||||||
serviceRowHovered: {
|
|
||||||
backgroundColor: theme.colors.surface2,
|
|
||||||
},
|
|
||||||
statusDot: {
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
borderRadius: 4,
|
|
||||||
flexShrink: 0,
|
|
||||||
},
|
|
||||||
serviceName: {
|
|
||||||
fontSize: theme.fontSize.sm,
|
|
||||||
flexShrink: 0,
|
|
||||||
},
|
|
||||||
serviceMeta: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing[1],
|
|
||||||
flexShrink: 0,
|
|
||||||
},
|
|
||||||
serviceLifecycleText: {
|
|
||||||
color: theme.colors.foregroundMuted,
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
},
|
|
||||||
serviceHealthText: {
|
|
||||||
color: theme.colors.foregroundMuted,
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
},
|
|
||||||
serviceUrl: {
|
|
||||||
color: theme.colors.foregroundMuted,
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 0,
|
|
||||||
},
|
|
||||||
serviceUrlSpacer: {
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 0,
|
|
||||||
},
|
|
||||||
startServiceButton: {
|
|
||||||
width: 22,
|
|
||||||
height: 22,
|
|
||||||
borderRadius: 11,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
backgroundColor: theme.colors.surface2,
|
|
||||||
},
|
|
||||||
startServiceButtonHovered: {
|
|
||||||
backgroundColor: theme.colors.surface3,
|
|
||||||
},
|
|
||||||
startServiceSpinner: {
|
|
||||||
transform: [{ rotate: "0deg" }],
|
|
||||||
},
|
|
||||||
sectionLabel: {
|
sectionLabel: {
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
fontWeight: theme.fontWeight.medium,
|
fontWeight: theme.fontWeight.medium,
|
||||||
@@ -697,13 +616,59 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
paddingTop: theme.spacing[2],
|
paddingTop: theme.spacing[2],
|
||||||
paddingBottom: theme.spacing[1],
|
paddingBottom: theme.spacing[1],
|
||||||
},
|
},
|
||||||
checksList: {
|
sectionList: {
|
||||||
paddingBottom: theme.spacing[1],
|
paddingBottom: theme.spacing[1],
|
||||||
},
|
},
|
||||||
checkName: {
|
listRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
paddingHorizontal: theme.spacing[3],
|
||||||
|
paddingVertical: 6,
|
||||||
|
minHeight: 28,
|
||||||
|
},
|
||||||
|
listRowHovered: {
|
||||||
|
backgroundColor: theme.colors.surface2,
|
||||||
|
},
|
||||||
|
listRowLabel: {
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
listRowSecondary: {
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
textAlign: "right",
|
||||||
|
},
|
||||||
|
listRowSpacer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
},
|
},
|
||||||
|
statusDot: {
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
flexShrink: 0,
|
||||||
|
},
|
||||||
|
checksSummaryRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
paddingHorizontal: theme.spacing[3],
|
||||||
|
paddingVertical: 6,
|
||||||
|
minHeight: 28,
|
||||||
|
},
|
||||||
|
checksSummaryLabel: {
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
fontWeight: theme.fontWeight.medium,
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
},
|
||||||
|
checksSummaryCounts: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: theme.spacing[2],
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user