diff --git a/frontend/src/components/LinkedAppsSection.jsx b/frontend/src/components/LinkedAppsSection.jsx new file mode 100644 index 00000000..77f47272 --- /dev/null +++ b/frontend/src/components/LinkedAppsSection.jsx @@ -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 ( +
+
+

+ + Environment Linking +

+ {!app.has_linked_app && ( + + )} +
+ +
+ {/* Current app environment */} +
+ This app is: + + {envLabels[app.environment_type] || 'Standalone'} + +
+ + {/* Linked apps list */} + {linkedApps && linkedApps.length > 0 ? ( +
+
+ Linked to +
+ {linkedApps.map(linkedApp => ( +
+
+
+ +
+
+ {linkedApp.name} +
+ + {envLabels[linkedApp.environment_type] || linkedApp.environment_type} + + + + {linkedApp.status} + + {linkedApp.port && ( + :{linkedApp.port} + )} +
+
+
+
+ + +
+
+ ))} +
+ ) : app.environment_type !== 'standalone' ? ( +
+

No linked apps. Link another app to share database resources.

+ +
+ ) : ( +
+

+ Link this app to another to create a production/development pair. + Linked apps can share database credentials with different table prefixes. +

+
+ )} + + {/* Shared config info */} + {app.shared_config && app.shared_config.db_credentials_propagated && ( +
+
+ + Shared Database +
+ {app.shared_config.shared_db && ( +
+ Host: {app.shared_config.shared_db.db_host} + Table prefix: {app.shared_config.shared_db.target_prefix || app.shared_config.shared_db.source_prefix} +
+ )} +
+ )} +
+
+ ); +}; + +export default LinkedAppsSection; diff --git a/frontend/src/styles/components/_linked-apps.less b/frontend/src/styles/components/_linked-apps.less new file mode 100644 index 00000000..2cdb82be --- /dev/null +++ b/frontend/src/styles/components/_linked-apps.less @@ -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; + } +} diff --git a/frontend/src/styles/main.less b/frontend/src/styles/main.less index e3f19435..12bd9c7c 100644 --- a/frontend/src/styles/main.less +++ b/frontend/src/styles/main.less @@ -49,6 +49,7 @@ @import 'components/_deploy'; @import 'components/_build'; @import 'components/_users'; +@import 'components/_linked-apps'; // -------------------------------------------- // 5. PAGE-SPECIFIC STYLES