This commit is contained in:
2025-08-27 00:53:35 -04:00
parent 2772dcc966
commit bc47279240
37 changed files with 0 additions and 36981 deletions

View File

@ -1 +0,0 @@
SKIP_PREFLIGHT_CHECK=true

1
web/.gitignore vendored
View File

@ -1 +0,0 @@
node_modules

View File

@ -1,23 +0,0 @@
import type { PackageType as PackageType_0,RemoteKeys as RemoteKeys_0 } from './kms/apis.d.ts';
declare module "@module-federation/runtime" {
type RemoteKeys = RemoteKeys_0;
type PackageType<T, Y=any> = T extends RemoteKeys_0 ? PackageType_0<T> :
Y ;
export function loadRemote<T extends RemoteKeys,Y>(packageName: T): Promise<PackageType<T, Y>>;
export function loadRemote<T extends string,Y>(packageName: T): Promise<PackageType<T, Y>>;
}
declare module "@module-federation/enhanced/runtime" {
type RemoteKeys = RemoteKeys_0;
type PackageType<T, Y=any> = T extends RemoteKeys_0 ? PackageType_0<T> :
Y ;
export function loadRemote<T extends RemoteKeys,Y>(packageName: T): Promise<PackageType<T, Y>>;
export function loadRemote<T extends string,Y>(packageName: T): Promise<PackageType<T, Y>>;
}
declare module "@module-federation/runtime-tools" {
type RemoteKeys = RemoteKeys_0;
type PackageType<T, Y=any> = T extends RemoteKeys_0 ? PackageType_0<T> :
Y ;
export function loadRemote<T extends RemoteKeys,Y>(packageName: T): Promise<PackageType<T, Y>>;
export function loadRemote<T extends string,Y>(packageName: T): Promise<PackageType<T, Y>>;
}

View File

@ -1,2 +0,0 @@
export * from './compiled-types/federated/KMSApp';
export { default } from './compiled-types/federated/KMSApp';

View File

@ -1,2 +0,0 @@
export * from './compiled-types/federated/SearchProvider';
export { default } from './compiled-types/federated/SearchProvider';

View File

@ -1,3 +0,0 @@
export type RemoteKeys = 'kms/App' | 'kms/SearchProvider';
type PackageType<T> = T extends 'kms/SearchProvider' ? typeof import('kms/SearchProvider') :T extends 'kms/App' ? typeof import('kms/App') :any;

View File

@ -1,3 +0,0 @@
import React from 'react';
declare const Applications: React.FC;
export default Applications;

View File

@ -1,3 +0,0 @@
import React from 'react';
declare const Audit: React.FC;
export default Audit;

View File

@ -1,3 +0,0 @@
import React from 'react';
declare const Dashboard: React.FC;
export default Dashboard;

View File

@ -1,3 +0,0 @@
import React from 'react';
declare const Login: React.FC;
export default Login;

View File

@ -1,3 +0,0 @@
import React from 'react';
declare const TokenTester: React.FC;
export default TokenTester;

View File

@ -1,3 +0,0 @@
import React from 'react';
declare const TokenTesterCallback: React.FC;
export default TokenTesterCallback;

View File

@ -1,3 +0,0 @@
import React from 'react';
declare const Tokens: React.FC;
export default Tokens;

View File

@ -1,3 +0,0 @@
import React from 'react';
declare const Users: React.FC;
export default Users;

View File

@ -1,17 +0,0 @@
import React, { ReactNode } from 'react';
interface User {
email: string;
permissions: string[];
}
interface AuthContextType {
user: User | null;
login: (email: string) => Promise<boolean>;
logout: () => void;
loading: boolean;
}
export declare const useAuth: () => AuthContextType;
interface AuthProviderProps {
children: ReactNode;
}
export declare const AuthProvider: React.FC<AuthProviderProps>;
export {};

View File

@ -1,3 +0,0 @@
import React from 'react';
declare const KMSApp: React.FC;
export default KMSApp;

View File

