mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-29 00:01:22 +00:00
Overhaul: real metrics, Git integration UI, open-source hygiene, root cleanup
Backend: - GET /services/:id/metrics with real Docker container stats - DELETE /git/providers/:id and GET repo branches endpoint Frontend: - Service detail shows real telemetry or honest empty states - Live status distinguishes idle/polling/reconnecting - Git providers section in Settings; repo+branch pickers in Add Service - Real logo, compact Projects header, canvas fitView fix - index.css deduplicated (1725 -> 1384 lines) - Fixed smoke tests, lint errors, ToastProvider/context unification Repo hygiene: - Added LICENSE (MIT), CONTRIBUTING.md, SECURITY.md; rewrote README - Untracked .env.prod and data/letsencrypt secrets; fixed .gitignore - Removed legacy root src/internal/cmd and stale configs/docs/artifacts - Moved guides and design references under docs/ Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
355a97bab4
commit
9f3b196032
499 files changed
+4471
-94235
No files matched your search
@@ -148,6 +148,105 @@ export type UpdateUserProfileInput = {
|
||||
avatarUrl?: string;
|
||||
};
|
||||
|
||||
export type CreateUserInput = {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type UpgradeStatus = {
|
||||
imageRef: string;
|
||||
registry: string;
|
||||
dockerAvailable: boolean;
|
||||
installed: boolean;
|
||||
digest?: string;
|
||||
size?: number;
|
||||
authConfigured: boolean;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type SystemLoad = {
|
||||
load1m: number;
|
||||
load5m: number;
|
||||
load15m: number;
|
||||
};
|
||||
|
||||
export type HostMonitoring = {
|
||||
hostname: string;
|
||||
os: string;
|
||||
architecture: string;
|
||||
cpu: {
|
||||
cores: number;
|
||||
};
|
||||
memory: {
|
||||
total: number;
|
||||
used: number;
|
||||
available: number;
|
||||
usagePercent: number;
|
||||
};
|
||||
storage: {
|
||||
path: string;
|
||||
total: number;
|
||||
used: number;
|
||||
available: number;
|
||||
usagePercent: number;
|
||||
};
|
||||
load: SystemLoad;
|
||||
uptimeSeconds: number;
|
||||
dockerAvailable: boolean;
|
||||
docker?: {
|
||||
containers?: number;
|
||||
images?: number;
|
||||
driver?: string;
|
||||
server?: string;
|
||||
};
|
||||
collectedAt: string;
|
||||
};
|
||||
|
||||
export type AgentCapabilities = {
|
||||
containerRuntimes: string[];
|
||||
supportedArchitectures: string[];
|
||||
maxContainers: number;
|
||||
storageDriver: string;
|
||||
networkPlugins: string[];
|
||||
features: string[];
|
||||
};
|
||||
|
||||
export type NodeResources = {
|
||||
cpu: {
|
||||
cores: number;
|
||||
allocation: number;
|
||||
usage: number;
|
||||
};
|
||||
memory: {
|
||||
total: number;
|
||||
allocated: number;
|
||||
used: number;
|
||||
available: number;
|
||||
};
|
||||
storage: {
|
||||
total: number;
|
||||
allocated: number;
|
||||
used: number;
|
||||
available: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type NodeAgentEntity = {
|
||||
id: string;
|
||||
name: string;
|
||||
hostname: string;
|
||||
ipAddress: string;
|
||||
port: number;
|
||||
status: string;
|
||||
version: string;
|
||||
capabilities: AgentCapabilities;
|
||||
resources: NodeResources;
|
||||
lastHeartbeat?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
export type AuditLogEntity = {
|
||||
id: string;
|
||||
userId: string;
|
||||
@@ -256,6 +355,91 @@ type RawServiceLog = components['schemas']['ServiceLogEntry'];
|
||||
type RawServiceLogsResponse = components['schemas']['ServiceLogsResponse'];
|
||||
type RawDeploymentLogsResponse = components['schemas']['DeploymentLogsResponse'];
|
||||
type RawRollbackDeploymentResponse = components['schemas']['RollbackDeploymentResponse'];
|
||||
type RawUpgradeStatus = {
|
||||
image_ref?: string;
|
||||
registry?: string;
|
||||
docker_available?: boolean;
|
||||
installed?: boolean;
|
||||
digest?: string;
|
||||
size?: number;
|
||||
auth_configured?: boolean;
|
||||
message?: string;
|
||||
};
|
||||
type RawSystemLoad = {
|
||||
load_1m?: number;
|
||||
load_5m?: number;
|
||||
load_15m?: number;
|
||||
};
|
||||
type RawHostMonitoring = {
|
||||
hostname?: string;
|
||||
os?: string;
|
||||
architecture?: string;
|
||||
cpu?: {
|
||||
cores?: number;
|
||||
};
|
||||
memory?: {
|
||||
total?: number;
|
||||
used?: number;
|
||||
available?: number;
|
||||
usage_percent?: number;
|
||||
};
|
||||
storage?: {
|
||||
path?: string;
|
||||
total?: number;
|
||||
used?: number;
|
||||
available?: number;
|
||||
usage_percent?: number;
|
||||
};
|
||||
load?: RawSystemLoad;
|
||||
uptime_seconds?: number;
|
||||
docker_available?: boolean;
|
||||
docker?: {
|
||||
containers?: number;
|
||||
images?: number;
|
||||
driver?: string;
|
||||
server?: string;
|
||||
};
|
||||
collected_at?: string;
|
||||
};
|
||||
type RawNodeAgent = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
hostname?: string;
|
||||
ip_address?: string;
|
||||
port?: number;
|
||||
status?: string;
|
||||
version?: string;
|
||||
capabilities?: {
|
||||
container_runtimes?: string[];
|
||||
supported_architectures?: string[];
|
||||
max_containers?: number;
|
||||
storage_driver?: string;
|
||||
network_plugins?: string[];
|
||||
features?: string[];
|
||||
};
|
||||
resources?: {
|
||||
cpu?: {
|
||||
cores?: number;
|
||||
allocation?: number;
|
||||
usage?: number;
|
||||
};
|
||||
memory?: {
|
||||
total?: number;
|
||||
allocated?: number;
|
||||
used?: number;
|
||||
available?: number;
|
||||
};
|
||||
storage?: {
|
||||
total?: number;
|
||||
allocated?: number;
|
||||
used?: number;
|
||||
available?: number;
|
||||
};
|
||||
};
|
||||
last_heartbeat?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
export type CreateProjectInput = components['schemas']['CreateProjectRequest'];
|
||||
export type CreateServiceInput = components['schemas']['CreateServiceRequest'] & {
|
||||
@@ -286,12 +470,17 @@ type JsonLike = Record<string, unknown>;
|
||||
|
||||
const rawBase = (import.meta.env.VITE_API_URL as string | undefined) ?? 'http://localhost:8082';
|
||||
const normalizedBase = rawBase.replace(/\/$/, '');
|
||||
const API_ROOT = normalizedBase.replace(/\/api\/v1$/, '').replace(/\/api$/, '');
|
||||
const API_BASE = /\/api\/v1$/.test(normalizedBase) ? normalizedBase : `${normalizedBase}/api/v1`;
|
||||
|
||||
export function getApiBaseUrl(): string {
|
||||
return API_BASE;
|
||||
}
|
||||
|
||||
export function getAgentPublicBaseUrl(): string {
|
||||
return `${API_ROOT}/api/agents`;
|
||||
}
|
||||
|
||||
function authHeaders(): HeadersInit {
|
||||
return { 'Content-Type': 'application/json' };
|
||||
}
|
||||
@@ -669,6 +858,151 @@ export async function updateCurrentUserProfile(input: UpdateUserProfileInput): P
|
||||
return profile;
|
||||
}
|
||||
|
||||
export async function createUser(input: CreateUserInput): Promise<UserProfile> {
|
||||
const payload = await requestJson<RawUserProfile | { user?: RawUserProfile }>(`/users`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
const rawProfile = (payload as { user?: RawUserProfile }).user ?? (payload as RawUserProfile);
|
||||
const profile = normalizeUserProfile(rawProfile);
|
||||
|
||||
if (!profile) {
|
||||
throw new ApiError('Create user response is invalid', 500);
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
function normalizeUpgradeStatus(payload: RawUpgradeStatus): UpgradeStatus {
|
||||
return {
|
||||
imageRef: payload.image_ref ?? '',
|
||||
registry: payload.registry ?? '',
|
||||
dockerAvailable: Boolean(payload.docker_available),
|
||||
installed: Boolean(payload.installed),
|
||||
digest: payload.digest,
|
||||
size: payload.size,
|
||||
authConfigured: Boolean(payload.auth_configured),
|
||||
message: payload.message ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSystemLoad(payload?: RawSystemLoad): SystemLoad {
|
||||
return {
|
||||
load1m: payload?.load_1m ?? 0,
|
||||
load5m: payload?.load_5m ?? 0,
|
||||
load15m: payload?.load_15m ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeHostMonitoring(payload: RawHostMonitoring): HostMonitoring {
|
||||
return {
|
||||
hostname: payload.hostname ?? '',
|
||||
os: payload.os ?? '',
|
||||
architecture: payload.architecture ?? '',
|
||||
cpu: {
|
||||
cores: payload.cpu?.cores ?? 0,
|
||||
},
|
||||
memory: {
|
||||
total: payload.memory?.total ?? 0,
|
||||
used: payload.memory?.used ?? 0,
|
||||
available: payload.memory?.available ?? 0,
|
||||
usagePercent: payload.memory?.usage_percent ?? 0,
|
||||
},
|
||||
storage: {
|
||||
path: payload.storage?.path ?? '/',
|
||||
total: payload.storage?.total ?? 0,
|
||||
used: payload.storage?.used ?? 0,
|
||||
available: payload.storage?.available ?? 0,
|
||||
usagePercent: payload.storage?.usage_percent ?? 0,
|
||||
},
|
||||
load: normalizeSystemLoad(payload.load),
|
||||
uptimeSeconds: payload.uptime_seconds ?? 0,
|
||||
dockerAvailable: Boolean(payload.docker_available),
|
||||
docker: payload.docker,
|
||||
collectedAt: payload.collected_at ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAgent(agent: RawNodeAgent): NodeAgentEntity | null {
|
||||
if (!agent.id || !agent.name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: agent.id,
|
||||
name: agent.name,
|
||||
hostname: agent.hostname ?? '',
|
||||
ipAddress: agent.ip_address ?? '',
|
||||
port: agent.port ?? 0,
|
||||
status: agent.status ?? 'unknown',
|
||||
version: agent.version ?? '',
|
||||
capabilities: {
|
||||
containerRuntimes: agent.capabilities?.container_runtimes ?? [],
|
||||
supportedArchitectures: agent.capabilities?.supported_architectures ?? [],
|
||||
maxContainers: agent.capabilities?.max_containers ?? 0,
|
||||
storageDriver: agent.capabilities?.storage_driver ?? '',
|
||||
networkPlugins: agent.capabilities?.network_plugins ?? [],
|
||||
features: agent.capabilities?.features ?? [],
|
||||
},
|
||||
resources: {
|
||||
cpu: {
|
||||
cores: agent.resources?.cpu?.cores ?? 0,
|
||||
allocation: agent.resources?.cpu?.allocation ?? 0,
|
||||
usage: agent.resources?.cpu?.usage ?? 0,
|
||||
},
|
||||
memory: {
|
||||
total: agent.resources?.memory?.total ?? 0,
|
||||
allocated: agent.resources?.memory?.allocated ?? 0,
|
||||
used: agent.resources?.memory?.used ?? 0,
|
||||
available: agent.resources?.memory?.available ?? 0,
|
||||
},
|
||||
storage: {
|
||||
total: agent.resources?.storage?.total ?? 0,
|
||||
allocated: agent.resources?.storage?.allocated ?? 0,
|
||||
used: agent.resources?.storage?.used ?? 0,
|
||||
available: agent.resources?.storage?.available ?? 0,
|
||||
},
|
||||
},
|
||||
lastHeartbeat: agent.last_heartbeat,
|
||||
createdAt: agent.created_at,
|
||||
updatedAt: agent.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAgentArray(raw: unknown): NodeAgentEntity[] {
|
||||
if (!Array.isArray(raw)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return raw
|
||||
.map((entry) => normalizeAgent(entry as RawNodeAgent))
|
||||
.filter((entry): entry is NodeAgentEntity => entry !== null);
|
||||
}
|
||||
|
||||
export async function getUpgradeStatus(): Promise<UpgradeStatus> {
|
||||
const payload = await requestJson<RawUpgradeStatus>(`/system/upgrade/status`);
|
||||
return normalizeUpgradeStatus(payload);
|
||||
}
|
||||
|
||||
export async function pullUpgradeImage(): Promise<UpgradeStatus> {
|
||||
const payload = await requestJson<RawUpgradeStatus>(`/system/upgrade/pull`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
return normalizeUpgradeStatus(payload);
|
||||
}
|
||||
|
||||
export async function getHostMonitoring(): Promise<HostMonitoring> {
|
||||
const payload = await requestJson<RawHostMonitoring>(`/system/host`);
|
||||
return normalizeHostMonitoring(payload);
|
||||
}
|
||||
|
||||
export async function listAgents(): Promise<NodeAgentEntity[]> {
|
||||
const payload = await requestJson<{ agents?: RawNodeAgent[] }>(`/agents`);
|
||||
return normalizeAgentArray(payload.agents);
|
||||
}
|
||||
|
||||
export async function createProject(input: CreateProjectInput): Promise<ProjectEntity> {
|
||||
const payload = await requestJson<RawProject | { project?: RawProject }>(`/projects`, {
|
||||
method: 'POST',
|
||||
@@ -958,3 +1292,109 @@ export function serviceStatusClass(status: ServiceStatus): string {
|
||||
return 'status-stopped';
|
||||
}
|
||||
}
|
||||
|
||||
export type ServiceInstanceMetrics = {
|
||||
container_id: string;
|
||||
name: string;
|
||||
state: string;
|
||||
cpu_percent: number;
|
||||
memory_usage_bytes: number;
|
||||
memory_limit_bytes: number;
|
||||
network_rx_bytes: number;
|
||||
network_tx_bytes: number;
|
||||
started_at?: string;
|
||||
};
|
||||
|
||||
export type ServiceMetrics = {
|
||||
service_id: string;
|
||||
status: 'ok' | 'no_containers' | 'docker_unavailable';
|
||||
instances: ServiceInstanceMetrics[];
|
||||
cpu_percent: number;
|
||||
memory_usage_bytes: number;
|
||||
memory_limit_bytes: number;
|
||||
network_rx_bytes: number;
|
||||
network_tx_bytes: number;
|
||||
collected_at: string;
|
||||
};
|
||||
|
||||
export async function getServiceMetrics(serviceId: string): Promise<ServiceMetrics> {
|
||||
const payload = await requestJson<{ metrics?: ServiceMetrics }>(`/services/${serviceId}/metrics`);
|
||||
if (!payload.metrics) {
|
||||
throw new ApiError('Service metrics payload is invalid', 500);
|
||||
}
|
||||
return payload.metrics;
|
||||
}
|
||||
|
||||
export type GitProviderEntity = {
|
||||
id: string;
|
||||
name: string;
|
||||
display_name: string;
|
||||
api_url: string;
|
||||
created_at?: string;
|
||||
};
|
||||
|
||||
export type GitRepositoryEntity = {
|
||||
id: string;
|
||||
provider_id: string;
|
||||
name: string;
|
||||
full_name: string;
|
||||
description: string;
|
||||
clone_url: string;
|
||||
default_branch: string;
|
||||
is_private: boolean;
|
||||
};
|
||||
|
||||
export type GitBranchEntity = {
|
||||
name: string;
|
||||
protected: boolean;
|
||||
};
|
||||
|
||||
export async function listGitProviders(): Promise<GitProviderEntity[]> {
|
||||
const payload = await requestJson<{ providers?: GitProviderEntity[] }>('/git/providers');
|
||||
return payload.providers ?? [];
|
||||
}
|
||||
|
||||
export async function createGitProvider(input: {
|
||||
name: 'github' | 'gitlab' | 'bitbucket' | 'gitea';
|
||||
display_name: string;
|
||||
access_token: string;
|
||||
api_url?: string;
|
||||
}): Promise<GitProviderEntity> {
|
||||
const payload = await requestJson<GitProviderEntity>('/git/providers', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
if (!payload.id) {
|
||||
throw new ApiError('Provider response is invalid', 500);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function deleteGitProvider(providerId: string): Promise<void> {
|
||||
await requestJson(`/git/providers/${providerId}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export async function listGitRepositories(
|
||||
providerId: string,
|
||||
search?: string,
|
||||
): Promise<GitRepositoryEntity[]> {
|
||||
const params = new URLSearchParams({ limit: '100' });
|
||||
if (search?.trim()) {
|
||||
params.set('search', search.trim());
|
||||
}
|
||||
const payload = await requestJson<{ repositories?: GitRepositoryEntity[] }>(
|
||||
`/git/providers/${providerId}/repositories?${params.toString()}`,
|
||||
);
|
||||
return payload.repositories ?? [];
|
||||
}
|
||||
|
||||
export async function listGitBranches(
|
||||
providerId: string,
|
||||
owner: string,
|
||||
repo: string,
|
||||
): Promise<GitBranchEntity[]> {
|
||||
const payload = await requestJson<{ branches?: GitBranchEntity[] }>(
|
||||
`/git/providers/${providerId}/repositories/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branches`,
|
||||
);
|
||||
return payload.branches ?? [];
|
||||
}
|
||||
@@ -16,6 +16,12 @@ export type AuthSessionPayload = {
|
||||
session: AuthSession;
|
||||
};
|
||||
|
||||
export type AuthBootstrap = {
|
||||
hasUsers: boolean;
|
||||
userCount: number;
|
||||
mode: 'register' | 'login';
|
||||
};
|
||||
|
||||
export class AuthError extends Error {
|
||||
readonly status: number;
|
||||
|
||||
@@ -33,6 +39,30 @@ export function getAuthBaseUrl(): string {
|
||||
return AUTH_BASE;
|
||||
}
|
||||
|
||||
export async function getAuthBootstrap(): Promise<AuthBootstrap> {
|
||||
const apiBase = ((import.meta.env.VITE_API_URL as string | undefined) ?? 'http://localhost:8082').replace(/\/$/, '');
|
||||
const normalizedApiBase = /\/api\/v1$/.test(apiBase) ? apiBase : `${apiBase}/api/v1`;
|
||||
const response = await fetch(`${normalizedApiBase}/auth/bootstrap`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
});
|
||||
const payload = (await response.json().catch(() => null)) as Record<string, unknown> | null;
|
||||
|
||||
if (!response.ok) {
|
||||
const message =
|
||||
(payload?.message as string | undefined) ??
|
||||
(payload?.error as string | undefined) ??
|
||||
`Auth bootstrap request failed with status ${response.status}`;
|
||||
throw new AuthError(message, response.status);
|
||||
}
|
||||
|
||||
return {
|
||||
hasUsers: Boolean(payload?.has_users),
|
||||
userCount: typeof payload?.user_count === 'number' ? payload.user_count : 0,
|
||||
mode: payload?.mode === 'register' ? 'register' : 'login',
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSessionPayload(payload: unknown): AuthSessionPayload | null {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return null;
|
||||
@@ -160,19 +190,6 @@ export async function signUpWithEmail(name: string, email: string, password: str
|
||||
return normalizeSessionPayload(payload);
|
||||
}
|
||||
|
||||
export async function requestMagicLinkInvite(email: string, callbackURL: string): Promise<void> {
|
||||
await authRequest('/sign-in/magic-link', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
callbackURL,
|
||||
metadata: {
|
||||
invite: true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function startGitHubSignIn(callbackURL: string): Promise<void> {
|
||||
const payload = await authRequest('/sign-in/social', {
|
||||
method: 'POST',
|
||||
@@ -189,11 +206,11 @@ export async function startGitHubSignIn(callbackURL: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function startOAuth2ProviderSignIn(providerId: string, callbackURL: string): Promise<void> {
|
||||
const payload = await authRequest('/sign-in/oauth2', {
|
||||
export async function startGoogleSignIn(callbackURL: string): Promise<void> {
|
||||
const payload = await authRequest('/sign-in/social', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
providerId,
|
||||
provider: 'google',
|
||||
callbackURL,
|
||||
disableRedirect: true,
|
||||
}),
|
||||
@@ -205,18 +222,6 @@ async function startOAuth2ProviderSignIn(providerId: string, callbackURL: string
|
||||
}
|
||||
}
|
||||
|
||||
export async function startGitLabSignIn(callbackURL: string): Promise<void> {
|
||||
await startOAuth2ProviderSignIn('gitlab', callbackURL);
|
||||
}
|
||||
|
||||
export async function startBitbucketSignIn(callbackURL: string): Promise<void> {
|
||||
await startOAuth2ProviderSignIn('bitbucket', callbackURL);
|
||||
}
|
||||
|
||||
export async function startGiteaSignIn(callbackURL: string): Promise<void> {
|
||||
await startOAuth2ProviderSignIn('gitea', callbackURL);
|
||||
}
|
||||
|
||||
export async function signOutAuthSession(): Promise<void> {
|
||||
await authRequest('/sign-out', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -36,6 +36,16 @@ export function formatDate(iso?: string): string {
|
||||
return dt.toLocaleString();
|
||||
}
|
||||
|
||||
export function formatBytes(bytes: number): string {
|
||||
if (!Number.isFinite(bytes) || bytes <= 0) {
|
||||
return '0 B';
|
||||
}
|
||||
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
||||
const index = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
|
||||
const value = bytes / Math.pow(1024, index);
|
||||
return `${value >= 100 ? Math.round(value) : value.toFixed(1)} ${units[index]}`;
|
||||
}
|
||||
|
||||
export function seededMetric(seed: string, min: number, max: number): number {
|
||||
let hash = 0;
|
||||
for (let idx = 0; idx < seed.length; idx += 1) {
|
||||
|
||||
@@ -17,10 +17,12 @@ function toWebSocketUrl(apiBase: string): string {
|
||||
return `${wsBase}/ws`;
|
||||
}
|
||||
|
||||
export type LiveSyncStatus = 'idle' | 'offline' | 'live';
|
||||
|
||||
export function useBuildUpdates(
|
||||
buildIds: string[],
|
||||
onBuildUpdate: (payload: BuildUpdatePayload) => void,
|
||||
): boolean {
|
||||
): LiveSyncStatus {
|
||||
const [connected, setConnected] = useState(false);
|
||||
const callbackRef = useRef(onBuildUpdate);
|
||||
useEffect(() => {
|
||||
@@ -127,5 +129,8 @@ export function useBuildUpdates(
|
||||
};
|
||||
}, [buildIdsKey, subscriptionIds]);
|
||||
|
||||
return subscriptionIds.length > 0 && connected;
|
||||
if (subscriptionIds.length === 0) {
|
||||
return 'idle';
|
||||
}
|
||||
return connected ? 'live' : 'offline';
|
||||
}
|
||||
Reference in new issue
Block a user