mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-08 21:14:47 +09:00
feat(13): Create LinkedAppsSection component for showing linked apps
This commit is contained in:
1 parent
60ba8967b8
commit
92df579228
3 files changed
+372
No files matched your search
@@ -0,0 +1,151 @@
|
||||
import React from 'react';
|
||||
import { GitBranch, Link2, Unlink, ExternalLink, Server } from 'lucide-react';
|
||||
|
||||
const LinkedAppsSection = ({
|
||||
app,
|
||||
linkedApps,
|
||||
onLink,
|
||||
onUnlink,
|
||||
onNavigate,
|
||||
loading
|
||||
}) => {
|
||||
const envLabels = {
|
||||
production: 'Production',
|
||||
development: 'Development',
|
||||
staging: 'Staging',
|
||||
standalone: 'Standalone'
|
||||
};
|
||||
|
||||
const envColors = {
|
||||
production: 'env-production',
|
||||
development: 'env-development',
|
||||
staging: 'env-staging'
|
||||
};
|
||||
|
||||
const getStatusClass = (status) => {
|
||||
switch (status) {
|
||||
case 'running': return 'status-active';
|
||||
case 'stopped': return 'status-stopped';
|
||||
case 'error': return 'status-error';
|
||||
default: return 'status-warning';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card linked-apps-section">
|
||||
<div className="linked-apps-header">
|
||||
<h3>
|
||||
<GitBranch size={18} />
|
||||
Environment Linking
|
||||
</h3>
|
||||
{!app.has_linked_app && (
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={onLink}
|
||||
disabled={loading}
|
||||
>
|
||||
<Link2 size={14} />
|
||||
Link App
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="linked-apps-content">
|
||||
{/* Current app environment */}
|
||||
<div className="current-environment">
|
||||
<span className="env-label">This app is:</span>
|
||||
<span className={`env-badge ${envColors[app.environment_type] || ''}`}>
|
||||
{envLabels[app.environment_type] || 'Standalone'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Linked apps list */}
|
||||
{linkedApps && linkedApps.length > 0 ? (
|
||||
<div className="linked-apps-list">
|
||||
<div className="linked-apps-divider">
|
||||
<span>Linked to</span>
|
||||
</div>
|
||||
{linkedApps.map(linkedApp => (
|
||||
<div key={linkedApp.id} className="linked-app-item">
|
||||
<div className="linked-app-info">
|
||||
<div className="linked-app-icon">
|
||||
<Server size={16} />
|
||||
</div>
|
||||
<div className="linked-app-details">
|
||||
<span className="linked-app-name">{linkedApp.name}</span>
|
||||
<div className="linked-app-meta">
|
||||
<span className={`env-badge ${envColors[linkedApp.environment_type] || ''}`}>
|
||||
{envLabels[linkedApp.environment_type] || linkedApp.environment_type}
|
||||
</span>
|
||||
<span className={`status-badge-sm ${getStatusClass(linkedApp.status)}`}>
|
||||
<span className="status-dot" />
|
||||
{linkedApp.status}
|
||||
</span>
|
||||
{linkedApp.port && (
|
||||
<span className="linked-app-port">:{linkedApp.port}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="linked-app-actions">
|
||||
<button
|
||||
className="btn btn-secondary btn-sm btn-icon"
|
||||
onClick={() => onNavigate(linkedApp.id)}
|
||||
title="View app"
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm btn-icon"
|
||||
onClick={onUnlink}
|
||||
disabled={loading}
|
||||
title="Unlink apps"
|
||||
>
|
||||
<Unlink size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : app.environment_type !== 'standalone' ? (
|
||||
<div className="linked-apps-empty">
|
||||
<p>No linked apps. Link another app to share database resources.</p>
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={onLink}
|
||||
disabled={loading}
|
||||
>
|
||||
<Link2 size={14} />
|
||||
Link App
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="linked-apps-info">
|
||||
<p>
|
||||
Link this app to another to create a production/development pair.
|
||||
Linked apps can share database credentials with different table prefixes.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Shared config info */}
|
||||
{app.shared_config && app.shared_config.db_credentials_propagated && (
|
||||
<div className="shared-config-info">
|
||||
<div className="shared-config-badge">
|
||||
<Server size={12} />
|
||||
Shared Database
|
||||
</div>
|
||||
{app.shared_config.shared_db && (
|
||||
<div className="shared-config-details">
|
||||
<span>Host: {app.shared_config.shared_db.db_host}</span>
|
||||
<span>Table prefix: {app.shared_config.shared_db.target_prefix || app.shared_config.shared_db.source_prefix}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinkedAppsSection;
|
||||
@@ -0,0 +1,220 @@
|
||||
// ============================================
|
||||
// LINKED APPS SECTION
|
||||
// ============================================
|
||||
|
||||
.linked-apps-section {
|
||||
margin-top: @space-4;
|
||||
}
|
||||
|
||||
.linked-apps-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: @space-4;
|
||||
|
||||
h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: @space-2;
|
||||
margin: 0;
|
||||
font-size: @font-size-base;
|
||||
font-weight: @font-weight-semibold;
|
||||
color: @text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.linked-apps-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-4;
|
||||
}
|
||||
|
||||
.current-environment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: @space-3;
|
||||
padding: @space-3;
|
||||
background: @bg-hover;
|
||||
border-radius: @radius-md;
|
||||
|
||||
.env-label {
|
||||
font-size: @font-size-sm;
|
||||
color: @text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.linked-apps-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: @space-3;
|
||||
color: @text-tertiary;
|
||||
font-size: @font-size-sm;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: @border-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
.linked-apps-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-3;
|
||||
}
|
||||
|
||||
.linked-app-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: @space-3;
|
||||
background: @bg-card;
|
||||
border: 1px solid @border-subtle;
|
||||
border-radius: @radius-md;
|
||||
transition: border-color @transition-fast;
|
||||
|
||||
&:hover {
|
||||
border-color: @border-default;
|
||||
}
|
||||
}
|
||||
|
||||
.linked-app-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: @space-3;
|
||||
}
|
||||
|
||||
.linked-app-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: @accent-glow;
|
||||
color: @accent-primary;
|
||||
border-radius: @radius-md;
|
||||
}
|
||||
|
||||
.linked-app-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-1;
|
||||
}
|
||||
|
||||
.linked-app-name {
|
||||
font-weight: @font-weight-medium;
|
||||
color: @text-primary;
|
||||
}
|
||||
|
||||
.linked-app-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: @space-2;
|
||||
font-size: @font-size-sm;
|
||||
}
|
||||
|
||||
.linked-app-port {
|
||||
color: @text-tertiary;
|
||||
font-family: @font-family-mono;
|
||||
font-size: @font-size-xs;
|
||||
}
|
||||
|
||||
.linked-app-actions {
|
||||
display: flex;
|
||||
gap: @space-2;
|
||||
}
|
||||
|
||||
.linked-apps-empty,
|
||||
.linked-apps-info {
|
||||
padding: @space-4;
|
||||
text-align: center;
|
||||
background: @bg-hover;
|
||||
border-radius: @radius-md;
|
||||
|
||||
p {
|
||||
margin: 0 0 @space-3;
|
||||
color: @text-secondary;
|
||||
font-size: @font-size-sm;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-top: @space-2;
|
||||
}
|
||||
}
|
||||
|
||||
.linked-apps-info p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Shared config info
|
||||
.shared-config-info {
|
||||
padding: @space-3;
|
||||
background: fade(@accent-primary, 10%);
|
||||
border: 1px solid fade(@accent-primary, 20%);
|
||||
border-radius: @radius-md;
|
||||
}
|
||||
|
||||
.shared-config-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: @space-1;
|
||||
font-size: @font-size-sm;
|
||||
font-weight: @font-weight-medium;
|
||||
color: @accent-primary;
|
||||
margin-bottom: @space-2;
|
||||
}
|
||||
|
||||
.shared-config-details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: @space-3;
|
||||
font-size: @font-size-xs;
|
||||
color: @text-secondary;
|
||||
font-family: @font-family-mono;
|
||||
|
||||
span {
|
||||
background: @bg-card;
|
||||
padding: @space-1 @space-2;
|
||||
border-radius: @radius-sm;
|
||||
}
|
||||
}
|
||||
|
||||
// Small status badge variant
|
||||
.status-badge-sm {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border-radius: @radius-full;
|
||||
font-size: @font-size-xs;
|
||||
font-weight: @font-weight-medium;
|
||||
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
&.status-active {
|
||||
background: @success-bg;
|
||||
color: @success;
|
||||
}
|
||||
|
||||
&.status-stopped {
|
||||
background: fade(@text-tertiary, 10%);
|
||||
color: @text-tertiary;
|
||||
}
|
||||
|
||||
&.status-error {
|
||||
background: @danger-bg;
|
||||
color: @danger;
|
||||
}
|
||||
|
||||
&.status-warning {
|
||||
background: @warning-bg;
|
||||
color: @warning;
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@
|
||||
@import 'components/_deploy';
|
||||
@import 'components/_build';
|
||||
@import 'components/_users';
|
||||
@import 'components/_linked-apps';
|
||||
|
||||
// --------------------------------------------
|
||||
// 5. PAGE-SPECIFIC STYLES
|
||||
|
||||
Reference in new issue
Block a user