@ -1,9 +0,0 @@
interface SearchResult {
id: string;
title: string;
description?: string;
appId: string;
action?: () => void;
}
export declare const kmsSearchProvider: (query: string) => Promise<SearchResult[]>;
export default kmsSearchProvider;

View File

@ -1,152 +0,0 @@
export interface Application {
app_id: string;
app_link: string;
type: string[];
callback_url: string;
hmac_key: string;
token_prefix?: string;
token_renewal_duration: number;
max_token_duration: number;
owner: {
type: string;
name: string;
owner: string;
};
created_at: string;
updated_at: string;
}
export interface StaticToken {
id: string;
app_id: string;
owner: {
type: string;
name: string;
owner: string;
};
type: string;
created_at: string;
updated_at: string;
}
export interface CreateApplicationRequest {
app_id: string;
app_link: string;
type: string[];
callback_url: string;
token_prefix?: string;
token_renewal_duration: string;
max_token_duration: string;
owner: {
type: string;
name: string;
owner: string;
};
}
export interface CreateTokenRequest {
owner: {
type: string;
name: string;
owner: string;
};
permissions: string[];
}
export interface CreateTokenResponse {
id: string;
token: string;
permissions: string[];
created_at: string;
}
export interface PaginatedResponse<T> {
data: T[];
limit: number;
offset: number;
count: number;
}
export interface VerifyRequest {
app_id: string;
user_id?: string;
token: string;
permissions?: string[];
}
export interface VerifyResponse {
valid: boolean;
permitted: boolean;
user_id?: string;
permissions: string[];
permission_results?: Record<string, boolean>;
expires_at?: string;
max_valid_at?: string;
token_type: string;
claims?: Record<string, string>;
error?: string;
}
export interface AuditEvent {
id: string;
type: string;
status: string;
timestamp: string;
actor_id?: string;
actor_ip?: string;
user_agent?: string;
resource_id?: string;
resource_type?: string;
action: string;
description: string;
details?: Record<string, any>;
request_id?: string;
session_id?: string;
}
export interface AuditQueryParams {
event_types?: string[];
statuses?: string[];
actor_id?: string;
resource_id?: string;
resource_type?: string;
start_time?: string;
end_time?: string;
limit?: number;
offset?: number;
order_by?: string;
order_desc?: boolean;
}
export interface AuditResponse {
events: AuditEvent[];
total: number;
limit: number;
offset: number;
}
export interface AuditStats {
total_events: number;
by_type: Record<string, number>;
by_severity: Record<string, number>;
by_status: Record<string, number>;
by_time?: Record<string, number>;
}
export interface AuditStatsParams {
event_types?: string[];
start_time?: string;
end_time?: string;
group_by?: string;
}
declare class ApiService {
private api;
private baseURL;
constructor();
healthCheck(): Promise<any>;
readinessCheck(): Promise<any>;
getApplications(limit?: number, offset?: number): Promise<PaginatedResponse<Application>>;
getApplication(appId: string): Promise<Application>;
createApplication(data: CreateApplicationRequest): Promise<Application>;
updateApplication(appId: string, data: Partial<CreateApplicationRequest>): Promise<Application>;
deleteApplication(appId: string): Promise<void>;
getTokensForApplication(appId: string, limit?: number, offset?: number): Promise<PaginatedResponse<StaticToken>>;
createToken(appId: string, data: CreateTokenRequest): Promise<CreateTokenResponse>;
deleteToken(tokenId: string): Promise<void>;
verifyToken(data: VerifyRequest): Promise<VerifyResponse>;
login(appId: string, permissions: string[], redirectUri?: string, tokenDelivery?: string): Promise<any>;
renewToken(appId: string, userId: string, token: string): Promise<any>;
getAuditEvents(params?: AuditQueryParams): Promise<AuditResponse>;
getAuditEvent(eventId: string): Promise<AuditEvent>;
getAuditStats(params?: AuditStatsParams): Promise<AuditStats>;
}
export declare const apiService: ApiService;
export {};

View File

