Merge branch 'feat/busy-container-overlays' into dev
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Icon } from "../../icon";
|
import { Icon } from "../../icon";
|
||||||
|
import { LoadingOverlay } from "../../loading-overlay";
|
||||||
import {
|
import {
|
||||||
createInstance,
|
createInstance,
|
||||||
startInstance,
|
startInstance,
|
||||||
@@ -215,6 +216,7 @@ export const CreateSessionForm = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`create-session-form-wrapper ${className}`}>
|
<div className={`create-session-form-wrapper ${className}`}>
|
||||||
|
<LoadingOverlay visible={isSubmitting} label="Creating session..." />
|
||||||
<form onSubmit={handleSubmit} className="stack create-session-form">
|
<form onSubmit={handleSubmit} className="stack create-session-form">
|
||||||
{/* Project */}
|
{/* Project */}
|
||||||
<div className="form-field">
|
<div className="form-field">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Session } from "../../../api/sessions";
|
|||||||
import { Icon } from "../../icon";
|
import { Icon } from "../../icon";
|
||||||
import { useMobileViewport } from "../../../hooks/use-mobile-viewport";
|
import { useMobileViewport } from "../../../hooks/use-mobile-viewport";
|
||||||
import { MobileActionSheet } from "../mobile/mobile-action-sheet";
|
import { MobileActionSheet } from "../mobile/mobile-action-sheet";
|
||||||
|
import { LoadingOverlay } from "../../loading-overlay";
|
||||||
import type { IconName } from "../../icon";
|
import type { IconName } from "../../icon";
|
||||||
|
|
||||||
export interface SessionCardProps {
|
export interface SessionCardProps {
|
||||||
@@ -14,6 +15,7 @@ export interface SessionCardProps {
|
|||||||
onRecreateTunnel?: (session: Session) => void;
|
onRecreateTunnel?: (session: Session) => void;
|
||||||
onRename?: (session: Session, newName: string) => void;
|
onRename?: (session: Session, newName: string) => void;
|
||||||
isBusy?: boolean;
|
isBusy?: boolean;
|
||||||
|
busyLabel?: string;
|
||||||
tunnelHealth?: {
|
tunnelHealth?: {
|
||||||
healthy: boolean;
|
healthy: boolean;
|
||||||
container_status: string;
|
container_status: string;
|
||||||
@@ -46,6 +48,7 @@ export function SessionCard({
|
|||||||
onRecreateTunnel,
|
onRecreateTunnel,
|
||||||
onRename,
|
onRename,
|
||||||
isBusy = false,
|
isBusy = false,
|
||||||
|
busyLabel = "Working...",
|
||||||
tunnelHealth = null,
|
tunnelHealth = null,
|
||||||
}: SessionCardProps) {
|
}: SessionCardProps) {
|
||||||
const [showActionSheet, setShowActionSheet] = useState(false);
|
const [showActionSheet, setShowActionSheet] = useState(false);
|
||||||
@@ -106,6 +109,7 @@ export function SessionCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="card session-card">
|
<article className="card session-card">
|
||||||
|
<LoadingOverlay visible={isBusy} label={busyLabel} />
|
||||||
<div className="session-card-content">
|
<div className="session-card-content">
|
||||||
<div className="session-card-header">
|
<div className="session-card-header">
|
||||||
<div className="session-card-title">
|
<div className="session-card-title">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Icon } from "../../icon";
|
import { Icon } from "../../icon";
|
||||||
|
import { LoadingOverlay } from "../../loading-overlay";
|
||||||
import type { ToolInstance } from "../../../api/sessions";
|
import type { ToolInstance } from "../../../api/sessions";
|
||||||
import {
|
import {
|
||||||
deleteInstance,
|
deleteInstance,
|
||||||
@@ -60,6 +61,7 @@ export const InstanceList = ({
|
|||||||
|
|
||||||
// Per-instance busy state for actions
|
// Per-instance busy state for actions
|
||||||
const [busyInstanceId, setBusyInstanceId] = useState<string | null>(null);
|
const [busyInstanceId, setBusyInstanceId] = useState<string | null>(null);
|
||||||
|
const [busyLabel, setBusyLabel] = useState("Working...");
|
||||||
|
|
||||||
const loadInstances = useCallback(async () => {
|
const loadInstances = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -135,6 +137,7 @@ export const InstanceList = ({
|
|||||||
sshKeyIds?: string[],
|
sshKeyIds?: string[],
|
||||||
) => {
|
) => {
|
||||||
setBusyInstanceId(instanceId);
|
setBusyInstanceId(instanceId);
|
||||||
|
setBusyLabel("Starting...");
|
||||||
try {
|
try {
|
||||||
await startInstance(
|
await startInstance(
|
||||||
projectId,
|
projectId,
|
||||||
@@ -156,6 +159,7 @@ export const InstanceList = ({
|
|||||||
|
|
||||||
const handleStop = async (instanceId: string) => {
|
const handleStop = async (instanceId: string) => {
|
||||||
setBusyInstanceId(instanceId);
|
setBusyInstanceId(instanceId);
|
||||||
|
setBusyLabel("Stopping...");
|
||||||
try {
|
try {
|
||||||
await stopInstance(projectId, repoId, instanceId);
|
await stopInstance(projectId, repoId, instanceId);
|
||||||
setStopConfirmId(null);
|
setStopConfirmId(null);
|
||||||
@@ -173,6 +177,7 @@ export const InstanceList = ({
|
|||||||
sshKeyIds?: string[],
|
sshKeyIds?: string[],
|
||||||
) => {
|
) => {
|
||||||
setBusyInstanceId(instanceId);
|
setBusyInstanceId(instanceId);
|
||||||
|
setBusyLabel("Restarting...");
|
||||||
try {
|
try {
|
||||||
await restartInstance(
|
await restartInstance(
|
||||||
projectId,
|
projectId,
|
||||||
@@ -195,6 +200,7 @@ export const InstanceList = ({
|
|||||||
const handleDelete = async (instanceId: string) => {
|
const handleDelete = async (instanceId: string) => {
|
||||||
if (!confirm("Are you sure you want to delete this instance?")) return;
|
if (!confirm("Are you sure you want to delete this instance?")) return;
|
||||||
setBusyInstanceId(instanceId);
|
setBusyInstanceId(instanceId);
|
||||||
|
setBusyLabel("Deleting...");
|
||||||
try {
|
try {
|
||||||
await deleteInstance(projectId, repoId, instanceId);
|
await deleteInstance(projectId, repoId, instanceId);
|
||||||
// Update state immediately instead of reloading
|
// Update state immediately instead of reloading
|
||||||
@@ -250,6 +256,10 @@ export const InstanceList = ({
|
|||||||
<div className="instance-grid">
|
<div className="instance-grid">
|
||||||
{instances.map((instance) => (
|
{instances.map((instance) => (
|
||||||
<div key={instance.id} className="instance-card">
|
<div key={instance.id} className="instance-card">
|
||||||
|
<LoadingOverlay
|
||||||
|
visible={busyInstanceId === instance.id}
|
||||||
|
label={busyLabel}
|
||||||
|
/>
|
||||||
<div className="instance-info">
|
<div className="instance-info">
|
||||||
<div className="instance-name">{instance.display_name}</div>
|
<div className="instance-name">{instance.display_name}</div>
|
||||||
<div className="instance-meta">
|
<div className="instance-meta">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { Icon } from "../../icon";
|
import { Icon } from "../../icon";
|
||||||
|
import { LoadingOverlay } from "../../loading-overlay";
|
||||||
import { listToolTypes, type ToolType } from "../../../api/tool-types";
|
import { listToolTypes, type ToolType } from "../../../api/tool-types";
|
||||||
import {
|
import {
|
||||||
listConfigProfiles,
|
listConfigProfiles,
|
||||||
@@ -172,6 +173,7 @@ export function ToolStarter({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tool-starter">
|
<div className="tool-starter">
|
||||||
|
<LoadingOverlay visible={starting} label="Starting tool..." />
|
||||||
{/* Context header — read-only workspace info */}
|
{/* Context header — read-only workspace info */}
|
||||||
<div className="tool-starter-context">
|
<div className="tool-starter-context">
|
||||||
<div className="context-row">
|
<div className="context-row">
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Icon } from "./icon";
|
||||||
|
|
||||||
|
interface LoadingOverlayProps {
|
||||||
|
visible: boolean;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoadingOverlay({ visible, label }: LoadingOverlayProps) {
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="loading-overlay" aria-live="polite">
|
||||||
|
<div className="loading-overlay-content">
|
||||||
|
<Icon name="loading" size="lg" className="icon-spin" ariaLabel={label} />
|
||||||
|
{label && (
|
||||||
|
<span className="loading-overlay-label">{label}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -383,6 +383,51 @@ a.nav-item,
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Loading Overlay
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.loading-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.12);
|
||||||
|
backdrop-filter: blur(1px);
|
||||||
|
border-radius: inherit;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-overlay-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
color: var(--text, currentColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-overlay-label {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-spin {
|
||||||
|
animation: icon-spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes icon-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-card,
|
||||||
|
.tool-starter,
|
||||||
|
.create-session-form-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
Terminal Styles
|
Terminal Styles
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|||||||
Reference in New Issue
Block a user