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.
Files changed (151) hide show
  1. osism-0.20250824.0/ChangeLog +7 -0
  2. {osism-0.20250823.0/osism.egg-info → osism-0.20250824.0}/PKG-INFO +2 -1
  3. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/Containerfile +3 -3
  4. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/Dockerfile +3 -3
  5. osism-0.20250824.0/frontend/app/api/config/route.ts +7 -0
  6. osism-0.20250824.0/frontend/app/api/health/route.ts +9 -0
  7. osism-0.20250824.0/frontend/app/components/ConnectionStatus.tsx +51 -0
  8. osism-0.20250824.0/frontend/app/components/EventsFilters.tsx +150 -0
  9. osism-0.20250824.0/frontend/app/components/EventsList.tsx +189 -0
  10. osism-0.20250824.0/frontend/app/events/page.tsx +197 -0
  11. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/app/globals.css +1 -3
  12. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/app/layout.tsx +5 -4
  13. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/app/nodes/page.tsx +32 -10
  14. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/app/page.tsx +14 -50
  15. osism-0.20250824.0/frontend/lib/api.ts +52 -0
  16. osism-0.20250824.0/frontend/lib/hooks/useWebSocket.ts +209 -0
  17. osism-0.20250824.0/frontend/lib/types.ts +72 -0
  18. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/package-lock.json +747 -1014
  19. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/package.json +8 -6
  20. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/postcss.config.mjs +1 -1
  21. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/tailwind.config.ts +0 -1
  22. {osism-0.20250823.0 → osism-0.20250824.0}/osism/api.py +101 -3
  23. osism-0.20250824.0/osism/services/event_bridge.py +304 -0
  24. {osism-0.20250823.0 → osism-0.20250824.0}/osism/services/listener.py +130 -19
  25. osism-0.20250824.0/osism/services/websocket_manager.py +271 -0
  26. {osism-0.20250823.0 → osism-0.20250824.0/osism.egg-info}/PKG-INFO +2 -1
  27. {osism-0.20250823.0 → osism-0.20250824.0}/osism.egg-info/SOURCES.txt +9 -0
  28. osism-0.20250824.0/osism.egg-info/pbr.json +1 -0
  29. {osism-0.20250823.0 → osism-0.20250824.0}/osism.egg-info/requires.txt +1 -0
  30. {osism-0.20250823.0 → osism-0.20250824.0}/requirements.txt +1 -0
  31. osism-0.20250823.0/ChangeLog +0 -7
  32. osism-0.20250823.0/frontend/lib/api.ts +0 -29
  33. osism-0.20250823.0/frontend/lib/types.ts +0 -29
  34. osism-0.20250823.0/osism.egg-info/pbr.json +0 -1
  35. {osism-0.20250823.0 → osism-0.20250824.0}/.flake8 +0 -0
  36. {osism-0.20250823.0 → osism-0.20250824.0}/.github/renovate.json +0 -0
  37. {osism-0.20250823.0 → osism-0.20250824.0}/.github/workflows/publish.yml +0 -0
  38. {osism-0.20250823.0 → osism-0.20250824.0}/.hadolint.yaml +0 -0
  39. {osism-0.20250823.0 → osism-0.20250824.0}/.zuul.yaml +0 -0
  40. {osism-0.20250823.0 → osism-0.20250824.0}/AUTHORS +0 -0
  41. {osism-0.20250823.0 → osism-0.20250824.0}/Containerfile +0 -0
  42. {osism-0.20250823.0 → osism-0.20250824.0}/Dockerfile +0 -0
  43. {osism-0.20250823.0 → osism-0.20250824.0}/LICENSE +0 -0
  44. {osism-0.20250823.0 → osism-0.20250824.0}/Pipfile +0 -0
  45. {osism-0.20250823.0 → osism-0.20250824.0}/Pipfile.lock +0 -0
  46. {osism-0.20250823.0 → osism-0.20250824.0}/README.md +0 -0
  47. {osism-0.20250823.0 → osism-0.20250824.0}/files/change.sh +0 -0
  48. {osism-0.20250823.0 → osism-0.20250824.0}/files/cleanup-ansible-collections.sh +0 -0
  49. {osism-0.20250823.0 → osism-0.20250824.0}/files/clustershell/clush.conf +0 -0
  50. {osism-0.20250823.0 → osism-0.20250824.0}/files/clustershell/groups.conf +0 -0
  51. {osism-0.20250823.0 → osism-0.20250824.0}/files/data/SCS-Spec.MandatoryFlavors.verbose.yaml +0 -0
  52. {osism-0.20250823.0 → osism-0.20250824.0}/files/netbox-manager/settings.toml +0 -0
  53. {osism-0.20250823.0 → osism-0.20250824.0}/files/redfishMockupCreate.py +0 -0
  54. {osism-0.20250823.0 → osism-0.20250824.0}/files/run-ansible-console.sh +0 -0
  55. {osism-0.20250823.0 → osism-0.20250824.0}/files/sonic/config_db.json +0 -0
  56. {osism-0.20250823.0 → osism-0.20250824.0}/files/sonic/port_config/Accton-AS4625-54T.ini +0 -0
  57. {osism-0.20250823.0 → osism-0.20250824.0}/files/sonic/port_config/Accton-AS5835-54T.ini +0 -0
  58. {osism-0.20250823.0 → osism-0.20250824.0}/files/sonic/port_config/Accton-AS5835-54X.ini +0 -0
  59. {osism-0.20250823.0 → osism-0.20250824.0}/files/sonic/port_config/Accton-AS7326-56X.ini +0 -0
  60. {osism-0.20250823.0 → osism-0.20250824.0}/files/sonic/port_config/Accton-AS7726-32X.ini +0 -0
  61. {osism-0.20250823.0 → osism-0.20250824.0}/files/sonic/port_config/Accton-AS9716-32D.ini +0 -0
  62. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/.dockerignore +0 -0
  63. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/.gitignore +0 -0
  64. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/app/favicon.ico +0 -0
  65. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/app/services/page.tsx +0 -0
  66. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/components.json +0 -0
  67. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/eslint.config.mjs +0 -0
  68. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/lib/utils.ts +0 -0
  69. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/next.config.ts +0 -0
  70. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/public/file.svg +0 -0
  71. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/public/globe.svg +0 -0
  72. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/public/next.svg +0 -0
  73. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/public/vercel.svg +0 -0
  74. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/public/window.svg +0 -0
  75. {osism-0.20250823.0 → osism-0.20250824.0}/frontend/tsconfig.json +0 -0
  76. {osism-0.20250823.0 → osism-0.20250824.0}/osism/__init__.py +0 -0
  77. {osism-0.20250823.0 → osism-0.20250824.0}/osism/__main__.py +0 -0
  78. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/__init__.py +0 -0
  79. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/apply.py +0 -0
  80. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/baremetal.py +0 -0
  81. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/compose.py +0 -0
  82. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/compute.py +0 -0
  83. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/configuration.py +0 -0
  84. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/console.py +0 -0
  85. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/container.py +0 -0
  86. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/get.py +0 -0
  87. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/log.py +0 -0
  88. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/manage.py +0 -0
  89. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/netbox.py +0 -0
  90. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/noset.py +0 -0
  91. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/reconciler.py +0 -0
  92. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/redfish.py +0 -0
  93. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/server.py +0 -0
  94. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/service.py +0 -0
  95. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/set.py +0 -0
  96. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/sonic.py +0 -0
  97. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/status.py +0 -0
  98. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/sync.py +0 -0
  99. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/task.py +0 -0
  100. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/validate.py +0 -0
  101. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/vault.py +0 -0
  102. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/volume.py +0 -0
  103. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/wait.py +0 -0
  104. {osism-0.20250823.0 → osism-0.20250824.0}/osism/commands/worker.py +0 -0
  105. {osism-0.20250823.0 → osism-0.20250824.0}/osism/data/__init__.py +0 -0
  106. {osism-0.20250823.0 → osism-0.20250824.0}/osism/data/enums.py +0 -0
  107. {osism-0.20250823.0 → osism-0.20250824.0}/osism/data/playbooks.py +0 -0
  108. {osism-0.20250823.0 → osism-0.20250824.0}/osism/main.py +0 -0
  109. {osism-0.20250823.0 → osism-0.20250824.0}/osism/services/__init__.py +0 -0
  110. {osism-0.20250823.0 → osism-0.20250824.0}/osism/settings.py +0 -0
  111. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/__init__.py +0 -0
  112. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/ansible.py +0 -0
  113. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/ceph.py +0 -0
  114. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/__init__.py +0 -0
  115. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/config.py +0 -0
  116. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/ironic.py +0 -0
  117. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/netbox.py +0 -0
  118. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/redfish.py +0 -0
  119. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/__init__.py +0 -0
  120. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/bgp.py +0 -0
  121. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/cache.py +0 -0
  122. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/config_generator.py +0 -0
  123. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/connections.py +0 -0
  124. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/constants.py +0 -0
  125. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/device.py +0 -0
  126. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/exporter.py +0 -0
  127. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/interface.py +0 -0
  128. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/sonic/sync.py +0 -0
  129. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor/utils.py +0 -0
  130. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/conductor.py +0 -0
  131. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/kolla.py +0 -0
  132. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/kubernetes.py +0 -0
  133. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/netbox.py +0 -0
  134. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/openstack.py +0 -0
  135. {osism-0.20250823.0 → osism-0.20250824.0}/osism/tasks/reconciler.py +0 -0
  136. {osism-0.20250823.0 → osism-0.20250824.0}/osism/utils/__init__.py +0 -0
  137. {osism-0.20250823.0 → osism-0.20250824.0}/osism/utils/ssh.py +0 -0
  138. {osism-0.20250823.0 → osism-0.20250824.0}/osism.egg-info/dependency_links.txt +0 -0
  139. {osism-0.20250823.0 → osism-0.20250824.0}/osism.egg-info/entry_points.txt +0 -0
  140. {osism-0.20250823.0 → osism-0.20250824.0}/osism.egg-info/not-zip-safe +0 -0
  141. {osism-0.20250823.0 → osism-0.20250824.0}/osism.egg-info/top_level.txt +0 -0
  142. {osism-0.20250823.0 → osism-0.20250824.0}/playbooks/build.yml +0 -0
  143. {osism-0.20250823.0 → osism-0.20250824.0}/playbooks/pre.yml +0 -0
  144. {osism-0.20250823.0 → osism-0.20250824.0}/playbooks/test-setup.yml +0 -0
  145. {osism-0.20250823.0 → osism-0.20250824.0}/requirements.ansible.txt +0 -0
  146. {osism-0.20250823.0 → osism-0.20250824.0}/requirements.netbox-manager.txt +0 -0
  147. {osism-0.20250823.0 → osism-0.20250824.0}/requirements.openstack-flavor-manager.txt +0 -0
  148. {osism-0.20250823.0 → osism-0.20250824.0}/requirements.openstack-image-manager.txt +0 -0
  149. {osism-0.20250823.0 → osism-0.20250824.0}/requirements.yml +0 -0
  150. {osism-0.20250823.0 → osism-0.20250824.0}/setup.cfg +0 -0
  151. {osism-0.20250823.0 → osism-0.20250824.0}/setup.py +0 -0