@ -1,179 +0,0 @@
# Skybridge Platform Shell
A module federated React platform that provides a unified shell for multiple applications, similar to AWS Console.
## Features
- **Module Federation**: Apps are loaded as federated modules
- **Tab-based Navigation**: Each application appears as a tab
- **Global Search**: Federated search across all applications
- **Timezone Clocks**: PST, EST, CST, and IST clocks in the header
- **User Management**: User dropdown with logout functionality
- **Extensible**: Easy to add new federated applications
## Getting Started
### Prerequisites
- Node.js 24+
- npm 11+
### Installation
```bash
cd web
npm install
```
### Development
Start the shell (runs on port 3000):
```bash
npm start
```
Start the KMS federated app (runs on port 3001):
```bash
cd ../kms/kms-frontend
npm install
npm start
```
## Architecture
### Shell Application (`web/`)
- **Port**: 3000
- **Role**: Host application that loads federated modules
- **Components**:
- `ShellHeader`: Global navigation with search and timezone clocks
- `AppContainer`: Tab-based container for federated applications
- `GlobalSearch`: Federated search across all applications
- `TimeZoneClock`: Multi-timezone display
- `UserDropdown`: User management interface
### KMS Application (`kms/kms-frontend/`)
- **Port**: 3001
- **Role**: Federated module providing KMS functionality
- **Exposes**:
- `./App`: Main KMS application component
- `./SearchProvider`: Search functionality for KMS data
## Adding New Applications
To add a new federated application:
1. **Create the application** with module federation configuration
2. **Expose components** in `craco.config.js`:
```javascript
exposes: {
'./App': './src/YourApp',
'./SearchProvider': './src/SearchProvider', // Optional
}
```
3. **Update shell configuration** in `web/craco.config.js`:
```javascript
remotes: {
kms: 'kms@http://localhost:3001/remoteEntry.js',
yourapp: 'yourapp@http://localhost:3002/remoteEntry.js',
}
```
4. **Create a loader component** similar to `LoadKMS.tsx`:
```typescript
const YourApp = React.lazy(() => import('yourapp/App'));
const LoadYourApp: React.FC = () => {
const { registerApp } = useApp();
useEffect(() => {
registerApp({
id: 'yourapp',
name: 'Your App',
path: '/yourapp',
component: YourApp,
searchProvider: yourSearchProvider, // Optional
});
}, [registerApp]);
return null;
};
```
5. **Include the loader** in the main App component
## Search Integration
Applications can provide search functionality by exposing a search provider:
```typescript
export const yourSearchProvider = async (query: string): Promise<SearchResult[]> => {
// Implement your search logic
return [
{
id: 'result-1',
title: 'Search Result',
description: 'Description of the result',
appId: 'Your App',
action: () => {
// Navigate to result
}
}
];
};
```
## Configuration
### Ports
- Shell: 3000
- KMS: 3001
- Future apps: 3002+
### Environment
The shell includes:
- **React 19** with TypeScript
- **Ant Design 5.27** for UI components
- **Module Federation** for app loading
- **React Router** for routing
- **Moment Timezone** for clock functionality
## File Structure
```
web/
├── src/
│ ├── components/
│ │ ├── AppContainer.tsx # Tab container
│ │ ├── GlobalSearch.tsx # Federated search
│ │ ├── ShellHeader.tsx # Main header
│ │ ├── TimeZoneClock.tsx # Timezone display
│ │ ├── UserDropdown.tsx # User menu
│ │ └── LoadKMS.tsx # KMS app loader
│ ├── contexts/
│ │ └── AppContext.tsx # Global app state
│ ├── types/
│ │ ├── index.ts # Type definitions
│ │ └── federated.d.ts # Module federation types
│ ├── App.tsx # Main app component
│ └── index.tsx # Entry point
├── public/
├── craco.config.js # Module federation config
├── package.json
└── README.md
```
## Development Notes
- Applications run on separate ports and are loaded dynamically
- Shared dependencies (React, Ant Design) are singleton across all apps
- Each application maintains its own routing under its path prefix
- Global search aggregates results from all registered applications
- User state is managed globally in the shell

View File

