-
Notifications
You must be signed in to change notification settings - Fork 0
Add container controls and detail inspector #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a4c033d
6190010
08b32f5
fa5c6f7
26d25a9
e7bb071
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -328,7 +328,18 @@ nonisolated struct DockerContainerResource: Identifiable, Hashable, Codable, Sen | |
| var browserURL: URL? { | ||
| guard hostPort > 0 else { return nil } | ||
| let scheme = containerPort == 443 || hostPort == 443 ? "https" : "http" | ||
| return URL(string: "\(scheme)://localhost:\(hostPort)") | ||
| return URL(string: "\(scheme)://\(browserHost):\(hostPort)") | ||
| } | ||
|
|
||
| private var browserHost: String { | ||
| let normalized = hostIP.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| if normalized.isEmpty || normalized == "0.0.0.0" || normalized == "::" { | ||
| return "localhost" | ||
| } | ||
| if normalized.contains(":") && !normalized.hasPrefix("[") { | ||
| return "[\(normalized)]" | ||
| } | ||
| return normalized | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -374,12 +385,105 @@ nonisolated struct DockerContainerResource: Identifiable, Hashable, Codable, Sen | |
| } | ||
|
|
||
| var health: BackendResourceHealth { | ||
| let normalized = state.lowercased() | ||
| let normalized = normalizedState | ||
| let status = normalizedStatus | ||
| if status.contains("unhealthy") { return .error } | ||
| if status.contains("health: starting") { return .warning } | ||
| if normalized == "running" { return .healthy } | ||
| if normalized == "dead" { return .error } | ||
| if normalized == "exited" || normalized == "restarting" || normalized == "paused" { return .warning } | ||
| return .unknown | ||
| } | ||
|
|
||
| var displayName: String { | ||
| name.isEmpty ? id : name | ||
| } | ||
|
|
||
| var normalizedState: String { | ||
| state.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() | ||
| } | ||
|
|
||
| var normalizedStatus: String { | ||
| status.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() | ||
| } | ||
|
|
||
| var composeProject: String? { | ||
| labels["com.docker.compose.project"]?.nonEmpty | ||
| } | ||
|
|
||
| var composeService: String? { | ||
| labels["com.docker.compose.service"]?.nonEmpty | ||
| } | ||
|
|
||
| var availableActions: Set<DockerContainerAction> { | ||
| var actions: Set<DockerContainerAction> = [.logs, .inspect, .copyID, .copyImage] | ||
| if !ports.isEmpty { actions.insert(.copyPorts) } | ||
| if !portBindings.isEmpty { actions.insert(.openPort) } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because Useful? React with 👍 / 👎. |
||
|
|
||
| switch normalizedState { | ||
| case "running": | ||
| actions.formUnion([.stop, .restart, .pause, .kill, .terminal]) | ||
| case "paused": | ||
| actions.formUnion([.resume, .restart]) | ||
| case "dead": | ||
| actions.formUnion([.delete]) | ||
| case "restarting": | ||
| actions.formUnion([.stop, .kill, .restart]) | ||
| case "exited", "created", "stopped": | ||
| actions.formUnion([.start, .delete]) | ||
| default: | ||
| actions.formUnion([.start, .restart, .delete]) | ||
| } | ||
| return actions | ||
| } | ||
| } | ||
|
|
||
| nonisolated enum DockerContainerAction: String, CaseIterable, Hashable, Codable, Sendable { | ||
| case start | ||
| case stop | ||
| case restart | ||
| case pause | ||
| case resume | ||
| case kill | ||
| case delete | ||
| case logs | ||
| case inspect | ||
| case terminal | ||
| case openPort | ||
| case copyID | ||
| case copyImage | ||
| case copyPorts | ||
| } | ||
|
|
||
| nonisolated struct DockerComposeContainerGroup: Identifiable, Hashable, Codable, Sendable { | ||
| var projectName: String | ||
| var containers: [DockerContainerResource] | ||
|
|
||
| var id: String { projectName } | ||
|
|
||
| var runningCount: Int { | ||
| containers.filter { $0.normalizedState == "running" }.count | ||
| } | ||
|
|
||
| var services: [String] { | ||
| Array(Set(containers.compactMap(\.composeService))).sorted() | ||
| } | ||
|
|
||
| static func groups(from containers: [DockerContainerResource]) -> [DockerComposeContainerGroup] { | ||
| Dictionary(grouping: containers.compactMap { container -> (String, DockerContainerResource)? in | ||
| guard let project = container.composeProject else { return nil } | ||
| return (project, container) | ||
| }, by: { $0.0 }) | ||
| .map { project, values in | ||
| DockerComposeContainerGroup( | ||
| projectName: project, | ||
| containers: values.map(\.1).sorted { | ||
| $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending | ||
| } | ||
| ) | ||
| } | ||
| .sorted { $0.projectName.localizedCaseInsensitiveCompare($1.projectName) == .orderedAscending } | ||
| } | ||
| } | ||
|
|
||
| nonisolated struct DockerImageResource: Identifiable, Hashable, Codable, Sendable { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
docker inspectreturns more thanmaxLogCharacters(for example a container with very large env/label metadata), this replaces the JSON with a truncation banner plus a suffix beforeloadInspectstores it. That string is no longer valid JSON, socontainerMounts(from: inspectText)in the Files tab cannot parseMountsand misleadingly reports no mounts; keep a raw/parsed inspect payload for mount extraction and cap only the displayed text.Useful? React with 👍 / 👎.