diff --git a/AUTH_PROVIDER_FEATURES.md b/AUTH_PROVIDER_FEATURES.md new file mode 100644 index 0000000..a13f09b --- /dev/null +++ b/AUTH_PROVIDER_FEATURES.md @@ -0,0 +1,242 @@ +# Authentication Provider Management Features + +## Overview +A comprehensive authentication provider management system with multi-step wizards, permission management, usage analytics, and audit logging. + +## Features Implemented + +### 1. Multi-Step Provider Setup Wizard (`/providers/new`) + +**Step 1: Select Provider Type** +- Choose from Azure AD, Google Workspace, GitHub, or SAML 2.0 +- Visual cards with provider icons and descriptions +- Popular providers highlighted + +**Step 2: External Application Registration** +- Provider-specific step-by-step registration guides +- Direct links to provider consoles (Azure Portal, Google Cloud Console, GitHub Settings) +- Detailed instructions for: + - App registration + - Client secret creation + - API permission configuration + - Required values to collect +- Links to official documentation + +**Step 3: OAuth Configuration** +- Form fields for: + - Client ID (required) + - Client Secret (required, password-masked) + - Tenant ID (Azure AD only) + - Redirect URI (pre-filled, editable) + - OAuth Scopes (optional) +- Security notice about credential handling +- Form validation + +**Step 4: Permission Assignment** +- Interactive checkboxes for: + - Individual users + - Teams +- Split view showing users and teams side-by-side +- Note about ability to modify later + +**Step 5: Review & Create** +- Comprehensive summary showing: + - Provider information + - OAuth configuration (client secret masked) + - Permission assignments (users and teams with labels) + - Security reminders +- Final confirmation before creation + +### 2. Provider List View (`/providers`) + +**Features:** +- Card-based grid layout (responsive) +- Search functionality +- Provider status indicators (Active/Inactive) +- For each provider: + - Provider type icon + - Name and type + - Status badge + - Usage statistics (total logins, active users, success rate) + - Permission summary (user count, team count) +- Quick actions: + - Add Provider button + - View Audit Log button +- Empty state for first-time setup + +### 3. Enhanced Provider Details Page (`/providers/:id`) + +**Overview Tab:** +- Provider header with: + - Type icon and name + - Creation date + - Status badge + - Enable/Disable button + - Delete button +- Configuration section showing OAuth settings +- Permissions section: + - Authorized users (with labels) + - Authorized teams (with labels) + - Edit button for modifying permissions +- Usage statistics sidebar: + - Total logins + - Active users + - Success rate +- Quick links to audit events + +**Recent Activity Tab:** +- Table of recent audit events +- Columns: Timestamp, User, Action, Status, Details +- Link to full audit log +- Color-coded action and status labels + +**Interactive Features:** +- Edit permissions modal with checkboxes +- Delete confirmation modal with warning +- Toggle provider status (enable/disable) +- All actions create audit log entries + +### 4. Audit Log Page (`/audit-log`) + +**Features:** +- Searchable audit events +- Search by user, provider, action, or status +- Pagination controls (top and bottom) +- Event details table: + - Timestamp (with hover tooltip) + - User + - Action (color-coded label) + - Provider name (clickable link) + - Status (color-coded label) + - Details +- Clear all filters button +- Event counter badge +- Empty state handling + +**Sample Events Tracked:** +- Provider Created +- Provider Modified +- Provider Disabled/Enabled +- Provider Deleted +- User Login (Success/Failed) +- Permission Changed + +### 5. Data Persistence + +**Storage:** +- localStorage used for demo purposes +- Two data stores: + - `authProviders` - Provider configurations + - `auditLogs` - Audit event history + +**Sample Data:** +- Pre-populated with 4 sample providers +- Pre-populated with 8 sample audit events +- Automatically generated if not present + +## Navigation Structure + +``` +Home (/) +├── Auth Providers (/providers) +│ ├── Add Provider (/providers/new) - Multi-step wizard +│ └── Provider Details (/providers/:id) +├── Audit Log (/audit-log) +├── Dashboard (/dashboard) - Demo +├── Table Demo (/table-demo) - Demo +├── Form Demo (/form-demo) - Demo +└── Details Demo (/details-demo) - Demo +``` + +## Component Features + +### Security Features +- Client secrets masked in display +- Security warnings throughout wizard +- Delete confirmation modals +- Audit logging for all actions +- Permission-based access control + +### UX Features +- Consistent PatternFly design system +- Responsive layout (mobile, tablet, desktop) +- Loading and empty states +- Color-coded status indicators +- Icon-based visual hierarchy +- Hover tooltips for timestamps +- Inline help text and descriptions + +### Data Visualization +- Usage statistics with large numbers +- Success rate percentages +- Color-coded labels for status +- Badge counters +- Visual provider type icons + +## Provider Types Supported + +1. **Azure Active Directory** + - Microsoft enterprise authentication + - Requires: Client ID, Tenant ID, Client Secret + - Icon: Microsoft logo + +2. **Google Workspace** + - Google OAuth authentication + - Requires: Client ID, Client Secret + - Icon: Google magnifying glass + +3. **GitHub** + - GitHub OAuth authentication + - Requires: Client ID, Client Secret + - Icon: GitHub logo + +4. **SAML 2.0** + - Generic SAML identity provider + - Requires: SSO URL, Entity ID, Certificate + - Icon: Lock emoji + +## Usage Instructions + +### To Add a New Provider: +1. Click "Add Provider" from the providers list +2. Follow the 5-step wizard +3. Complete external registration in provider console +4. Enter OAuth credentials +5. Assign permissions +6. Review and create + +### To View Provider Details: +1. Click on any provider card in the list +2. View configuration, permissions, and statistics +3. Switch to "Recent Activity" tab for audit events + +### To Edit Permissions: +1. Open provider details +2. Click "Edit" in the Permissions section +3. Check/uncheck users and teams +4. Click "Save Changes" + +### To View Audit Log: +1. Click "Audit Log" in navigation +2. Use search and filters to find specific events +3. Click provider name to view details + +## Technical Stack + +- **Framework:** React with React Router +- **UI Library:** PatternFly 5 +- **Icons:** PatternFly Icons +- **Storage:** localStorage (demo) +- **Styling:** PatternFly CSS + +## Future Enhancements (Not Implemented) + +- Backend API integration +- Real authentication testing +- Export audit logs +- Email notifications +- Role-based access control +- Multi-factor authentication setup +- Provider health monitoring +- Advanced analytics dashboard + diff --git a/package-lock.json b/package-lock.json index d7d88a4..288594e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "test-framework", "version": "0.0.0", "dependencies": { - "@ansible/ansible-ui-framework": "^2.4.0", + "@ansible/ansible-ui-framework": "^2.4.2735", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^7.9.5" @@ -22,9 +22,9 @@ } }, "node_modules/@ansible/ansible-ui-framework": { - "version": "2.4.2730", - "resolved": "https://registry.npmjs.org/@ansible/ansible-ui-framework/-/ansible-ui-framework-2.4.2730.tgz", - "integrity": "sha512-7IhmYnn2YbouPsvt3PDmPPhlqwbjdSyUJQ/SAqz8kYdJIyMC6kS7kk2ZUxpCavFJEB88gEhZ+XvthJHKSyQTuw==", + "version": "2.4.2735", + "resolved": "https://registry.npmjs.org/@ansible/ansible-ui-framework/-/ansible-ui-framework-2.4.2735.tgz", + "integrity": "sha512-GoY1yyYFNy01yFe69FqEJOzCX6yvKwU2Uypsr2ObevYcvb1yVU/XHhPm85+ixo5YahuW7b3kkx+cikB17OQXrw==", "license": "Apache-2.0", "peerDependencies": { "@patternfly/patternfly": "^6.3", diff --git a/package.json b/package.json index 8ab764b..507519f 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "test:ui": "playwright test --ui" }, "dependencies": { - "@ansible/ansible-ui-framework": "^2.4.0", + "@ansible/ansible-ui-framework": "^2.4.2735", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^7.9.5" diff --git a/src/App.jsx b/src/App.jsx index 4a60e78..5c3fdd8 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,6 +5,10 @@ import DashboardDemo from './pages/DashboardDemo' import TableDemo from './pages/TableDemo' import FormDemo from './pages/FormDemo' import DetailsDemo from './pages/DetailsDemo' +import ProvidersList from './pages/ProvidersList' +import ProviderDetails from './pages/ProviderDetails' +import ProviderWizard from './pages/ProviderWizard' +import AuditLog from './pages/AuditLog' import '@patternfly/react-core/dist/styles/base.css' function App() { @@ -13,6 +17,10 @@ function App() { } /> + } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/src/Navigation.jsx b/src/Navigation.jsx index eab2480..024ea02 100644 --- a/src/Navigation.jsx +++ b/src/Navigation.jsx @@ -19,10 +19,8 @@ export function AppNavigation({ children }) { const navigation = [ { name: 'Home', path: '/' }, - { name: 'Dashboard', path: '/dashboard' }, - { name: 'Table Demo', path: '/table-demo' }, - { name: 'Form Demo', path: '/form-demo' }, - { name: 'Details Demo', path: '/details-demo' }, + { name: 'Authentication Providers', path: '/providers' }, + { name: 'Audit Log', path: '/audit-log' }, ] const Header = ( @@ -30,7 +28,7 @@ export function AppNavigation({ children }) { - Ansible UI Framework Prototypes + Auth Provider Management diff --git a/src/pages/AuditLog.jsx b/src/pages/AuditLog.jsx new file mode 100644 index 0000000..7697d1b --- /dev/null +++ b/src/pages/AuditLog.jsx @@ -0,0 +1,311 @@ +import { useState, useEffect, useMemo } from 'react' +import { Link } from 'react-router-dom' +import { PageLayout, PageHeader, PageTable } from '@ansible/ansible-ui-framework' +import { + Label, + Flex, + FlexItem, + Button, +} from '@patternfly/react-core' +import { + CheckCircleIcon, + ExclamationCircleIcon, + DownloadIcon, + FilterIcon, +} from '@patternfly/react-icons' + +function AuditLog() { + const [auditLogs, setAuditLogs] = useState([]) + const [page, setPage] = useState(1) + const [perPage, setPerPage] = useState(20) + + useEffect(() => { + // Load audit logs from localStorage (in real app, this would be an API call) + const logs = JSON.parse(localStorage.getItem('auditLogs') || '[]') + + // If no logs exist, create some sample data + if (logs.length === 0) { + const sampleLogs = [ + { + id: 1, + timestamp: new Date(Date.now() - 3600000).toISOString(), + user: 'admin@company.com', + action: 'Provider Created', + providerName: 'Azure AD Production', + providerType: 'azure-ad', + status: 'Success', + details: 'New Azure AD provider configured', + }, + { + id: 2, + timestamp: new Date(Date.now() - 7200000).toISOString(), + user: 'alice@company.com', + action: 'User Login', + providerName: 'Google Workspace', + providerType: 'google', + status: 'Success', + details: 'User successfully authenticated', + }, + { + id: 3, + timestamp: new Date(Date.now() - 10800000).toISOString(), + user: 'bob@company.com', + action: 'User Login', + providerName: 'Azure AD Production', + providerType: 'azure-ad', + status: 'Failed', + details: 'Invalid credentials provided', + }, + { + id: 4, + timestamp: new Date(Date.now() - 14400000).toISOString(), + user: 'admin@company.com', + action: 'Provider Modified', + providerName: 'GitHub OAuth', + providerType: 'github', + status: 'Success', + details: 'Updated permission assignments', + }, + { + id: 5, + timestamp: new Date(Date.now() - 18000000).toISOString(), + user: 'charlie@company.com', + action: 'User Login', + providerName: 'Google Workspace', + providerType: 'google', + status: 'Success', + details: 'User successfully authenticated', + }, + { + id: 6, + timestamp: new Date(Date.now() - 21600000).toISOString(), + user: 'admin@company.com', + action: 'Provider Disabled', + providerName: 'SAML Enterprise', + providerType: 'saml', + status: 'Success', + details: 'Provider temporarily disabled for maintenance', + }, + { + id: 7, + timestamp: new Date(Date.now() - 25200000).toISOString(), + user: 'diana@company.com', + action: 'User Login', + providerName: 'Azure AD Production', + providerType: 'azure-ad', + status: 'Success', + details: 'User successfully authenticated', + }, + { + id: 8, + timestamp: new Date(Date.now() - 28800000).toISOString(), + user: 'admin@company.com', + action: 'Provider Created', + providerName: 'Google Workspace', + providerType: 'google', + status: 'Success', + details: 'New Google OAuth provider configured', + }, + ] + localStorage.setItem('auditLogs', JSON.stringify(sampleLogs)) + setAuditLogs(sampleLogs) + } else { + setAuditLogs(logs) + } + }, []) + + const StatusIndicator = ({ status }) => { + if (status === 'Success') { + return ( + + + + + Success + + ) + } else if (status === 'Failed') { + return ( + + + + + Failed + + ) + } else { + return ( + + + + + Warning + + ) + } + } + + const getActionColor = (action) => { + if (action.includes('Created')) return 'blue' + if (action.includes('Modified')) return 'purple' + if (action.includes('Disabled')) return 'orange' + if (action.includes('Login')) return 'cyan' + return 'grey' + } + + const tableColumns = useMemo(() => [ + { + header: 'Timestamp', + cell: (log) => new Date(log.timestamp).toLocaleString(), + sort: 'timestamp', + defaultSort: true, + defaultSortDirection: 'desc', + }, + { + header: 'User', + cell: (log) => log.user, + sort: 'user', + }, + { + header: 'Action', + cell: (log) => , + sort: 'action', + }, + { + header: 'Provider', + cell: (log) => ( + {log.providerName} + ), + sort: 'providerName', + }, + { + header: 'Status', + cell: (log) => , + sort: 'status', + }, + { + header: 'Details', + cell: (log) => log.details, + }, + ], []) + + const toolbarFilters = useMemo(() => [ + { + key: 'action', + label: 'Action', + type: 'select', + options: [ + { label: 'Provider Created', value: 'Provider Created' }, + { label: 'Provider Modified', value: 'Provider Modified' }, + { label: 'Provider Disabled', value: 'Provider Disabled' }, + { label: 'User Login', value: 'User Login' }, + ], + placeholder: 'Filter by action', + }, + { + key: 'status', + label: 'Status', + type: 'select', + options: [ + { label: 'Success', value: 'Success' }, + { label: 'Failed', value: 'Failed' }, + ], + placeholder: 'Filter by status', + }, + { + key: 'user', + label: 'User', + type: 'string', + placeholder: 'Search by user', + }, + { + key: 'providerName', + label: 'Provider', + type: 'string', + placeholder: 'Search by provider name', + }, + ], []) + + const toolbarActions = useMemo(() => [ + { + type: 0, // PageActionType.Button + selection: 0, // PageActionSelection.None + variant: 'secondary', + label: 'Export logs', + onClick: () => { + console.log('Exporting audit logs...') + // In a real app, this would trigger a CSV/JSON download + const dataStr = JSON.stringify(auditLogs, null, 2) + const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr) + const exportFileDefaultName = `audit-logs-${new Date().toISOString().split('T')[0]}.json` + const linkElement = document.createElement('a') + linkElement.setAttribute('href', dataUri) + linkElement.setAttribute('download', exportFileDefaultName) + linkElement.click() + }, + icon: DownloadIcon, + }, + ], [auditLogs]) + + const rowActions = useMemo(() => [ + { + type: 0, // PageActionType.Button + selection: 1, // PageActionSelection.Single + label: 'View details', + onClick: (log) => { + console.log('View audit log details:', log) + }, + }, + { + type: 0, // PageActionType.Button + selection: 1, // PageActionSelection.Single + label: 'View provider', + onClick: (log) => { + console.log('Navigate to provider:', log.providerName) + }, + }, + ], []) + + return ( + + { + console.log('Exporting audit logs...') + const dataStr = JSON.stringify(auditLogs, null, 2) + const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr) + const exportFileDefaultName = `audit-logs-${new Date().toISOString().split('T')[0]}.json` + const linkElement = document.createElement('a') + linkElement.setAttribute('href', dataUri) + linkElement.setAttribute('download', exportFileDefaultName) + linkElement.click() + }} + > + Export logs + + } + /> + log.id} + tableColumns={tableColumns} + toolbarFilters={toolbarFilters} + toolbarActions={toolbarActions} + rowActions={rowActions} + itemCount={auditLogs.length} + pageItems={auditLogs} + page={page} + setPage={setPage} + perPage={perPage} + setPerPage={setPerPage} + errorStateTitle="Error loading audit events" + emptyStateTitle="No audit logs found" + /> + + ) +} + +export default AuditLog diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx index c54b45b..93e8133 100644 --- a/src/pages/HomePage.jsx +++ b/src/pages/HomePage.jsx @@ -14,44 +14,54 @@ function HomePage() { <> - Welcome to Ansible UI Framework Prototyping + Authentication Provider Management