@ -1,49 +0,0 @@
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
devServer: {
port: 3000,
historyApiFallback: true,
},
webpack: {
plugins: {
add: [
new ModuleFederationPlugin({
name: 'shell',
filename: 'remoteEntry.js',
remotes: {
kms: 'kms@http://localhost:3001/remoteEntry.js',
},
shared: {
react: {
singleton: true,
requiredVersion: '^19.1.1'
},
'react-dom': {
singleton: true,
requiredVersion: '^19.1.1'
},
'react-router-dom': {
singleton: true,
requiredVersion: '^7.8.2'
},
antd: {
singleton: true,
requiredVersion: '^5.27.1'
},
axios: {
singleton: true
}
},
}),
],
},
configure: (webpackConfig) => ({
...webpackConfig,
output: {
...webpackConfig.output,
publicPath: "auto",
},
}),
},
};

35751
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +0,0 @@
{
"name": "skybridge-shell",
"version": "0.1.0",
"private": true,
"dependencies": {
"@ant-design/icons": "^6.0.0",
"@types/react": "^19.1.11",
"@types/react-dom": "^19.1.7",
"antd": "^5.27.1",
"axios": "^1.11.0",
"moment-timezone": "^0.5.45",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.8.2",
"typescript": "^4.9.5"
},
"devDependencies": {
"@craco/craco": "^7.1.0",
"@types/moment-timezone": "^0.5.30",
"craco-module-federation": "^1.1.0",
"webpack": "^5.96.1"
},
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "craco eject"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Skybridge Cloud Platform"
/>
<title>Skybridge Platform</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@ -1,36 +0,0 @@
import React, { useEffect } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { Layout } from 'antd';
import ShellHeader from './components/ShellHeader';
import AppContainer from './components/AppContainer';
import LoadKMS from './components/LoadKMS';
import { useApp } from './contexts/AppContext';
const { Content } = Layout;
const App: React.FC = () => {
const { setUser } = useApp();
useEffect(() => {
setUser({
email: 'admin@example.com',
name: 'Admin User',
});
}, [setUser]);
return (
<Layout className="shell-layout">
<LoadKMS />
<ShellHeader />
<Content className="shell-content">
<Routes>
<Route path="/kms/*" element={<AppContainer />} />
<Route path="/" element={<Navigate to="/kms" replace />} />
<Route path="*" element={<AppContainer />} />
</Routes>
</Content>
</Layout>
);
};
export default App;

View File

@ -1,23 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { ConfigProvider } from 'antd';
import App from './App';
import { AppProvider } from './contexts/AppContext';
import './index.css';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<BrowserRouter>
<ConfigProvider>
<AppProvider>
<App />
</AppProvider>
</ConfigProvider>
</BrowserRouter>
</React.StrictMode>
);

View File

@ -1,96 +0,0 @@
import React, { Suspense, useEffect } from 'react';
import { Tabs, Spin } from 'antd';
import { useApp } from '../contexts/AppContext';
import { FederatedApp } from '../types';
import { useLocation, useNavigate } from 'react-router-dom';
const AppContainer: React.FC = () => {
const { apps, activeAppId, setActiveApp } = useApp();
const location = useLocation();
const navigate = useNavigate();
// Auto-detect active app based on current URL path
useEffect(() => {
if (apps.length > 0) {
const currentPath = location.pathname;
// Find app that matches the current path
const matchingApp = apps.find(app => currentPath.startsWith(app.path));
if (matchingApp && activeAppId !== matchingApp.id) {
setActiveApp(matchingApp.id);
} else if (!activeAppId && !matchingApp) {
// Default to first app if no match
setActiveApp(apps[0].id);
navigate(apps[0].path, { replace: true });
}
}
}, [location.pathname, apps, activeAppId, setActiveApp, navigate]);
if (apps.length === 0) {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
fontSize: '16px',
color: '#666'
}}>
No applications available
</div>
);
}
const handleTabChange = (key: string) => {
const selectedApp = apps.find(app => app.id === key);
if (selectedApp) {
setActiveApp(key);
navigate(selectedApp.path);
}
};
const renderAppContent = (app: FederatedApp) => {
const AppComponent = app.component;
return (
<Suspense
fallback={
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '400px'
}}>
<Spin size="large" />
</div>
}
>
<div style={{ height: '100%', overflow: 'auto' }}>
<AppComponent />
</div>
</Suspense>
);
};
const tabItems = apps.map(app => ({
key: app.id,
label: app.name,
children: renderAppContent(app)
}));
return (
<div style={{ height: '100%', padding: '0' }}>
<Tabs
activeKey={activeAppId || (apps.length > 0 ? apps[0].id : undefined)}
onChange={handleTabChange}
className="app-tabs"
type="card"
style={{ height: '100%' }}
items={tabItems}
tabBarStyle={{ margin: 0, padding: '0 16px' }}
/>
</div>
);
};
export default AppContainer;