@@ -0,0 +1,7 @@
1
+ CHANGES
2
+ =======
3
+
4
+ v0.20250824.0
5
+ -------------
6
+
7
+ * chore(deps): update dependency websockets to v15 (#1667)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: osism
3
- Version: 0.20250823.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:18-alpine AS dependencies
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:18-alpine AS build
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:18-alpine AS runner
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:18-alpine AS dependencies
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:18-alpine AS build
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:18-alpine AS runner
15
+ FROM node:22-alpine AS runner
16
16
  WORKDIR /app
17
17
 
18
18
  ENV NODE_ENV=production
@@ -0,0 +1,7 @@
1
+ import { NextResponse } from 'next/server';
2
+
3
+ export async function GET() {
4
+ return NextResponse.json({
5
+ apiUrl: process.env.NEXT_PUBLIC_OSISM_API_URL || 'http://api:8000'
6
+ });
7
+ }
@@ -0,0 +1,9 @@
1
+ import { NextResponse } from 'next/server';
2
+
3
+ export async function GET() {
4
+ return NextResponse.json({
5
+ status: 'healthy',
6
+ timestamp: new Date().toISOString(),
7
+ service: 'OSISM Frontend'
8
+ });
9
+ }
@@ -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
+ }
@@ -1,6 +1,4 @@
1
- @tailwind base;
2
- @tailwind components;
3
- @tailwind utilities;
1
+ @import "tailwindcss";
4
2
 
5
3
  :root {
6
4
  --background: #ffffff;