- This environment is set up for UX designers to prototype and experiment with - Ansible UI Framework components. Each demo page showcases different patterns - and components available in the framework. + Manage external authentication providers, configure single sign-on, and track + authentication events across your organization.

- Getting Started + Key Features - Navigate using the sidebar menu - Explore different component demos - Copy and modify existing pages - Create new prototype pages in src/pages/ + + Multi-Step Provider Setup: Guided wizard for configuring Azure AD, Google, GitHub, and SAML + + + Permission Management: Assign users and teams to specific providers + + + Usage Analytics: Track login statistics and success rates + + + Audit Logging: Monitor all authentication events and configuration changes + - Available Demos + Quick Links - Dashboard: Comprehensive dashboard with charts, stats, alerts, and more - - - Table Demo: Data tables with sorting and pagination + + View Authentication Providers + - Form Demo: Form inputs and validation patterns + + Add New Provider + - Details Demo: Resource details and information display + + View Audit Log + @@ -61,6 +71,15 @@ function HomePage() { Resources + + + Ansible UI Framework Documentation + + - See README.md for detailed instructions diff --git a/src/pages/ProviderDetails.jsx b/src/pages/ProviderDetails.jsx new file mode 100644 index 0000000..0a5c2f9 --- /dev/null +++ b/src/pages/ProviderDetails.jsx @@ -0,0 +1,567 @@ +import { useState, useEffect, useMemo } from 'react' +import { useParams, useNavigate, Link } from 'react-router-dom' +import { PageLayout, PageHeader, PageDetails, PageDetail, PageTable } from '@ansible/ansible-ui-framework' +import { + Button, + Card, + CardBody, + CardTitle, + Tabs, + Tab, + TabTitleText, + Flex, + FlexItem, + Label, + Alert, + Grid, + GridItem, + Modal, + ModalVariant, + Checkbox, + Icon, + Title, +} from '@patternfly/react-core' +import { + CheckCircleIcon, + ExclamationCircleIcon, + ChartLineIcon, +} from '@patternfly/react-icons' + +function ProviderDetails() { + const { id } = useParams() + const navigate = useNavigate() + const [activeTabKey, setActiveTabKey] = useState(0) + const [provider, setProvider] = useState(null) + const [auditLogs, setAuditLogs] = useState([]) + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) + const [isEditPermissionsOpen, setIsEditPermissionsOpen] = useState(false) + const [selectedUsers, setSelectedUsers] = useState([]) + const [selectedTeams, setSelectedTeams] = useState([]) + + // Mock data for editing + const availableUsers = [ + 'alice@company.com', + 'bob@company.com', + 'charlie@company.com', + 'diana@company.com', + ] + const availableTeams = ['Engineering', 'Operations', 'Security', 'Management'] + + useEffect(() => { + // Load provider from localStorage + const providers = JSON.parse(localStorage.getItem('authProviders') || '[]') + console.log('Looking for provider with ID:', id) + console.log('Available providers:', providers) + const foundProvider = providers.find((p) => p.id === parseInt(id)) + console.log('Found provider:', foundProvider) + + if (foundProvider) { + setProvider(foundProvider) + setSelectedUsers(foundProvider.permissions?.users || []) + setSelectedTeams(foundProvider.permissions?.teams || []) + } + + // Load relevant audit logs + const logs = JSON.parse(localStorage.getItem('auditLogs') || '[]') + const providerLogs = logs + .filter((log) => log.providerName === foundProvider?.name) + .slice(0, 5) + setAuditLogs(providerLogs) + }, [id]) + + if (!provider) { + return ( + + + +

The authentication provider with ID {id} does not exist or has been deleted.

+ +
+
+ ) + } + + const getProviderTypeName = (type) => { + const names = { + 'azure-ad': 'Azure Active Directory', + 'google': 'Google Workspace', + 'github': 'GitHub', + 'saml': 'SAML 2.0', + } + return names[type] || type + } + + const handleDelete = () => { + const providers = JSON.parse(localStorage.getItem('authProviders') || '[]') + const updated = providers.filter((p) => p.id !== provider.id) + localStorage.setItem('authProviders', JSON.stringify(updated)) + + // Add audit log + const logs = JSON.parse(localStorage.getItem('auditLogs') || '[]') + logs.unshift({ + id: Date.now(), + timestamp: new Date().toISOString(), + user: 'admin@company.com', + action: 'Provider Deleted', + providerName: provider.name, + providerType: provider.type, + status: 'Success', + details: 'Authentication provider permanently deleted', + }) + localStorage.setItem('auditLogs', JSON.stringify(logs)) + + navigate('/providers') + } + + const handleToggleStatus = () => { + const providers = JSON.parse(localStorage.getItem('authProviders') || '[]') + const updated = providers.map((p) => + p.id === provider.id + ? { ...p, status: p.status === 'active' ? 'inactive' : 'active' } + : p + ) + localStorage.setItem('authProviders', JSON.stringify(updated)) + + // Add audit log + const logs = JSON.parse(localStorage.getItem('auditLogs') || '[]') + const newStatus = provider.status === 'active' ? 'inactive' : 'active' + logs.unshift({ + id: Date.now(), + timestamp: new Date().toISOString(), + user: 'admin@company.com', + action: newStatus === 'active' ? 'Provider Enabled' : 'Provider Disabled', + providerName: provider.name, + providerType: provider.type, + status: 'Success', + details: `Provider status changed to ${newStatus}`, + }) + localStorage.setItem('auditLogs', JSON.stringify(logs)) + + setProvider({ ...provider, status: newStatus }) + } + + const handleSavePermissions = () => { + const providers = JSON.parse(localStorage.getItem('authProviders') || '[]') + const updated = providers.map((p) => + p.id === provider.id + ? { + ...p, + permissions: { + users: selectedUsers, + teams: selectedTeams, + }, + } + : p + ) + localStorage.setItem('authProviders', JSON.stringify(updated)) + + // Add audit log + const logs = JSON.parse(localStorage.getItem('auditLogs') || '[]') + logs.unshift({ + id: Date.now(), + timestamp: new Date().toISOString(), + user: 'admin@company.com', + action: 'Provider Modified', + providerName: provider.name, + providerType: provider.type, + status: 'Success', + details: 'Updated permission assignments', + }) + localStorage.setItem('auditLogs', JSON.stringify(logs)) + + setProvider({ + ...provider, + permissions: { users: selectedUsers, teams: selectedTeams }, + }) + setIsEditPermissionsOpen(false) + } + + const handleUserToggle = (user) => { + setSelectedUsers((prev) => + prev.includes(user) ? prev.filter((u) => u !== user) : [...prev, user] + ) + } + + const handleTeamToggle = (team) => { + setSelectedTeams((prev) => + prev.includes(team) ? prev.filter((t) => t !== team) : [...prev, team] + ) + } + + const StatusIndicator = ({ status }) => { + if (status === 'Success') { + return ( + + + + + Success + + ) + } else if (status === 'Failed') { + return ( + + + + + Failed + + ) + } else { + return ( + + + + + Warning + + ) + } + } + + const getActionColor = (action) => { + if (action.includes('Created')) return 'blue' + if (action.includes('Modified')) return 'purple' + if (action.includes('Disabled')) return 'orange' + if (action.includes('Login')) return 'cyan' + return 'grey' + } + + const auditTableColumns = useMemo(() => [ + { + header: 'Timestamp', + cell: (log) => new Date(log.timestamp).toLocaleString(), + }, + { + header: 'User', + cell: (log) => log.user, + }, + { + header: 'Action', + cell: (log) => , + }, + { + header: 'Status', + cell: (log) => , + }, + { + header: 'Details', + cell: (log) => log.details, + }, + ], []) + + const headerActions = ( + + + + + + + + + ) + + return ( + + + + {provider.status === 'inactive' && ( + + Users cannot authenticate using this provider while it is disabled. + + )} + + setActiveTabKey(tabIndex)} + aria-label="Provider details tabs" + > + Details}> +
+ + + + Configuration + + + + {getProviderTypeName(provider.type)} + + + {provider.config?.clientId || 'Not configured'} + + {provider.type === 'azure-ad' && ( + + {provider.config?.tenantId || 'Not configured'} + + )} + + {provider.config?.redirectUri || 'Not configured'} + + + {provider.status === 'active' ? ( + + + + + Active + + ) : ( + + + + + Inactive + + )} + + + + + + + + + Permissions + + + + + + + + + + Authorized Users + + {provider.permissions?.users?.length > 0 ? ( + + {provider.permissions.users.map((user) => ( + + + + ))} + + ) : ( +

+ No users assigned +

+ )} +
+ + + Authorized Teams + + {provider.permissions?.teams?.length > 0 ? ( + + {provider.permissions.teams.map((team) => ( + + + + ))} + + ) : ( +

+ No teams assigned +

+ )} +
+
+
+
+
+ + + + + + + + + + + Usage Statistics + + + + + + + {provider.stats?.totalLogins || 0} + + + + + {provider.stats?.activeUsers || 0} + + + + + {provider.stats?.successRate || 0}% + + + + + + +
+
+
+ + Recent Activity}> +
+ + + + Recent Audit Events + + + + + + + {auditLogs.length > 0 ? ( + log.id} + tableColumns={auditTableColumns} + itemCount={auditLogs.length} + pageItems={auditLogs} + page={1} + setPage={() => {}} + perPage={10} + setPerPage={() => {}} + errorStateTitle="Error loading audit events" + disableCardView + disableListView + disablePagination + /> + ) : ( + + There are no recent audit events for this provider. + + )} + + +
+
+
+ + {/* Delete Confirmation Modal */} + setIsDeleteModalOpen(false)} + actions={[ + , + , + ]} + > + + Deleting this provider will prevent all associated users from authenticating. All + configuration and statistics will be permanently lost. + +