View File

@ -1,98 +0,0 @@
import React, { useState, useCallback } from 'react';
import { Input, Dropdown, Spin, Empty, Typography } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import { useApp } from '../contexts/AppContext';
import { SearchResult } from '../types';
const { Text } = Typography;
const GlobalSearch: React.FC = () => {
const [query, setQuery] = useState('');
const [isOpen, setIsOpen] = useState(false);
const { searchResults, isSearching, performSearch, clearSearch } = useApp();
const handleSearch = useCallback(
async (value: string) => {
setQuery(value);
if (value.trim()) {
await performSearch(value);
setIsOpen(true);
} else {
clearSearch();
setIsOpen(false);
}
},
[performSearch, clearSearch]
);
const handleResultClick = (result: SearchResult) => {
if (result.action) {
result.action();
}
setIsOpen(false);
setQuery('');
clearSearch();
};
const searchContent = (
<div className="search-dropdown" style={{ minWidth: 400 }}>
{isSearching ? (
<div style={{ padding: '20px', textAlign: 'center' }}>
<Spin />
</div>
) : searchResults.length > 0 ? (
searchResults.map((result) => (
<div
key={result.id}
className="search-result-item"
onClick={() => handleResultClick(result)}
style={{ cursor: 'pointer' }}
>
<div className="search-result-title">{result.title}</div>
{result.description && (
<div className="search-result-description">{result.description}</div>
)}
<div className="search-result-app">
<Text type="secondary">{result.appId}</Text>
</div>
</div>
))
) : query ? (
<div style={{ padding: '20px' }}>
<Empty
description="No results found"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
</div>
) : null}
</div>
);
return (
<Dropdown
open={isOpen && (isSearching || searchResults.length > 0 || query.length > 0)}
onOpenChange={setIsOpen}
dropdownRender={() => searchContent}
trigger={['click']}
placement="bottomLeft"
>
<Input
size="middle"
placeholder="Search across all applications..."
prefix={<SearchOutlined />}
value={query}
onChange={(e) => handleSearch(e.target.value)}
onPressEnter={() => handleSearch(query)}
style={{ width: 400 }}
allowClear
onClear={() => {
setQuery('');
clearSearch();
setIsOpen(false);
}}
/>
</Dropdown>
);
};
export default GlobalSearch;

View File

@ -1,66 +0,0 @@
import React, { useEffect, useRef } from 'react';
import { useApp } from '../contexts/AppContext';
const LoadKMS: React.FC = () => {
const { registerApp } = useApp();
const registeredRef = useRef(false);
useEffect(() => {
const loadKMS = async () => {
if (registeredRef.current) return;
try {
console.log('Loading KMS app...');
// Dynamically import the KMS app
const KMSApp = React.lazy(() => import('kms/App'));
// Try to load search provider separately
let kmsSearchProvider;
try {
const SearchModule = await import('kms/SearchProvider');
kmsSearchProvider = SearchModule.kmsSearchProvider;
} catch (searchError) {
console.warn('Failed to load KMS search provider:', searchError);
kmsSearchProvider = undefined;
}
registerApp({
id: 'kms',
name: 'KMS',
path: '/kms',
component: KMSApp,
searchProvider: kmsSearchProvider,
});
registeredRef.current = true;
console.log('KMS app loaded successfully');
} catch (error) {
console.error('Failed to load KMS app:', error);
// Try to register with just basic import as fallback
try {
const KMSApp = React.lazy(() => import('kms/App'));
registerApp({
id: 'kms',
name: 'KMS',
path: '/kms',
component: KMSApp,
});
registeredRef.current = true;
console.log('KMS app loaded with fallback');
} catch (fallbackError) {
console.error('Complete KMS loading failure:', fallbackError);
}
}
};
loadKMS();
}, [registerApp]);
return null;
};
export default LoadKMS;

