osism 0.20250823.0__tar.gz → 0.20250824.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- osism-0.20250824.0/ChangeLog +7 -0
- {osism-0.20250823.0/osism.egg-info → osism-0.20250824.0}/PKG-INFO +2 -1
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/Containerfile +3 -3
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/Dockerfile +3 -3
- osism-0.20250824.0/frontend/app/api/config/route.ts +7 -0
- osism-0.20250824.0/frontend/app/api/health/route.ts +9 -0
- osism-0.20250824.0/frontend/app/components/ConnectionStatus.tsx +51 -0
- osism-0.20250824.0/frontend/app/components/EventsFilters.tsx +150 -0
- osism-0.20250824.0/frontend/app/components/EventsList.tsx +189 -0
- osism-0.20250824.0/frontend/app/events/page.tsx +197 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/app/globals.css +1 -3
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/app/layout.tsx +5 -4
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/app/nodes/page.tsx +32 -10
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/app/page.tsx +14 -50
- osism-0.20250824.0/frontend/lib/api.ts +52 -0
- osism-0.20250824.0/frontend/lib/hooks/useWebSocket.ts +209 -0
- osism-0.20250824.0/frontend/lib/types.ts +72 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/package-lock.json +747 -1014
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/package.json +8 -6
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/postcss.config.mjs +1 -1
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/tailwind.config.ts +0 -1
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/api.py +101 -3
- osism-0.20250824.0/osism/services/event_bridge.py +304 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/services/listener.py +130 -19
- osism-0.20250824.0/osism/services/websocket_manager.py +271 -0
- {osism-0.20250823.0 → osism-0.20250824.0/osism.egg-info}/PKG-INFO +2 -1
- {osism-0.20250823.0 → osism-0.20250824.0}/osism.egg-info/SOURCES.txt +9 -0
- osism-0.20250824.0/osism.egg-info/pbr.json +1 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism.egg-info/requires.txt +1 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/requirements.txt +1 -0
- osism-0.20250823.0/ChangeLog +0 -7
- osism-0.20250823.0/frontend/lib/api.ts +0 -29
- osism-0.20250823.0/frontend/lib/types.ts +0 -29
- osism-0.20250823.0/osism.egg-info/pbr.json +0 -1
- {osism-0.20250823.0 → osism-0.20250824.0}/.flake8 +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/.github/renovate.json +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/.github/workflows/publish.yml +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/.hadolint.yaml +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/.zuul.yaml +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/AUTHORS +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/Containerfile +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/Dockerfile +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/LICENSE +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/Pipfile +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/Pipfile.lock +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/README.md +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/files/change.sh +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/files/cleanup-ansible-collections.sh +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/files/clustershell/clush.conf +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/files/clustershell/groups.conf +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/files/data/SCS-Spec.MandatoryFlavors.verbose.yaml +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/files/netbox-manager/settings.toml +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/files/redfishMockupCreate.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/files/run-ansible-console.sh +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/files/sonic/config_db.json +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/files/sonic/port_config/Accton-AS4625-54T.ini +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/files/sonic/port_config/Accton-AS5835-54T.ini +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/files/sonic/port_config/Accton-AS5835-54X.ini +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/files/sonic/port_config/Accton-AS7326-56X.ini +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/files/sonic/port_config/Accton-AS7726-32X.ini +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/files/sonic/port_config/Accton-AS9716-32D.ini +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/.dockerignore +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/.gitignore +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/app/favicon.ico +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/app/services/page.tsx +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/components.json +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/eslint.config.mjs +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/lib/utils.ts +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/next.config.ts +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/public/file.svg +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/public/globe.svg +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/public/next.svg +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/public/vercel.svg +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/public/window.svg +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/frontend/tsconfig.json +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/__init__.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/__main__.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/__init__.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/apply.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/baremetal.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/compose.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/compute.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/configuration.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/console.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/container.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/get.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/log.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/manage.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/netbox.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/noset.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/reconciler.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/redfish.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/server.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/service.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/set.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/sonic.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/status.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/sync.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/task.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/validate.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/vault.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/volume.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/wait.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/worker.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/data/__init__.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/data/enums.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/data/playbooks.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/main.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/services/__init__.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/settings.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/__init__.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/ansible.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/ceph.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/__init__.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/config.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/ironic.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/netbox.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/redfish.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/__init__.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/bgp.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/cache.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/config_generator.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/connections.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/constants.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/device.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/exporter.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/interface.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/sync.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/utils.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/kolla.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/kubernetes.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/netbox.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/openstack.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/reconciler.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/utils/__init__.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism/utils/ssh.py +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism.egg-info/dependency_links.txt +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism.egg-info/entry_points.txt +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism.egg-info/not-zip-safe +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/osism.egg-info/top_level.txt +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/playbooks/build.yml +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/playbooks/pre.yml +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/playbooks/test-setup.yml +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/requirements.ansible.txt +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/requirements.netbox-manager.txt +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/requirements.openstack-flavor-manager.txt +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/requirements.openstack-image-manager.txt +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/requirements.yml +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/setup.cfg +0 -0
- {osism-0.20250823.0 → osism-0.20250824.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: osism
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.20250824.0
|
4
4
|
Summary: OSISM manager interface
|
5
5
|
Home-page: https://github.com/osism/python-osism
|
6
6
|
Author: OSISM GmbH
|
@@ -55,6 +55,7 @@ Requires-Dist: transitions==0.9.3
|
|
55
55
|
Requires-Dist: uvicorn[standard]==0.35.0
|
56
56
|
Requires-Dist: validators==0.35.0
|
57
57
|
Requires-Dist: watchdog==6.0.0
|
58
|
+
Requires-Dist: websockets==15.0.1
|
58
59
|
Provides-Extra: ansible
|
59
60
|
Requires-Dist: ansible-runner==2.4.1; extra == "ansible"
|
60
61
|
Requires-Dist: ansible-core==2.19.0; extra == "ansible"
|
@@ -1,10 +1,10 @@
|
|
1
1
|
# Multi-stage build for production-ready Next.js application
|
2
|
-
FROM node:
|
2
|
+
FROM node:22-alpine AS dependencies
|
3
3
|
WORKDIR /app
|
4
4
|
COPY package.json package-lock.json ./
|
5
5
|
RUN npm ci --only=production
|
6
6
|
|
7
|
-
FROM node:
|
7
|
+
FROM node:22-alpine AS build
|
8
8
|
WORKDIR /app
|
9
9
|
COPY package.json package-lock.json ./
|
10
10
|
RUN npm ci
|
@@ -12,7 +12,7 @@ COPY . .
|
|
12
12
|
ENV NEXT_TELEMETRY_DISABLED=1
|
13
13
|
RUN npm run build
|
14
14
|
|
15
|
-
FROM node:
|
15
|
+
FROM node:22-alpine AS runner
|
16
16
|
WORKDIR /app
|
17
17
|
|
18
18
|
ENV NODE_ENV=production
|
@@ -1,10 +1,10 @@
|
|
1
1
|
# Multi-stage build for production-ready Next.js application
|
2
|
-
FROM node:
|
2
|
+
FROM node:22-alpine AS dependencies
|
3
3
|
WORKDIR /app
|
4
4
|
COPY package.json package-lock.json ./
|
5
5
|
RUN npm ci --only=production
|
6
6
|
|
7
|
-
FROM node:
|
7
|
+
FROM node:22-alpine AS build
|
8
8
|
WORKDIR /app
|
9
9
|
COPY package.json package-lock.json ./
|
10
10
|
RUN npm ci
|
@@ -12,7 +12,7 @@ COPY . .
|
|
12
12
|
ENV NEXT_TELEMETRY_DISABLED=1
|
13
13
|
RUN npm run build
|
14
14
|
|
15
|
-
FROM node:
|
15
|
+
FROM node:22-alpine AS runner
|
16
16
|
WORKDIR /app
|
17
17
|
|
18
18
|
ENV NODE_ENV=production
|
@@ -0,0 +1,51 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import { Wifi, WifiOff, AlertCircle } from "lucide-react";
|
4
|
+
import { ConnectionStatus as ConnectionStatusType } from "@/lib/types";
|
5
|
+
import { formatDistanceToNow } from "date-fns";
|
6
|
+
|
7
|
+
interface ConnectionStatusProps {
|
8
|
+
status: ConnectionStatusType;
|
9
|
+
className?: string;
|
10
|
+
}
|
11
|
+
|
12
|
+
export default function ConnectionStatus({ status, className = "" }: ConnectionStatusProps) {
|
13
|
+
const getStatusContent = () => {
|
14
|
+
if (status.connected) {
|
15
|
+
return {
|
16
|
+
icon: Wifi,
|
17
|
+
text: "Connected",
|
18
|
+
color: "text-green-600",
|
19
|
+
bgColor: "bg-green-100",
|
20
|
+
};
|
21
|
+
} else if (status.error) {
|
22
|
+
return {
|
23
|
+
icon: AlertCircle,
|
24
|
+
text: `Disconnected: ${status.error}`,
|
25
|
+
color: "text-red-600",
|
26
|
+
bgColor: "bg-red-100",
|
27
|
+
};
|
28
|
+
} else {
|
29
|
+
return {
|
30
|
+
icon: WifiOff,
|
31
|
+
text: "Disconnected",
|
32
|
+
color: "text-gray-600",
|
33
|
+
bgColor: "bg-gray-100",
|
34
|
+
};
|
35
|
+
}
|
36
|
+
};
|
37
|
+
|
38
|
+
const { icon: Icon, text, color, bgColor } = getStatusContent();
|
39
|
+
|
40
|
+
return (
|
41
|
+
<div className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${bgColor} ${color} ${className}`}>
|
42
|
+
<Icon className="h-4 w-4 mr-2" />
|
43
|
+
<span>{text}</span>
|
44
|
+
{status.lastConnected && !status.connected && (
|
45
|
+
<span className="ml-2 text-xs opacity-75">
|
46
|
+
(Last: {formatDistanceToNow(status.lastConnected)} ago)
|
47
|
+
</span>
|
48
|
+
)}
|
49
|
+
</div>
|
50
|
+
);
|
51
|
+
}
|
@@ -0,0 +1,150 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import { useState, useCallback } from "react";
|
4
|
+
import { Filter, X } from "lucide-react";
|
5
|
+
import { WebSocketFilter } from "@/lib/types";
|
6
|
+
|
7
|
+
interface EventsFiltersProps {
|
8
|
+
onFiltersChange: (filters: WebSocketFilter) => void;
|
9
|
+
className?: string;
|
10
|
+
}
|
11
|
+
|
12
|
+
const BAREMETAL_EVENT_TYPES = [
|
13
|
+
"baremetal.node.power_set.end",
|
14
|
+
"baremetal.node.provision_set.start",
|
15
|
+
"baremetal.node.provision_set.end",
|
16
|
+
"baremetal.node.provision_set.success",
|
17
|
+
"baremetal.node.power_state_corrected.success",
|
18
|
+
"baremetal.node.maintenance_set.end",
|
19
|
+
"baremetal.node.create.end",
|
20
|
+
"baremetal.node.delete.end",
|
21
|
+
];
|
22
|
+
|
23
|
+
export default function EventsFilters({ onFiltersChange, className = "" }: EventsFiltersProps) {
|
24
|
+
const [isOpen, setIsOpen] = useState(false);
|
25
|
+
const [selectedEventTypes, setSelectedEventTypes] = useState<string[]>([]);
|
26
|
+
const [nodeFilter, setNodeFilter] = useState("");
|
27
|
+
|
28
|
+
const applyFilters = useCallback(() => {
|
29
|
+
const filters: WebSocketFilter = {
|
30
|
+
service_filters: ["baremetal"], // Always filter for baremetal events
|
31
|
+
};
|
32
|
+
|
33
|
+
if (selectedEventTypes.length > 0) {
|
34
|
+
filters.event_filters = selectedEventTypes;
|
35
|
+
}
|
36
|
+
|
37
|
+
if (nodeFilter.trim()) {
|
38
|
+
filters.node_filters = nodeFilter.split(",").map(n => n.trim()).filter(n => n);
|
39
|
+
}
|
40
|
+
|
41
|
+
onFiltersChange(filters);
|
42
|
+
setIsOpen(false);
|
43
|
+
}, [selectedEventTypes, nodeFilter, onFiltersChange]);
|
44
|
+
|
45
|
+
const clearFilters = useCallback(() => {
|
46
|
+
setSelectedEventTypes([]);
|
47
|
+
setNodeFilter("");
|
48
|
+
onFiltersChange({ service_filters: ["baremetal"] });
|
49
|
+
}, [onFiltersChange]);
|
50
|
+
|
51
|
+
const toggleEventType = useCallback((eventType: string) => {
|
52
|
+
setSelectedEventTypes(prev =>
|
53
|
+
prev.includes(eventType)
|
54
|
+
? prev.filter(t => t !== eventType)
|
55
|
+
: [...prev, eventType]
|
56
|
+
);
|
57
|
+
}, []);
|
58
|
+
|
59
|
+
const hasActiveFilters = selectedEventTypes.length > 0 || nodeFilter.trim().length > 0;
|
60
|
+
|
61
|
+
return (
|
62
|
+
<div className={`relative ${className}`}>
|
63
|
+
<button
|
64
|
+
onClick={() => setIsOpen(!isOpen)}
|
65
|
+
className={`inline-flex items-center px-4 py-2 border rounded-md text-sm font-medium transition-colors ${
|
66
|
+
hasActiveFilters
|
67
|
+
? "border-blue-500 bg-blue-50 text-blue-700"
|
68
|
+
: "border-gray-300 bg-white text-gray-700 hover:bg-gray-50"
|
69
|
+
}`}
|
70
|
+
>
|
71
|
+
<Filter className="h-4 w-4 mr-2" />
|
72
|
+
Filters
|
73
|
+
{hasActiveFilters && (
|
74
|
+
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
75
|
+
{(selectedEventTypes.length > 0 ? 1 : 0) + (nodeFilter ? 1 : 0)}
|
76
|
+
</span>
|
77
|
+
)}
|
78
|
+
</button>
|
79
|
+
|
80
|
+
{isOpen && (
|
81
|
+
<div className="absolute top-full left-0 mt-2 w-96 bg-white border border-gray-200 rounded-lg shadow-lg z-10">
|
82
|
+
<div className="p-4 border-b border-gray-200">
|
83
|
+
<div className="flex items-center justify-between">
|
84
|
+
<h3 className="text-sm font-medium text-gray-900">Event Filters</h3>
|
85
|
+
<button
|
86
|
+
onClick={() => setIsOpen(false)}
|
87
|
+
className="text-gray-400 hover:text-gray-600"
|
88
|
+
>
|
89
|
+
<X className="h-4 w-4" />
|
90
|
+
</button>
|
91
|
+
</div>
|
92
|
+
</div>
|
93
|
+
|
94
|
+
<div className="p-4 space-y-4">
|
95
|
+
{/* Node Filter */}
|
96
|
+
<div>
|
97
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
98
|
+
Node Names (comma-separated)
|
99
|
+
</label>
|
100
|
+
<input
|
101
|
+
type="text"
|
102
|
+
value={nodeFilter}
|
103
|
+
onChange={(e) => setNodeFilter(e.target.value)}
|
104
|
+
placeholder="e.g., server-01, server-02"
|
105
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
106
|
+
/>
|
107
|
+
</div>
|
108
|
+
|
109
|
+
{/* Event Type Filter */}
|
110
|
+
<div>
|
111
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
112
|
+
Event Types
|
113
|
+
</label>
|
114
|
+
<div className="space-y-2 max-h-48 overflow-y-auto">
|
115
|
+
{BAREMETAL_EVENT_TYPES.map((eventType) => (
|
116
|
+
<label key={eventType} className="flex items-center">
|
117
|
+
<input
|
118
|
+
type="checkbox"
|
119
|
+
checked={selectedEventTypes.includes(eventType)}
|
120
|
+
onChange={() => toggleEventType(eventType)}
|
121
|
+
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
122
|
+
/>
|
123
|
+
<span className="ml-2 text-sm text-gray-700 font-mono">
|
124
|
+
{eventType}
|
125
|
+
</span>
|
126
|
+
</label>
|
127
|
+
))}
|
128
|
+
</div>
|
129
|
+
</div>
|
130
|
+
</div>
|
131
|
+
|
132
|
+
<div className="p-4 border-t border-gray-200 flex justify-between">
|
133
|
+
<button
|
134
|
+
onClick={clearFilters}
|
135
|
+
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
136
|
+
>
|
137
|
+
Clear All
|
138
|
+
</button>
|
139
|
+
<button
|
140
|
+
onClick={applyFilters}
|
141
|
+
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
|
142
|
+
>
|
143
|
+
Apply Filters
|
144
|
+
</button>
|
145
|
+
</div>
|
146
|
+
</div>
|
147
|
+
)}
|
148
|
+
</div>
|
149
|
+
);
|
150
|
+
}
|
@@ -0,0 +1,189 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import { useMemo, useState } from "react";
|
4
|
+
import { format } from "date-fns";
|
5
|
+
import {
|
6
|
+
Server,
|
7
|
+
Power,
|
8
|
+
Settings,
|
9
|
+
Trash2,
|
10
|
+
Plus,
|
11
|
+
CheckCircle,
|
12
|
+
XCircle,
|
13
|
+
AlertTriangle,
|
14
|
+
Clock,
|
15
|
+
Pause,
|
16
|
+
Play
|
17
|
+
} from "lucide-react";
|
18
|
+
import { OpenStackEvent, BaremetalEvent } from "@/lib/types";
|
19
|
+
|
20
|
+
interface EventsListProps {
|
21
|
+
events: OpenStackEvent[];
|
22
|
+
className?: string;
|
23
|
+
}
|
24
|
+
|
25
|
+
const getEventIcon = (eventType: string) => {
|
26
|
+
if (eventType.includes("power")) return Power;
|
27
|
+
if (eventType.includes("provision")) return Settings;
|
28
|
+
if (eventType.includes("maintenance")) return AlertTriangle;
|
29
|
+
if (eventType.includes("create")) return Plus;
|
30
|
+
if (eventType.includes("delete")) return Trash2;
|
31
|
+
return Server;
|
32
|
+
};
|
33
|
+
|
34
|
+
const getEventColor = (eventType: string) => {
|
35
|
+
if (eventType.includes("power_set.end")) return "text-blue-600 bg-blue-50";
|
36
|
+
if (eventType.includes("provision_set.success")) return "text-green-600 bg-green-50";
|
37
|
+
if (eventType.includes("provision_set.start")) return "text-yellow-600 bg-yellow-50";
|
38
|
+
if (eventType.includes("maintenance")) return "text-orange-600 bg-orange-50";
|
39
|
+
if (eventType.includes("create")) return "text-green-600 bg-green-50";
|
40
|
+
if (eventType.includes("delete")) return "text-red-600 bg-red-50";
|
41
|
+
return "text-gray-600 bg-gray-50";
|
42
|
+
};
|
43
|
+
|
44
|
+
const getStatusIcon = (eventType: string) => {
|
45
|
+
if (eventType.includes("success")) return CheckCircle;
|
46
|
+
if (eventType.includes("error") || eventType.includes("fail")) return XCircle;
|
47
|
+
if (eventType.includes("start")) return Clock;
|
48
|
+
return null;
|
49
|
+
};
|
50
|
+
|
51
|
+
const formatEventData = (event: OpenStackEvent) => {
|
52
|
+
if (event.data.service_type === "baremetal" && "ironic_object" in event.data) {
|
53
|
+
const baremetalEvent = event as BaremetalEvent;
|
54
|
+
const ironicData = baremetalEvent.data.ironic_object?.data;
|
55
|
+
|
56
|
+
if (!ironicData) return null;
|
57
|
+
|
58
|
+
const details = [];
|
59
|
+
if (ironicData.power_state) details.push(`Power: ${ironicData.power_state}`);
|
60
|
+
if (ironicData.provision_state) details.push(`Provision: ${ironicData.provision_state}`);
|
61
|
+
if (ironicData.maintenance !== undefined) details.push(`Maintenance: ${ironicData.maintenance ? "Yes" : "No"}`);
|
62
|
+
|
63
|
+
return details.join(" | ");
|
64
|
+
}
|
65
|
+
|
66
|
+
return null;
|
67
|
+
};
|
68
|
+
|
69
|
+
export default function EventsList({ events, className = "" }: EventsListProps) {
|
70
|
+
const [isPaused, setIsPaused] = useState(false);
|
71
|
+
const [maxEvents, setMaxEvents] = useState(50);
|
72
|
+
|
73
|
+
const displayEvents = useMemo(() => {
|
74
|
+
return isPaused ? events : events.slice(0, maxEvents);
|
75
|
+
}, [events, isPaused, maxEvents]);
|
76
|
+
|
77
|
+
const togglePause = () => {
|
78
|
+
setIsPaused(!isPaused);
|
79
|
+
};
|
80
|
+
|
81
|
+
const loadMore = () => {
|
82
|
+
setMaxEvents(prev => prev + 50);
|
83
|
+
};
|
84
|
+
|
85
|
+
if (events.length === 0) {
|
86
|
+
return (
|
87
|
+
<div className={`text-center py-12 ${className}`}>
|
88
|
+
<Server className="mx-auto h-12 w-12 text-gray-400" />
|
89
|
+
<h3 className="mt-2 text-sm font-medium text-gray-900">No events yet</h3>
|
90
|
+
<p className="mt-1 text-sm text-gray-500">
|
91
|
+
Baremetal events will appear here in real-time.
|
92
|
+
</p>
|
93
|
+
</div>
|
94
|
+
);
|
95
|
+
}
|
96
|
+
|
97
|
+
return (
|
98
|
+
<div className={className}>
|
99
|
+
{/* Controls */}
|
100
|
+
<div className="flex items-center justify-between mb-4 text-sm text-gray-600">
|
101
|
+
<span>{events.length} events total</span>
|
102
|
+
<div className="flex items-center space-x-2">
|
103
|
+
<button
|
104
|
+
onClick={togglePause}
|
105
|
+
className="inline-flex items-center px-3 py-1 border border-gray-300 rounded-md hover:bg-gray-50"
|
106
|
+
>
|
107
|
+
{isPaused ? (
|
108
|
+
<>
|
109
|
+
<Play className="h-4 w-4 mr-1" />
|
110
|
+
Resume
|
111
|
+
</>
|
112
|
+
) : (
|
113
|
+
<>
|
114
|
+
<Pause className="h-4 w-4 mr-1" />
|
115
|
+
Pause
|
116
|
+
</>
|
117
|
+
)}
|
118
|
+
</button>
|
119
|
+
</div>
|
120
|
+
</div>
|
121
|
+
|
122
|
+
{/* Events List */}
|
123
|
+
<div className="space-y-1">
|
124
|
+
{displayEvents.map((event) => {
|
125
|
+
const Icon = getEventIcon(event.event_type);
|
126
|
+
const StatusIcon = getStatusIcon(event.event_type);
|
127
|
+
const colorClass = getEventColor(event.event_type);
|
128
|
+
const eventDetails = formatEventData(event);
|
129
|
+
|
130
|
+
return (
|
131
|
+
<div
|
132
|
+
key={event.id}
|
133
|
+
className="flex items-start space-x-3 p-3 bg-white border border-gray-200 rounded-lg hover:border-gray-300 transition-colors"
|
134
|
+
>
|
135
|
+
{/* Icon */}
|
136
|
+
<div className={`flex-shrink-0 p-2 rounded-lg ${colorClass}`}>
|
137
|
+
<Icon className="h-4 w-4" />
|
138
|
+
</div>
|
139
|
+
|
140
|
+
{/* Content */}
|
141
|
+
<div className="flex-1 min-w-0">
|
142
|
+
<div className="flex items-center justify-between">
|
143
|
+
<div className="flex items-center space-x-2">
|
144
|
+
<span className="text-sm font-medium text-gray-900 font-mono">
|
145
|
+
{event.event_type}
|
146
|
+
</span>
|
147
|
+
{StatusIcon && (
|
148
|
+
<StatusIcon className="h-4 w-4 text-gray-400" />
|
149
|
+
)}
|
150
|
+
</div>
|
151
|
+
<time className="text-xs text-gray-500 flex-shrink-0">
|
152
|
+
{format(new Date(event.timestamp), "HH:mm:ss")}
|
153
|
+
</time>
|
154
|
+
</div>
|
155
|
+
|
156
|
+
{event.node_name && (
|
157
|
+
<div className="flex items-center mt-1">
|
158
|
+
<Server className="h-3 w-3 text-gray-400 mr-1" />
|
159
|
+
<span className="text-sm font-medium text-gray-700">
|
160
|
+
{event.node_name}
|
161
|
+
</span>
|
162
|
+
</div>
|
163
|
+
)}
|
164
|
+
|
165
|
+
{eventDetails && (
|
166
|
+
<p className="text-sm text-gray-600 mt-1">
|
167
|
+
{eventDetails}
|
168
|
+
</p>
|
169
|
+
)}
|
170
|
+
</div>
|
171
|
+
</div>
|
172
|
+
);
|
173
|
+
})}
|
174
|
+
</div>
|
175
|
+
|
176
|
+
{/* Load More */}
|
177
|
+
{!isPaused && events.length > maxEvents && (
|
178
|
+
<div className="mt-4 text-center">
|
179
|
+
<button
|
180
|
+
onClick={loadMore}
|
181
|
+
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
182
|
+
>
|
183
|
+
Load More Events ({events.length - maxEvents} remaining)
|
184
|
+
</button>
|
185
|
+
</div>
|
186
|
+
)}
|
187
|
+
</div>
|
188
|
+
);
|
189
|
+
}
|
@@ -0,0 +1,197 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
4
|
+
import { Activity, RefreshCw, Trash2 } from "lucide-react";
|
5
|
+
import useWebSocket from "@/lib/hooks/useWebSocket";
|
6
|
+
import EventsList from "../components/EventsList";
|
7
|
+
import EventsFilters from "../components/EventsFilters";
|
8
|
+
import ConnectionStatus from "../components/ConnectionStatus";
|
9
|
+
import { WebSocketFilter } from "@/lib/types";
|
10
|
+
|
11
|
+
async function getWebSocketUrl(): Promise<string> {
|
12
|
+
if (typeof window !== 'undefined') {
|
13
|
+
try {
|
14
|
+
const response = await fetch('/api/config');
|
15
|
+
if (!response.ok) {
|
16
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
17
|
+
}
|
18
|
+
const config = await response.json();
|
19
|
+
// Convert HTTP URL to WebSocket URL
|
20
|
+
const apiUrl = config.apiUrl.replace(/^http/, 'ws');
|
21
|
+
const wsUrl = `${apiUrl}/v1/events/openstack`;
|
22
|
+
console.log('Using WebSocket URL from config:', wsUrl);
|
23
|
+
return wsUrl;
|
24
|
+
} catch (error) {
|
25
|
+
console.warn('Failed to fetch API config for WebSocket:', error);
|
26
|
+
const fallbackUrl = 'ws://localhost:8000/v1/events/openstack';
|
27
|
+
console.log('Using fallback WebSocket URL:', fallbackUrl);
|
28
|
+
return fallbackUrl;
|
29
|
+
}
|
30
|
+
}
|
31
|
+
// Server-side fallback
|
32
|
+
const apiUrl = (process.env.NEXT_PUBLIC_OSISM_API_URL || 'http://api:8000').replace(/^http/, 'ws');
|
33
|
+
const wsUrl = `${apiUrl}/v1/events/openstack`;
|
34
|
+
console.log('Using server-side WebSocket URL:', wsUrl);
|
35
|
+
return wsUrl;
|
36
|
+
}
|
37
|
+
|
38
|
+
export default function EventsPage() {
|
39
|
+
const [wsUrl, setWsUrl] = useState<string>('');
|
40
|
+
const [eventCount, setEventCount] = useState(0);
|
41
|
+
|
42
|
+
// Initialize WebSocket URL
|
43
|
+
useEffect(() => {
|
44
|
+
getWebSocketUrl().then((url) => {
|
45
|
+
console.log('WebSocket URL resolved:', url);
|
46
|
+
setWsUrl(url);
|
47
|
+
});
|
48
|
+
}, []);
|
49
|
+
|
50
|
+
const {
|
51
|
+
events,
|
52
|
+
connectionStatus,
|
53
|
+
connect,
|
54
|
+
disconnect,
|
55
|
+
clearEvents,
|
56
|
+
setFilters
|
57
|
+
} = useWebSocket(wsUrl, {
|
58
|
+
autoConnect: false, // Don't auto-connect until URL is ready
|
59
|
+
onEvent: useCallback(() => {
|
60
|
+
setEventCount(prev => prev + 1);
|
61
|
+
}, [])
|
62
|
+
});
|
63
|
+
|
64
|
+
// Connect only after wsUrl is available
|
65
|
+
useEffect(() => {
|
66
|
+
if (wsUrl) {
|
67
|
+
connect();
|
68
|
+
}
|
69
|
+
}, [wsUrl, connect]);
|
70
|
+
|
71
|
+
const handleFiltersChange = useCallback((filters: WebSocketFilter) => {
|
72
|
+
setFilters(filters);
|
73
|
+
}, [setFilters]);
|
74
|
+
|
75
|
+
const handleReconnect = () => {
|
76
|
+
disconnect();
|
77
|
+
setTimeout(() => {
|
78
|
+
connect();
|
79
|
+
}, 100);
|
80
|
+
};
|
81
|
+
|
82
|
+
const handleClearEvents = () => {
|
83
|
+
clearEvents();
|
84
|
+
setEventCount(0);
|
85
|
+
};
|
86
|
+
|
87
|
+
if (!wsUrl) {
|
88
|
+
return (
|
89
|
+
<div className="px-4 sm:px-0">
|
90
|
+
<div className="animate-pulse">
|
91
|
+
<div className="h-8 bg-gray-200 rounded w-1/4 mb-4"></div>
|
92
|
+
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
93
|
+
</div>
|
94
|
+
</div>
|
95
|
+
);
|
96
|
+
}
|
97
|
+
|
98
|
+
return (
|
99
|
+
<div className="px-4 sm:px-0">
|
100
|
+
{/* Header */}
|
101
|
+
<div className="mb-6">
|
102
|
+
<div className="flex items-center justify-between">
|
103
|
+
<div>
|
104
|
+
<h2 className="text-2xl font-bold text-gray-900 flex items-center">
|
105
|
+
<Activity className="h-7 w-7 mr-3" />
|
106
|
+
Events
|
107
|
+
</h2>
|
108
|
+
<p className="mt-1 text-sm text-gray-600">
|
109
|
+
Real-time Baremetal events from OpenStack Ironic
|
110
|
+
</p>
|
111
|
+
</div>
|
112
|
+
|
113
|
+
{/* Connection Status */}
|
114
|
+
<ConnectionStatus status={connectionStatus} />
|
115
|
+
</div>
|
116
|
+
</div>
|
117
|
+
|
118
|
+
{/* Stats & Controls */}
|
119
|
+
<div className="mb-6 flex flex-col sm:flex-row sm:items-center sm:justify-between space-y-4 sm:space-y-0">
|
120
|
+
<div className="flex items-center space-x-4">
|
121
|
+
<div className="bg-white px-4 py-2 rounded-lg border border-gray-200">
|
122
|
+
<div className="flex items-center">
|
123
|
+
<Activity className="h-4 w-4 text-gray-400 mr-2" />
|
124
|
+
<span className="text-sm font-medium text-gray-700">
|
125
|
+
{events.length} events
|
126
|
+
</span>
|
127
|
+
{eventCount > 0 && (
|
128
|
+
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
129
|
+
+{eventCount} new
|
130
|
+
</span>
|
131
|
+
)}
|
132
|
+
</div>
|
133
|
+
</div>
|
134
|
+
|
135
|
+
<EventsFilters onFiltersChange={handleFiltersChange} />
|
136
|
+
</div>
|
137
|
+
|
138
|
+
<div className="flex items-center space-x-2">
|
139
|
+
<button
|
140
|
+
onClick={handleReconnect}
|
141
|
+
className="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
|
142
|
+
disabled={connectionStatus.connected}
|
143
|
+
>
|
144
|
+
<RefreshCw className="h-4 w-4 mr-2" />
|
145
|
+
Reconnect
|
146
|
+
</button>
|
147
|
+
|
148
|
+
<button
|
149
|
+
onClick={handleClearEvents}
|
150
|
+
className="inline-flex items-center px-3 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
|
151
|
+
disabled={events.length === 0}
|
152
|
+
>
|
153
|
+
<Trash2 className="h-4 w-4 mr-2" />
|
154
|
+
Clear Events
|
155
|
+
</button>
|
156
|
+
</div>
|
157
|
+
</div>
|
158
|
+
|
159
|
+
{/* Connection Warning */}
|
160
|
+
{!connectionStatus.connected && (
|
161
|
+
<div className="mb-6 bg-yellow-50 border border-yellow-200 rounded-md p-4">
|
162
|
+
<div className="flex">
|
163
|
+
<div className="flex-shrink-0">
|
164
|
+
<Activity className="h-5 w-5 text-yellow-400" />
|
165
|
+
</div>
|
166
|
+
<div className="ml-3">
|
167
|
+
<h3 className="text-sm font-medium text-yellow-800">
|
168
|
+
Connection Issue
|
169
|
+
</h3>
|
170
|
+
<div className="mt-2 text-sm text-yellow-700">
|
171
|
+
<p>
|
172
|
+
Unable to connect to the events stream. Events may not appear in real-time.
|
173
|
+
{connectionStatus.error && ` Error: ${connectionStatus.error}`}
|
174
|
+
</p>
|
175
|
+
</div>
|
176
|
+
<div className="mt-4">
|
177
|
+
<div className="-mx-2 -my-1.5 flex">
|
178
|
+
<button
|
179
|
+
onClick={handleReconnect}
|
180
|
+
className="bg-yellow-50 px-2 py-1.5 rounded-md text-sm font-medium text-yellow-800 hover:bg-yellow-100"
|
181
|
+
>
|
182
|
+
Try Reconnecting
|
183
|
+
</button>
|
184
|
+
</div>
|
185
|
+
</div>
|
186
|
+
</div>
|
187
|
+
</div>
|
188
|
+
</div>
|
189
|
+
)}
|
190
|
+
|
191
|
+
{/* Events List */}
|
192
|
+
<div className="bg-gray-50 rounded-lg p-6">
|
193
|
+
<EventsList events={events} />
|
194
|
+
</div>
|
195
|
+
</div>
|
196
|
+
);
|
197
|
+
}
|