+ Provider: {provider.name} +

+
+ + {/* Edit Permissions Modal */} + setIsEditPermissionsOpen(false)} + actions={[ + , + , + ]} + > + + + + Authorized Users + + {availableUsers.map((user) => ( + handleUserToggle(user)} + style={{ marginBottom: '0.5rem' }} + /> + ))} + + + + Authorized Teams + + {availableTeams.map((team) => ( + handleTeamToggle(team)} + style={{ marginBottom: '0.5rem' }} + /> + ))} + + + +
+ ) +} + +export default ProviderDetails diff --git a/src/pages/ProviderWizard.jsx b/src/pages/ProviderWizard.jsx new file mode 100644 index 0000000..63d953a --- /dev/null +++ b/src/pages/ProviderWizard.jsx @@ -0,0 +1,518 @@ +import { useNavigate } from 'react-router-dom' +import { useState } from 'react' +import { + PageLayout, + PageWizard, + PageFormTextInput, + PageFormSelect, + PageFormCheckbox, +} from '@ansible/ansible-ui-framework' +import { + Card, + CardBody, + CardTitle, + Alert, + List, + ListItem, + DescriptionList, + DescriptionListGroup, + DescriptionListTerm, + DescriptionListDescription, + Label, + Grid, + GridItem, + Flex, + FlexItem, + Title, + FormGroup, + Divider, + Checkbox, +} from '@patternfly/react-core' +import { + ExternalLinkAltIcon, +} from '@patternfly/react-icons' + +function ProviderWizard() { + const navigate = useNavigate() + const [selectedUsers, setSelectedUsers] = useState([]) + const [selectedTeams, setSelectedTeams] = useState([]) + + const providerTypes = [ + { + id: 'azure-ad', + label: 'Azure Active Directory', + description: 'Enterprise identity and access management for Azure', + }, + { + id: 'google', + label: 'Google Workspace', + description: 'Google OAuth 2.0 authentication for your organization', + }, + { + id: 'github', + label: 'GitHub', + description: 'Authenticate with GitHub accounts', + }, + { + id: 'saml', + label: 'SAML 2.0', + description: 'Generic SAML 2.0 identity provider integration', + }, + ] + + const availableUsers = [ + 'admin', + 'user1', + 'user2', + 'alice@company.com', + 'bob@company.com', + ] + + const availableTeams = ['developers', 'qa', 'operations', 'management'] + + const getProviderTypeName = (type) => { + const names = { + 'azure-ad': 'Azure AD', + 'google': 'Google Workspace', + 'github': 'GitHub', + 'saml': 'SAML 2.0', + } + return names[type] || type + } + + const getRegistrationInstructions = (providerType) => { + const instructions = { + 'azure-ad': { + title: 'Register Application in Azure Portal', + steps: [ + { + title: 'Create App Registration', + description: 'Navigate to Azure Portal and create a new app registration', + details: [ + 'Go to Azure Active Directory > App registrations', + 'Click "New registration"', + 'Name: Ansible Automation Platform', + 'Supported account types: Single tenant', + 'Redirect URI: Web - http://localhost:8080/oauth/callback', + ], + }, + { + title: 'Create Client Secret', + description: 'Generate a client secret for authentication', + details: [ + 'Go to Certificates & secrets', + 'Click "New client secret"', + 'Add a description and set expiration', + 'Copy the secret value immediately', + ], + }, + { + title: 'Configure API Permissions', + description: 'Set up required Microsoft Graph permissions', + details: [ + 'Go to API permissions', + 'Add Microsoft Graph delegated permissions', + 'Required: User.Read, openid, profile, email', + 'Grant admin consent for the organization', + ], + }, + ], + documentation: 'https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app', + }, + 'google': { + title: 'Create OAuth 2.0 Credentials in Google Cloud', + steps: [ + { + title: 'Configure OAuth Consent Screen', + description: 'Set up the consent screen shown to users', + details: [ + 'Go to Google Cloud Console > APIs & Services > OAuth consent screen', + 'Select Internal or External user type', + 'Fill in app name, user support email, developer email', + 'Add scopes: openid, profile, email', + ], + }, + { + title: 'Create OAuth Client ID', + description: 'Generate OAuth 2.0 credentials', + details: [ + 'Go to Credentials > Create Credentials > OAuth client ID', + 'Application type: Web application', + 'Name: Ansible Automation Platform', + 'Authorized redirect URIs: http://localhost:8080/oauth/callback', + 'Copy Client ID and Client Secret', + ], + }, + ], + documentation: 'https://developers.google.com/identity/protocols/oauth2', + }, + 'github': { + title: 'Create OAuth App in GitHub', + steps: [ + { + title: 'Register New OAuth Application', + description: 'Create an OAuth app in your GitHub organization or account', + details: [ + 'Go to Settings > Developer settings > OAuth Apps', + 'Click "New OAuth App"', + 'Application name: Ansible Automation Platform', + 'Homepage URL: http://localhost:8080', + 'Authorization callback URL: http://localhost:8080/oauth/callback', + 'Generate a new client secret', + ], + }, + ], + documentation: 'https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app', + }, + 'saml': { + title: 'Configure SAML 2.0 Identity Provider', + steps: [ + { + title: 'Gather IdP Information', + description: 'Collect required information from your SAML provider', + details: [ + 'SAML SSO URL (or IdP Login URL)', + 'Identity Provider Entity ID', + 'X.509 Certificate', + ], + }, + { + title: 'Configure Service Provider', + description: 'Provide these values to your SAML IdP:', + details: [ + 'SP Entity ID: urn:ansible:automation-platform', + 'Assertion Consumer Service URL: http://localhost:8080/auth/saml/callback', + 'Name ID format: EmailAddress', + ], + }, + { + title: 'Attribute Mapping', + description: 'Configure SAML attribute mappings', + details: [ + 'email → Email address', + 'firstName → First name', + 'lastName → Last name', + 'groups → Group membership (optional)', + ], + }, + ], + documentation: 'https://en.wikipedia.org/wiki/SAML_2.0', + }, + } + return instructions[providerType] || {} + } + + const steps = [ + { + id: 'provider-type', + label: 'Select Provider Type', + inputs: ( + ({ + label: type.label, + description: type.description, + value: type.id, + }))} + isRequired + labelHelp="Select the external identity provider your organization uses for authentication" + /> + ), + }, + { + id: 'external-setup', + label: 'External Setup', + inputs: ( + <> + + +
+ + External Application Registration + + +

After selecting your provider type, detailed registration instructions will appear here.

+
+ + + + + + + + + + Register Your Application + + + + + Navigate to your identity provider's developer console + Create a new OAuth application or SAML configuration + Configure redirect URIs and permissions + Copy the Client ID, Client Secret, and other credentials + Enter those credentials in the next step + + + + + +
+ + ), + }, + { + id: 'oauth-config', + label: 'OAuth Configuration', + inputs: ( + <> + + + + + + + + ), + }, + { + id: 'permissions', + label: 'Permission Assignment', + inputs: ( + <> + {availableUsers.map((user) => ( + { + if (checked) { + setSelectedUsers((prev) => [...prev, user]) + } else { + setSelectedUsers((prev) => prev.filter(item => item !== user)) + } + }} + /> + ))} + + {availableTeams.map((team) => ( + { + if (checked) { + setSelectedTeams((prev) => [...prev, team]) + } else { + setSelectedTeams((prev) => prev.filter(item => item !== team)) + } + }} + /> + ))} + + ), + }, + { + id: 'review', + label: 'Review & Create', + inputs: ( + <> + + +
+ + Review and Create + + +

Your provider configuration will be reviewed here before submission.

+
+ + + Configuration Summary + +

When you click "Create Provider", your authentication provider will be created with:

+ + The selected provider type from Step 1 + OAuth credentials configured in Step 3 + User and team permissions assigned in Step 4 + +
+
+ + + Selected Permissions + + + + + Assigned Users + + {selectedUsers.length > 0 ? ( + + {selectedUsers.map((user, i) => ( + + + + ))} + + ) : ( +

No users assigned

+ )} +
+ + + Assigned Teams + + {selectedTeams.length > 0 ? ( + + {selectedTeams.map((team, i) => ( + + + + ))} + + ) : ( +

No teams assigned

+ )} +
+
+
+
+ + + + + Client secrets are encrypted at rest and in transit + + + Only authorized users and teams can authenticate with this provider + + All authentication attempts are logged in the audit log + + You can disable this provider at any time from the provider details page + + + +
+ + ), + }, + ] + + const onSubmit = async (wizardData) => { + const newProvider = { + id: Date.now(), + type: wizardData.providerType, + name: wizardData.providerName || `${getProviderTypeName(wizardData.providerType)} Provider`, + status: 'active', + createdAt: new Date().toISOString(), + config: { + clientId: wizardData.clientId, + clientSecret: wizardData.clientSecret, + tenantId: wizardData.tenantId, + redirectUri: wizardData.redirectUri || 'http://localhost:8080/oauth/callback', + scopes: wizardData.scopes, + }, + permissions: { + users: selectedUsers, + teams: selectedTeams, + }, + stats: { + totalLogins: 0, + activeUsers: 0, + successRate: 0, + }, + } + + // Store in localStorage for demo purposes + const providers = JSON.parse(localStorage.getItem('authProviders') || '[]') + providers.push(newProvider) + localStorage.setItem('authProviders', JSON.stringify(providers)) + + // Add audit log entry + const auditLogs = JSON.parse(localStorage.getItem('auditLogs') || '[]') + auditLogs.unshift({ + id: Date.now() + 1, + timestamp: new Date().toISOString(), + user: 'System', + action: `Provider Created: ${newProvider.name}`, + providerName: newProvider.name, + status: 'Success', + details: `New ${getProviderTypeName(wizardData.providerType)} provider '${newProvider.name}' was created.`, + }) + localStorage.setItem('auditLogs', JSON.stringify(auditLogs)) + + navigate('/providers') + } + + return ( + + navigate('/providers')} + title="Create Authentication Provider" + singleColumn + /> + + ) +} + +export default ProviderWizard diff --git a/src/pages/ProvidersList.jsx b/src/pages/ProvidersList.jsx new file mode 100644 index 0000000..3eebc76 --- /dev/null +++ b/src/pages/ProvidersList.jsx @@ -0,0 +1,376 @@ +import { useState, useEffect, useMemo } from 'react' +import { useNavigate } from 'react-router-dom' +import { PageLayout, PageHeader, PageTable, usePageNavigate } from '@ansible/ansible-ui-framework' +import { + Button, + Label, + Badge, + Flex, + FlexItem, + SearchInput, +} from '@patternfly/react-core' +import { + CheckCircleIcon, + ExclamationCircleIcon, + PencilAltIcon, + TrashIcon, + PowerOffIcon, + CheckIcon, +} from '@patternfly/react-icons' + +function ProvidersList() { + const navigate = useNavigate() + const [providers, setProviders] = useState([]) + const [page, setPage] = useState(1) + const [perPage, setPerPage] = useState(20) + const [filterState, setFilterState] = useState({}) + const [searchValue, setSearchValue] = useState('') + + useEffect(() => { + // Load providers from localStorage (in real app, this would be an API call) + const storedProviders = JSON.parse(localStorage.getItem('authProviders') || '[]') + + // If no providers exist, create some sample data + if (storedProviders.length === 0) { + const sampleProviders = [ + { + id: 1, + type: 'azure-ad', + name: 'Azure AD Production', + status: 'active', + createdAt: new Date(Date.now() - 86400000 * 7).toISOString(), + permissions: { + users: ['alice@company.com', 'bob@company.com'], + teams: ['Engineering', 'Operations'], + }, + stats: { + totalLogins: 342, + activeUsers: 28, + successRate: 98.5, + }, + }, + { + id: 2, + type: 'google', + name: 'Google Workspace', + status: 'active', + createdAt: new Date(Date.now() - 86400000 * 14).toISOString(), + permissions: { + users: ['charlie@company.com', 'diana@company.com'], + teams: ['Engineering'], + }, + stats: { + totalLogins: 156, + activeUsers: 12, + successRate: 99.2, + }, + }, + { + id: 3, + type: 'github', + name: 'GitHub OAuth', + status: 'active', + createdAt: new Date(Date.now() - 86400000 * 21).toISOString(), + permissions: { + users: ['alice@company.com'], + teams: ['Engineering'], + }, + stats: { + totalLogins: 89, + activeUsers: 8, + successRate: 97.8, + }, + }, + { + id: 4, + type: 'saml', + name: 'SAML Enterprise', + status: 'inactive', + createdAt: new Date(Date.now() - 86400000 * 30).toISOString(), + permissions: { + users: [], + teams: ['Management'], + }, + stats: { + totalLogins: 0, + activeUsers: 0, + successRate: 0, + }, + }, + ] + localStorage.setItem('authProviders', JSON.stringify(sampleProviders)) + setProviders(sampleProviders) + } else { + setProviders(storedProviders) + } + }, []) + + const getProviderTypeName = (type) => { + const names = { + 'azure-ad': 'Azure AD', + 'google': 'Google Workspace', + 'github': 'GitHub', + 'saml': 'SAML 2.0', + } + return names[type] || type + } + + const StatusIndicator = ({ status }) => { + if (status === 'active') { + return ( + + + + + Active + + ) + } else { + return ( + + + + + Inactive + + ) + } + } + + const tableColumns = useMemo(() => [ + { + header: 'Name', + cell: (provider) => ( + + ), + sort: 'name', + card: 'name', + list: 'name', + }, + { + header: 'Type', + cell: (provider) => getProviderTypeName(provider.type), + sort: 'type', + }, + { + header: 'Status', + cell: (provider) => , + sort: 'status', + }, + { + header: 'Total Logins', + cell: (provider) => provider.stats?.totalLogins || 0, + sort: 'stats.totalLogins', + }, + { + header: 'Active Users', + cell: (provider) => provider.stats?.activeUsers || 0, + sort: 'stats.activeUsers', + }, + { + header: 'Success Rate', + cell: (provider) => `${provider.stats?.successRate || 0}%`, + sort: 'stats.successRate', + }, + { + header: 'Permissions', + cell: (provider) => ( + + + + {provider.permissions?.users?.length || 0} users + + + + + {provider.permissions?.teams?.length || 0} teams + + + + ), + }, + ], []) + + const toolbarFilters = useMemo(() => [ + { + key: 'status', + label: 'Status', + type: 'select', + options: [ + { label: 'Active', value: 'active' }, + { label: 'Inactive', value: 'inactive' }, + ], + placeholder: 'Filter by status', + }, + { + key: 'type', + label: 'Provider Type', + type: 'select', + options: [ + { label: 'Azure AD', value: 'azure-ad' }, + { label: 'Google Workspace', value: 'google' }, + { label: 'GitHub', value: 'github' }, + { label: 'SAML 2.0', value: 'saml' }, + ], + placeholder: 'Filter by type', + }, + ], []) + + // Filter providers based on search + const filteredProviders = useMemo(() => { + if (!searchValue) return providers + return providers.filter(provider => + provider.name.toLowerCase().includes(searchValue.toLowerCase()) + ) + }, [providers, searchValue]) + + const toolbarActions = useMemo( + () => [ + { + type: 0, // PageActionType.Button + selection: 0, // PageActionSelection.None (no selection needed) + variant: 'primary', + label: 'Create provider', + onClick: () => navigate('/providers/new'), + isPinned: true, // Pin to toolbar instead of dropdown + }, + ], + [navigate] + ) + + const rowActions = useMemo(() => [ + { + type: 0, // PageActionType.Button + selection: 1, // PageActionSelection.Single + icon: PencilAltIcon, + label: 'Edit', + onClick: (provider) => navigate(`/providers/${provider.id}`), + }, + { + type: 0, // PageActionType.Button + selection: 1, // PageActionSelection.Single + icon: (provider) => provider.status === 'active' ? PowerOffIcon : CheckIcon, + label: (provider) => provider.status === 'active' ? 'Disable' : 'Enable', + onClick: (provider) => { + const providers = JSON.parse(localStorage.getItem('authProviders') || '[]') + const updated = providers.map((p) => + p.id === provider.id + ? { ...p, status: p.status === 'active' ? 'inactive' : 'active' } + : p + ) + localStorage.setItem('authProviders', JSON.stringify(updated)) + setProviders(updated) + }, + }, + { + type: 4, // PageActionType.Separator + }, + { + type: 0, // PageActionType.Button + selection: 1, // PageActionSelection.Single + icon: TrashIcon, + label: 'Delete', + onClick: (provider) => { + const providers = JSON.parse(localStorage.getItem('authProviders') || '[]') + const updated = providers.filter((p) => p.id !== provider.id) + localStorage.setItem('authProviders', JSON.stringify(updated)) + setProviders(updated) + }, + isDanger: true, + }, + ], [navigate, setProviders]) + + const bulkActions = useMemo(() => [ + { + type: 0, // PageActionType.Button + selection: 2, // PageActionSelection.Multiple + label: 'Enable selected', + onClick: (selectedProviders) => { + const providerIds = selectedProviders.map(p => p.id) + const providers = JSON.parse(localStorage.getItem('authProviders') || '[]') + const updated = providers.map((p) => + providerIds.includes(p.id) ? { ...p, status: 'active' } : p + ) + localStorage.setItem('authProviders', JSON.stringify(updated)) + setProviders(updated) + }, + }, + { + type: 0, // PageActionType.Button + selection: 2, // PageActionSelection.Multiple + label: 'Disable selected', + onClick: (selectedProviders) => { + const providerIds = selectedProviders.map(p => p.id) + const providers = JSON.parse(localStorage.getItem('authProviders') || '[]') + const updated = providers.map((p) => + providerIds.includes(p.id) ? { ...p, status: 'inactive' } : p + ) + localStorage.setItem('authProviders', JSON.stringify(updated)) + setProviders(updated) + }, + }, + { + type: 4, // PageActionType.Separator + }, + { + type: 0, // PageActionType.Button + selection: 2, // PageActionSelection.Multiple + label: 'Delete selected', + onClick: (selectedProviders) => { + const providerIds = selectedProviders.map(p => p.id) + const providers = JSON.parse(localStorage.getItem('authProviders') || '[]') + const updated = providers.filter((p) => !providerIds.includes(p.id)) + localStorage.setItem('authProviders', JSON.stringify(updated)) + setProviders(updated) + }, + isDanger: true, + }, + ], [setProviders]) + + return ( + + + provider.id} + tableColumns={tableColumns} + toolbarFilters={toolbarFilters} + filterState={filterState} + setFilterState={setFilterState} + toolbarContent={ + setSearchValue(value)} + onClear={() => setSearchValue('')} + style={{ width: '300px' }} + /> + } + toolbarActions={toolbarActions} + rowActions={rowActions} + bulkActions={bulkActions} + itemCount={filteredProviders.length} + pageItems={filteredProviders} + page={page} + setPage={setPage} + perPage={perPage} + setPerPage={setPerPage} + errorStateTitle="Error loading providers" + emptyStateTitle="No providers found" + showSelect + isSelectMultiple + /> + + ) +} + +export default ProvidersList diff --git a/src/pages/TableDemo.jsx b/src/pages/TableDemo.jsx index 46afd83..1f15c55 100644 --- a/src/pages/TableDemo.jsx +++ b/src/pages/TableDemo.jsx @@ -1,48 +1,347 @@ -import { useState } from 'react' +import { useState, useMemo } from 'react' +import { useNavigate } from 'react-router-dom' import { PageLayout, PageHeader, PageTable } from '@ansible/ansible-ui-framework' +import { + Button, + Label, + Flex, + FlexItem, + Progress, + ProgressVariant, +} from '@patternfly/react-core' +import { + CheckCircleIcon, + ExclamationCircleIcon, + SyncAltIcon, + ClockIcon, + PlusCircleIcon, + PlayIcon, +} from '@patternfly/react-icons' function TableDemo() { - const [items] = useState([ - { id: 1, name: 'Automation Job 1', status: 'Success', created: '2024-11-03' }, - { id: 2, name: 'Automation Job 2', status: 'Running', created: '2024-11-03' }, - { id: 3, name: 'Automation Job 3', status: 'Failed', created: '2024-11-02' }, - { id: 4, name: 'Automation Job 4', status: 'Success', created: '2024-11-02' }, - { id: 5, name: 'Automation Job 5', status: 'Pending', created: '2024-11-01' }, + const navigate = useNavigate() + const [items, setItems] = useState([ + { + id: 1, + name: 'Deploy Production App', + status: 'Success', + template: 'Deploy Application', + created: '2024-11-13 14:23:00', + duration: '2m 34s', + progress: 100, + user: 'admin', + }, + { + id: 2, + name: 'Database Backup', + status: 'Running', + template: 'Backup Database', + created: '2024-11-13 14:45:00', + duration: '1m 12s', + progress: 65, + user: 'automation', + }, + { + id: 3, + name: 'Update Security Patches', + status: 'Failed', + template: 'System Updates', + created: '2024-11-13 12:15:00', + duration: '0m 45s', + progress: 30, + user: 'admin', + }, + { + id: 4, + name: 'Restart Web Services', + status: 'Success', + template: 'Service Management', + created: '2024-11-13 11:00:00', + duration: '0m 15s', + progress: 100, + user: 'operator', + }, + { + id: 5, + name: 'Configure Load Balancer', + status: 'Pending', + template: 'Network Configuration', + created: '2024-11-13 15:00:00', + duration: '0m 00s', + progress: 0, + user: 'admin', + }, + { + id: 6, + name: 'Infrastructure Scan', + status: 'Running', + template: 'Security Audit', + created: '2024-11-13 14:50:00', + duration: '3m 45s', + progress: 42, + user: 'security', + }, + { + id: 7, + name: 'Deploy Staging Environment', + status: 'Success', + template: 'Deploy Application', + created: '2024-11-13 10:30:00', + duration: '4m 12s', + progress: 100, + user: 'developer', + }, ]) + + const [page, setPage] = useState(1) + const [perPage, setPerPage] = useState(20) + + const StatusIndicator = ({ status }) => { + const statusConfig = { + 'Success': { icon: CheckCircleIcon, color: '#3e8635', label: 'Success' }, + 'Failed': { icon: ExclamationCircleIcon, color: '#c9190b', label: 'Failed' }, + 'Running': { icon: SyncAltIcon, color: '#0066cc', label: 'Running' }, + 'Pending': { icon: ClockIcon, color: '#f0ab00', label: 'Pending' }, + } + + const config = statusConfig[status] || statusConfig['Pending'] + const Icon = config.icon + + return ( + + + + + {config.label} + + ) + } - const tableColumns = [ + const tableColumns = useMemo(() => [ { - header: 'Name', - cell: (item) => item.name, + header: 'Job Name', + cell: (item) => ( + + ), sort: 'name', + card: 'name', + list: 'name', }, { header: 'Status', - cell: (item) => item.status, + cell: (item) => , sort: 'status', }, + { + header: 'Template', + cell: (item) => , + sort: 'template', + }, + { + header: 'Progress', + cell: (item) => { + let variant = ProgressVariant.success + if (item.status === 'Failed') variant = ProgressVariant.danger + else if (item.status === 'Running') variant = ProgressVariant.info + else if (item.status === 'Pending') variant = ProgressVariant.warning + + return ( + + ) + }, + }, + { + header: 'User', + cell: (item) => item.user, + sort: 'user', + }, + { + header: 'Duration', + cell: (item) => item.duration, + sort: 'duration', + }, { header: 'Created', cell: (item) => item.created, sort: 'created', + defaultSort: true, + defaultSortDirection: 'desc', + }, + ], []) + + const toolbarActions = useMemo(() => [ + { + type: 0, // PageActionType.Button + selection: 0, // PageActionSelection.None + variant: 'primary', + label: 'Launch job', + onClick: () => console.log('Launch new job'), + icon: PlayIcon, + }, + ], []) + + const rowActions = useMemo(() => [ + { + type: 0, // PageActionType.Button + selection: 1, // PageActionSelection.Single + label: 'View details', + onClick: (item) => console.log('View details:', item.name), + }, + { + type: 0, // PageActionType.Button + selection: 1, // PageActionSelection.Single + label: 'Relaunch', + onClick: (item) => { + const newJob = { + ...item, + id: Date.now(), + status: 'Running', + created: new Date().toLocaleString(), + duration: '0m 00s', + progress: 0, + } + setItems((prev) => [newJob, ...prev]) + console.log('Relaunched:', item.name) + }, + }, + { + type: 4, // PageActionType.Separator + }, + { + type: 0, // PageActionType.Button + selection: 1, // PageActionSelection.Single + label: 'Cancel job', + onClick: (item) => { + setItems((prev) => + prev.map((i) => + i.id === item.id && i.status === 'Running' + ? { ...i, status: 'Failed', progress: i.progress } + : i + ) + ) + console.log('Cancelled:', item.name) + }, + }, + { + type: 0, // PageActionType.Button + selection: 1, // PageActionSelection.Single + label: 'Delete', + onClick: (item) => { + setItems((prev) => prev.filter((i) => i.id !== item.id)) + console.log('Deleted:', item.name) + }, + isDanger: true, + }, + ], []) + + const bulkActions = useMemo(() => [ + { + type: 0, // PageActionType.Button + selection: 2, // PageActionSelection.Multiple + label: 'Cancel selected', + onClick: (selectedItems) => { + const itemIds = selectedItems.map(i => i.id) + setItems((prev) => + prev.map((i) => + itemIds.includes(i.id) && i.status === 'Running' + ? { ...i, status: 'Failed' } + : i + ) + ) + console.log('Cancelled selected jobs') + }, + }, + { + type: 4, // PageActionType.Separator + }, + { + type: 0, // PageActionType.Button + selection: 2, // PageActionSelection.Multiple + label: 'Delete selected', + onClick: (selectedItems) => { + const itemIds = selectedItems.map(i => i.id) + setItems((prev) => prev.filter((i) => !itemIds.includes(i.id))) + console.log('Deleted selected jobs') + }, + isDanger: true, + }, + ], []) + + const toolbarFilters = useMemo(() => [ + { + key: 'status', + label: 'Status', + type: 'select', + options: [ + { label: 'Success', value: 'Success' }, + { label: 'Failed', value: 'Failed' }, + { label: 'Running', value: 'Running' }, + { label: 'Pending', value: 'Pending' }, + ], + placeholder: 'Filter by status', + }, + { + key: 'user', + label: 'User', + type: 'select', + options: [ + { label: 'admin', value: 'admin' }, + { label: 'operator', value: 'operator' }, + { label: 'developer', value: 'developer' }, + { label: 'automation', value: 'automation' }, + { label: 'security', value: 'security' }, + ], + placeholder: 'Filter by user', + }, + { + key: 'name', + label: 'Name', + type: 'string', + placeholder: 'Filter by job name', }, - ] + ], []) return ( console.log('Launch job')}> + Launch job + + } /> item.id} tableColumns={tableColumns} + toolbarFilters={toolbarFilters} + toolbarActions={toolbarActions} + rowActions={rowActions} + bulkActions={bulkActions} itemCount={items.length} pageItems={items} - toolbarFilters={[]} + page={page} + setPage={setPage} + perPage={perPage} + setPerPage={setPerPage} emptyStateTitle="No automation jobs" errorStateTitle="Error loading automation jobs" + showSelect + isSelectMultiple /> )