View File

@ -1,41 +0,0 @@
import React from 'react';
import { Layout, Space, Typography } from 'antd';
import GlobalSearch from './GlobalSearch';
import TimeZoneClock from './TimeZoneClock';
import UserDropdown from './UserDropdown';
const { Header } = Layout;
const { Title } = Typography;
const ShellHeader: React.FC = () => {
return (
<Header className="shell-header" style={{
padding: '0 24px',
background: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
borderBottom: '1px solid #f0f0f0'
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Title level={4} style={{ margin: 0, marginRight: 32 }}>
Skybridge Platform
</Title>
<GlobalSearch />
</div>
<Space size="large">
<div className="timezone-clocks">
<TimeZoneClock timezone="America/Los_Angeles" label="PST" />
<TimeZoneClock timezone="America/New_York" label="EST" />
<TimeZoneClock timezone="America/Chicago" label="CST" />
<TimeZoneClock timezone="Asia/Kolkata" label="IST" />
</div>
<UserDropdown />
</Space>
</Header>
);
};
export default ShellHeader;

View File

@ -1,28 +0,0 @@
import React, { useState, useEffect } from 'react';
import moment from 'moment-timezone';
import { TimeZoneClockProps } from '../types';
const TimeZoneClock: React.FC<TimeZoneClockProps> = ({ timezone, label }) => {
const [time, setTime] = useState(moment.tz(timezone));
useEffect(() => {
const interval = setInterval(() => {
setTime(moment.tz(timezone));
}, 1000);
return () => clearInterval(interval);
}, [timezone]);
return (
<div className="timezone-clock">
<div className="timezone-clock-time">
{time.format('HH:mm')}
</div>
<div className="timezone-clock-label">
{label}
</div>
</div>
);
};
export default TimeZoneClock;

View File

@ -1,44 +0,0 @@
import React from 'react';
import { Dropdown, Avatar, Typography, Space } from 'antd';
import { UserOutlined, LogoutOutlined } from '@ant-design/icons';
import type { MenuProps } from 'antd';
import { useApp } from '../contexts/AppContext';
const { Text } = Typography;
const UserDropdown: React.FC = () => {
const { user, logout } = useApp();
if (!user) return null;
const menuItems: MenuProps['items'] = [
{
key: 'logout',
icon: <LogoutOutlined />,
label: 'Logout',
onClick: logout,
},
];
return (
<Dropdown menu={{ items: menuItems }} placement="bottomRight">
<Space style={{ cursor: 'pointer' }}>
<Avatar
size="small"
icon={<UserOutlined />}
src={user.avatar}
/>
<div style={{ textAlign: 'left' }}>
<div>{user.name || user.email}</div>
{user.name && (
<Text type="secondary" style={{ fontSize: '12px' }}>
{user.email}
</Text>
)}
</div>
</Space>
</Dropdown>
);
};
export default UserDropdown;

View File

@ -1,108 +0,0 @@
import React, { createContext, useContext, useState, ReactNode } from 'react';
import { FederatedApp, User, SearchResult } from '../types';
interface AppContextType {
user: User | null;
apps: FederatedApp[];
activeAppId: string | null;
searchResults: SearchResult[];
isSearching: boolean;
setUser: (user: User | null) => void;
registerApp: (app: FederatedApp) => void;
setActiveApp: (appId: string | null) => void;
performSearch: (query: string) => Promise<void>;
clearSearch: () => void;
logout: () => void;
}
const AppContext = createContext<AppContextType | undefined>(undefined);
interface AppProviderProps {
children: ReactNode;
}
export const AppProvider: React.FC<AppProviderProps> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [apps, setApps] = useState<FederatedApp[]>([]);
const [activeAppId, setActiveAppId] = useState<string | null>(null);
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
const [isSearching, setIsSearching] = useState(false);
const registerApp = (app: FederatedApp) => {
setApps(prev => {
const exists = prev.find(a => a.id === app.id);
if (exists) return prev;
return [...prev, app];
});
};
const setActiveApp = (appId: string | null) => {
setActiveAppId(appId);
};
const performSearch = async (query: string) => {
if (!query.trim()) {
setSearchResults([]);
return;
}
setIsSearching(true);
const allResults: SearchResult[] = [];
try {
await Promise.all(
apps.map(async (app) => {
if (app.searchProvider) {
try {
const results = await app.searchProvider(query);
allResults.push(...results);
} catch (error) {
console.error(`Search failed for app ${app.id}:`, error);
}
}
})
);
setSearchResults(allResults);
} finally {
setIsSearching(false);
}
};
const clearSearch = () => {
setSearchResults([]);
};
const logout = () => {
setUser(null);
setActiveAppId(null);
};
return (
<AppContext.Provider
value={{
user,
apps,
activeAppId,
searchResults,
isSearching,
setUser,
registerApp,
setActiveApp,
performSearch,
clearSearch,
logout,
}}
>
{children}
</AppContext.Provider>
);
};
export const useApp = (): AppContextType => {
const context = useContext(AppContext);
if (context === undefined) {
throw new Error('useApp must be used within an AppProvider');
}
return context;
};

View File

@ -1,101 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
* {
box-sizing: border-box;
}
#root {
height: 100vh;
display: flex;
flex-direction: column;
}
.shell-layout {
display: flex;
flex-direction: column;
height: 100vh;
}
.shell-header {
flex: 0 0 auto;
border-bottom: 1px solid #f0f0f0;
background: #fff;
z-index: 1000;
}
.shell-content {
flex: 1;
overflow: hidden;
}
.timezone-clocks {
display: flex;
gap: 24px;
align-items: center;
}
.timezone-clock {
text-align: center;
font-size: 12px;
}
.timezone-clock-time {
font-weight: 600;
font-size: 14px;
margin-bottom: 2px;
}
.timezone-clock-label {
color: #666;
font-size: 11px;
}
.search-dropdown {
max-height: 400px;
overflow-y: auto;
}
.search-result-item {
padding: 8px 12px;
border-bottom: 1px solid #f0f0f0;
}
.search-result-item:last-child {
border-bottom: none;
}
.search-result-title {
font-weight: 500;
margin-bottom: 4px;
}
.search-result-description {
font-size: 12px;
color: #666;
}
.search-result-app {
font-size: 11px;
color: #999;
margin-top: 4px;
}
.app-tabs .ant-tabs-content-holder {
padding: 0;
}
.app-tabs .ant-tabs-tabpane {
height: 100%;
}

View File

@ -1,3 +0,0 @@
import('./bootstrap');
export {};

View File

@ -1,10 +0,0 @@
declare module 'kms/App' {
import React from 'react';
const KMSApp: React.ComponentType;
export default KMSApp;
}
declare module 'kms/SearchProvider' {
import { SearchResult } from './index';
export const kmsSearchProvider: (query: string) => Promise<SearchResult[]>;
}

View File

@ -1,26 +0,0 @@
export interface FederatedApp {
id: string;
name: string;
path: string;
component: React.ComponentType;
searchProvider?: (query: string) => Promise<SearchResult[]>;
}
export interface SearchResult {
id: string;
title: string;
description?: string;
appId: string;
action?: () => void;
}
export interface User {
email: string;
name?: string;
avatar?: string;
}
export interface TimeZoneClockProps {
timezone: string;
label: string;
}

View File

@ -1,26 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}