Skip to content

Commit 8c3e2de

Browse files
feat: Add Container deployment status visibility (#968)
* feat: Add container deployment status visualization - Create ContainerStatus component with state-specific icons and colors - Add ContainerStatusList component with expandable container details - Integrate container status column in DeployedApplicationsTable Users can now view detailed container deployment status including running, exited, dead, and processing states with visual indicators and expandable details showing individual container images and their current states. Improves user experience with clear button descriptions Signed-off-by: Omar <omar.brbutovic@secomind.com>
1 parent 9735d22 commit 8c3e2de

File tree

4 files changed

+477
-1
lines changed

4 files changed

+477
-1
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
This file is part of Edgehog.
3+
4+
Copyright 2025 SECO Mind Srl
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
18+
SPDX-License-Identifier: Apache-2.0
19+
*/
20+
21+
import { defineMessages, FormattedMessage } from "react-intl";
22+
23+
import Icon from "components/Icon";
24+
25+
type ContainerState =
26+
| "CREATED"
27+
| "RUNNING"
28+
| "PAUSED"
29+
| "RESTARTING"
30+
| "REMOVING"
31+
| "EXITED"
32+
| "DEAD"
33+
| "UNKNOWN";
34+
35+
function parseContainerState(state?: string): ContainerState {
36+
switch (state?.toLowerCase()) {
37+
case "created":
38+
return "CREATED";
39+
case "running":
40+
return "RUNNING";
41+
case "paused":
42+
return "PAUSED";
43+
case "restarting":
44+
return "RESTARTING";
45+
case "removing":
46+
return "REMOVING";
47+
case "exited":
48+
case "stopped":
49+
return "EXITED";
50+
case "dead":
51+
return "DEAD";
52+
default:
53+
return "UNKNOWN";
54+
}
55+
}
56+
57+
const stateColors: Record<ContainerState, string> = {
58+
CREATED: "text-secondary",
59+
RUNNING: "text-success",
60+
PAUSED: "text-warning",
61+
RESTARTING: "text-warning",
62+
REMOVING: "text-warning",
63+
EXITED: "text-secondary",
64+
DEAD: "text-danger",
65+
UNKNOWN: "text-muted",
66+
};
67+
68+
const stateMessages = defineMessages<ContainerState>({
69+
CREATED: {
70+
id: "components.ContainerStatus.created",
71+
defaultMessage: "Created",
72+
},
73+
RUNNING: {
74+
id: "components.ContainerStatus.running",
75+
defaultMessage: "Running",
76+
},
77+
PAUSED: {
78+
id: "components.ContainerStatus.paused",
79+
defaultMessage: "Paused",
80+
},
81+
RESTARTING: {
82+
id: "components.ContainerStatus.restarting",
83+
defaultMessage: "Restarting",
84+
},
85+
REMOVING: {
86+
id: "components.ContainerStatus.removing",
87+
defaultMessage: "Removing",
88+
},
89+
EXITED: {
90+
id: "components.ContainerStatus.exited",
91+
defaultMessage: "Exited",
92+
},
93+
DEAD: {
94+
id: "components.ContainerStatus.dead",
95+
defaultMessage: "Dead",
96+
},
97+
UNKNOWN: {
98+
id: "components.ContainerStatus.unknown",
99+
defaultMessage: "Unknown",
100+
},
101+
});
102+
103+
const stateIcons: Record<
104+
ContainerState,
105+
React.ComponentProps<typeof Icon>["icon"]
106+
> = {
107+
CREATED: "circle",
108+
RUNNING: "circle",
109+
PAUSED: "circle",
110+
RESTARTING: "spinner",
111+
REMOVING: "spinner",
112+
EXITED: "circle",
113+
DEAD: "circle",
114+
UNKNOWN: "circle",
115+
};
116+
117+
type ContainerStatusProps = {
118+
state: ContainerState;
119+
};
120+
121+
const ContainerStatusComponent = ({ state }: ContainerStatusProps) => {
122+
return (
123+
<div className="d-flex align-items-center small">
124+
<Icon
125+
icon={stateIcons[state]}
126+
className={`me-2 ${stateColors[state]} ${
127+
["RESTARTING", "REMOVING"].includes(state) ? "fa-spin" : ""
128+
}`}
129+
/>
130+
<FormattedMessage id={stateMessages[state].id} />
131+
</div>
132+
);
133+
};
134+
135+
export type { ContainerState };
136+
export { parseContainerState };
137+
138+
export default ContainerStatusComponent;
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*
2+
This file is part of Edgehog.
3+
4+
Copyright 2025 SECO Mind Srl
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
18+
SPDX-License-Identifier: Apache-2.0
19+
*/
20+
21+
import { FormattedMessage, useIntl } from "react-intl";
22+
import { useState } from "react";
23+
import { Col, Collapse, Container, Row } from "react-bootstrap";
24+
25+
import ContainerStatus, {
26+
parseContainerState,
27+
} from "components/ContainerStatus";
28+
import Icon from "components/Icon";
29+
import Tag from "components/Tag";
30+
31+
interface ContainerDeployment {
32+
id: string;
33+
state: string | null;
34+
container: {
35+
image: {
36+
reference: string;
37+
};
38+
} | null;
39+
}
40+
41+
interface Props {
42+
containerDeployments: ContainerDeployment[];
43+
isExpanded?: boolean;
44+
onToggleExpanded?: () => void;
45+
}
46+
47+
const ContainerStatusList = ({
48+
containerDeployments,
49+
isExpanded: externalIsExpanded,
50+
onToggleExpanded,
51+
}: Props) => {
52+
const intl = useIntl();
53+
54+
const [internalIsExpanded, setInternalIsExpanded] = useState(false);
55+
56+
const isExpanded =
57+
externalIsExpanded !== undefined ? externalIsExpanded : internalIsExpanded;
58+
59+
const toggleExpanded =
60+
onToggleExpanded || (() => setInternalIsExpanded(!internalIsExpanded));
61+
62+
if (!containerDeployments || containerDeployments.length === 0) {
63+
return (
64+
<div className="text-muted fst-italic">
65+
<FormattedMessage
66+
id="components.ContainerStatusList.noContainers"
67+
defaultMessage="No containers"
68+
/>
69+
</div>
70+
);
71+
}
72+
73+
const statusCounts = containerDeployments.reduce(
74+
(acc, containerDeployment) => {
75+
const state = parseContainerState(containerDeployment.state || undefined);
76+
acc[state] = (acc[state] || 0) + 1;
77+
return acc;
78+
},
79+
{} as Record<string, number>,
80+
);
81+
82+
const totalContainers = containerDeployments.length;
83+
const runningCount = statusCounts.RUNNING || 0;
84+
const exitedCount = statusCounts.EXITED || 0;
85+
const deadCount = statusCounts.DEAD || 0;
86+
const restartingCount = statusCounts.RESTARTING || 0;
87+
const removingCount = statusCounts.REMOVING || 0;
88+
89+
return (
90+
<div>
91+
<div
92+
className="d-flex align-items-center cursor-pointer user-select-none"
93+
onClick={toggleExpanded}
94+
style={{ cursor: "pointer" }}
95+
role="button"
96+
aria-expanded={isExpanded}
97+
title={
98+
isExpanded
99+
? intl.formatMessage({
100+
id: "components.ContainerStatusList.collapseContainerList",
101+
defaultMessage: "Collapse container list",
102+
})
103+
: intl.formatMessage({
104+
id: "components.ContainerStatusList.expandContainerList",
105+
defaultMessage: "Expand container list",
106+
})
107+
}
108+
>
109+
<Tag className="bg-secondary me-1 small">{totalContainers}</Tag>
110+
111+
{runningCount > 0 && (
112+
<Tag className="bg-success me-1 small">
113+
<FormattedMessage
114+
id="components.ContainerStatusList.runningCount"
115+
defaultMessage="{count} running"
116+
values={{ count: runningCount }}
117+
/>
118+
</Tag>
119+
)}
120+
121+
{exitedCount > 0 && (
122+
<Tag className="bg-secondary me-1 small">
123+
<FormattedMessage
124+
id="components.ContainerStatusList.exitedCount"
125+
defaultMessage="{count} exited"
126+
values={{ count: exitedCount }}
127+
/>
128+
</Tag>
129+
)}
130+
131+
{deadCount > 0 && (
132+
<Tag className="bg-danger me-1 small">
133+
<FormattedMessage
134+
id="components.ContainerStatusList.deadCount"
135+
defaultMessage="{count} dead"
136+
values={{ count: deadCount }}
137+
/>
138+
</Tag>
139+
)}
140+
141+
{(restartingCount > 0 || removingCount > 0) && (
142+
<Tag className="bg-warning me-1 small">
143+
<FormattedMessage
144+
id="components.ContainerStatusList.processingCount"
145+
defaultMessage="{count} processing"
146+
values={{ count: restartingCount + removingCount }}
147+
/>
148+
</Tag>
149+
)}
150+
151+
<Icon
152+
icon={isExpanded ? "caretUp" : "caretDown"}
153+
className="text-secondary ms-2"
154+
size="sm"
155+
/>
156+
</div>
157+
158+
<Collapse in={isExpanded}>
159+
<div className="mt-2">
160+
{containerDeployments.map((containerDeployment) => (
161+
<Container key={containerDeployment.id}>
162+
<Row className="justify-content-between align-items-center">
163+
<Col>
164+
<small className="text-muted">
165+
<FormattedMessage
166+
id="components.ContainerStatusList.containerReference"
167+
defaultMessage="{reference}"
168+
values={{
169+
reference:
170+
containerDeployment.container?.image?.reference ||
171+
"Unknown",
172+
}}
173+
/>
174+
</small>
175+
</Col>
176+
177+
<Col>
178+
<ContainerStatus
179+
state={parseContainerState(
180+
containerDeployment.state || undefined,
181+
)}
182+
/>
183+
</Col>
184+
</Row>
185+
</Container>
186+
))}
187+
</div>
188+
</Collapse>
189+
</div>
190+
);
191+
};
192+
193+
export default ContainerStatusList;

0 commit comments

Comments
 (0)