module federation
This commit is contained in:
@ -5,3 +5,4 @@ REACT_APP_VERSION=1.0.0
|
||||
|
||||
# Development settings
|
||||
GENERATE_SOURCEMAP=true
|
||||
SKIP_PREFLIGHT_CHECK=true
|
||||
|
||||
1
kms/kms-frontend/.gitignore
vendored
1
kms/kms-frontend/.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
dist
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
|
||||
50
kms/kms-frontend/craco.config.js
Normal file
50
kms/kms-frontend/craco.config.js
Normal file
@ -0,0 +1,50 @@
|
||||
const { ModuleFederationPlugin } = require("webpack").container;
|
||||
|
||||
module.exports = {
|
||||
devServer: {
|
||||
port: 3001,
|
||||
historyApiFallback: true,
|
||||
},
|
||||
webpack: {
|
||||
plugins: {
|
||||
add: [
|
||||
new ModuleFederationPlugin({
|
||||
name: 'kms',
|
||||
filename: 'remoteEntry.js',
|
||||
exposes: {
|
||||
'./App': './src/federated/KMSApp',
|
||||
'./SearchProvider': './src/federated/SearchProvider',
|
||||
},
|
||||
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",
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
14880
kms/kms-frontend/package-lock.json
generated
14880
kms/kms-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -23,11 +23,16 @@
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@craco/craco": "^7.1.0",
|
||||
"craco-module-federation": "^1.1.0",
|
||||
"webpack": "^5.96.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
"start": "craco start",
|
||||
"build": "craco build",
|
||||
"test": "craco test",
|
||||
"eject": "craco eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
||||
24
kms/kms-frontend/src/bootstrap.tsx
Normal file
24
kms/kms-frontend/src/bootstrap.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { ConfigProvider, theme } from 'antd';
|
||||
import App from './App';
|
||||
import './App.css';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
algorithm: theme.defaultAlgorithm,
|
||||
}}
|
||||
>
|
||||
<Router>
|
||||
<App />
|
||||
</Router>
|
||||
</ConfigProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
120
kms/kms-frontend/src/federated/KMSApp.tsx
Normal file
120
kms/kms-frontend/src/federated/KMSApp.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import React from 'react';
|
||||
import { Routes, Route, Navigate, useNavigate } from 'react-router-dom';
|
||||
import { Layout, Menu, theme } from 'antd';
|
||||
import {
|
||||
DashboardOutlined,
|
||||
AppstoreOutlined,
|
||||
KeyOutlined,
|
||||
UserOutlined,
|
||||
AuditOutlined,
|
||||
ExperimentOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import Dashboard from '../components/Dashboard';
|
||||
import Applications from '../components/Applications';
|
||||
import Tokens from '../components/Tokens';
|
||||
import Users from '../components/Users';
|
||||
import Audit from '../components/Audit';
|
||||
import TokenTester from '../components/TokenTester';
|
||||
import TokenTesterCallback from '../components/TokenTesterCallback';
|
||||
import { AuthProvider, useAuth } from '../contexts/AuthContext';
|
||||
import Login from '../components/Login';
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
|
||||
const KMSAppContent: React.FC = () => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
token: { colorBgContainer, borderRadiusLG },
|
||||
} = theme.useToken();
|
||||
|
||||
if (!user) {
|
||||
return <Login />;
|
||||
}
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
key: '/kms',
|
||||
icon: <DashboardOutlined />,
|
||||
label: 'Dashboard',
|
||||
},
|
||||
{
|
||||
key: '/kms/applications',
|
||||
icon: <AppstoreOutlined />,
|
||||
label: 'Applications',
|
||||
},
|
||||
{
|
||||
key: '/kms/tokens',
|
||||
icon: <KeyOutlined />,
|
||||
label: 'Tokens',
|
||||
},
|
||||
{
|
||||
key: '/kms/token-tester',
|
||||
icon: <ExperimentOutlined />,
|
||||
label: 'Token Tester',
|
||||
},
|
||||
{
|
||||
key: '/kms/users',
|
||||
icon: <UserOutlined />,
|
||||
label: 'Users',
|
||||
},
|
||||
{
|
||||
key: '/kms/audit',
|
||||
icon: <AuditOutlined />,
|
||||
label: 'Audit Log',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Layout style={{ height: '100%', minHeight: '600px' }}>
|
||||
<Sider trigger={null} collapsible collapsed={collapsed} width={200}>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
defaultSelectedKeys={['/kms']}
|
||||
items={menuItems}
|
||||
onClick={({ key }) => {
|
||||
navigate(key);
|
||||
}}
|
||||
style={{ height: '100%', borderRight: 0 }}
|
||||
/>
|
||||
</Sider>
|
||||
<Layout style={{ background: colorBgContainer }}>
|
||||
<Content
|
||||
style={{
|
||||
margin: '16px',
|
||||
padding: 24,
|
||||
minHeight: 280,
|
||||
background: colorBgContainer,
|
||||
borderRadius: borderRadiusLG,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/kms" element={<Dashboard />} />
|
||||
<Route path="/kms/applications" element={<Applications />} />
|
||||
<Route path="/kms/tokens" element={<Tokens />} />
|
||||
<Route path="/kms/token-tester" element={<TokenTester />} />
|
||||
<Route path="/kms/token-tester/callback" element={<TokenTesterCallback />} />
|
||||
<Route path="/kms/users" element={<Users />} />
|
||||
<Route path="/kms/audit" element={<Audit />} />
|
||||
<Route path="*" element={<Navigate to="/kms" replace />} />
|
||||
</Routes>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
const KMSApp: React.FC = () => {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<KMSAppContent />
|
||||
</AuthProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default KMSApp;
|
||||
64
kms/kms-frontend/src/federated/SearchProvider.tsx
Normal file
64
kms/kms-frontend/src/federated/SearchProvider.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { apiService, Application, PaginatedResponse } from '../services/apiService';
|
||||
|
||||
interface SearchResult {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
appId: string;
|
||||
action?: () => void;
|
||||
}
|
||||
|
||||
export const kmsSearchProvider = async (query: string): Promise<SearchResult[]> => {
|
||||
const results: SearchResult[] = [];
|
||||
|
||||
try {
|
||||
// Search applications
|
||||
const applicationsResponse: PaginatedResponse<Application> = await apiService.getApplications();
|
||||
applicationsResponse.data
|
||||
.filter((app: Application) =>
|
||||
app.app_id.toLowerCase().includes(query.toLowerCase()) ||
|
||||
(app.owner?.name && app.owner.name.toLowerCase().includes(query.toLowerCase()))
|
||||
)
|
||||
.forEach((app: Application) => {
|
||||
results.push({
|
||||
id: `app-${app.app_id}`,
|
||||
title: app.app_id,
|
||||
description: `Application owned by ${app.owner?.name || 'Unknown'}`,
|
||||
appId: 'KMS',
|
||||
action: () => {
|
||||
window.location.hash = '/kms/applications';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add quick actions
|
||||
const quickActions = [
|
||||
{ key: 'applications', title: 'Applications', path: '/kms/applications' },
|
||||
{ key: 'tokens', title: 'Tokens', path: '/kms/tokens' },
|
||||
{ key: 'users', title: 'Users', path: '/kms/users' },
|
||||
{ key: 'audit', title: 'Audit Log', path: '/kms/audit' },
|
||||
{ key: 'dashboard', title: 'Dashboard', path: '/kms' },
|
||||
];
|
||||
|
||||
quickActions
|
||||
.filter(action => action.title.toLowerCase().includes(query.toLowerCase()))
|
||||
.forEach(action => {
|
||||
results.push({
|
||||
id: `quick-${action.key}`,
|
||||
title: action.title,
|
||||
description: `Navigate to ${action.title}`,
|
||||
appId: 'KMS',
|
||||
action: () => {
|
||||
window.location.hash = action.path;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('KMS search error:', error);
|
||||
}
|
||||
|
||||
return results.slice(0, 10); // Limit results
|
||||
};
|
||||
|
||||
export default kmsSearchProvider;
|
||||
@ -1,19 +1,3 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import('./bootstrap');
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
export {};
|
||||
|
||||
1
web/.gitignore
vendored
Normal file
1
web/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
||||
23
web/@mf-types/index.d.ts
vendored
Normal file
23
web/@mf-types/index.d.ts
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
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>>;
|
||||
}
|
||||
|
||||
2
web/@mf-types/kms/App.d.ts
vendored
Normal file
2
web/@mf-types/kms/App.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './compiled-types/federated/KMSApp';
|
||||
export { default } from './compiled-types/federated/KMSApp';
|
||||
2
web/@mf-types/kms/SearchProvider.d.ts
vendored
Normal file
2
web/@mf-types/kms/SearchProvider.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './compiled-types/federated/SearchProvider';
|
||||
export { default } from './compiled-types/federated/SearchProvider';
|
||||
3
web/@mf-types/kms/apis.d.ts
vendored
Normal file
3
web/@mf-types/kms/apis.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
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;
|
||||
3
web/@mf-types/kms/compiled-types/components/Applications.d.ts
vendored
Normal file
3
web/@mf-types/kms/compiled-types/components/Applications.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import React from 'react';
|
||||
declare const Applications: React.FC;
|
||||
export default Applications;
|
||||
3
web/@mf-types/kms/compiled-types/components/Audit.d.ts
vendored
Normal file
3
web/@mf-types/kms/compiled-types/components/Audit.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import React from 'react';
|
||||
declare const Audit: React.FC;
|
||||
export default Audit;
|
||||
3
web/@mf-types/kms/compiled-types/components/Dashboard.d.ts
vendored
Normal file
3
web/@mf-types/kms/compiled-types/components/Dashboard.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import React from 'react';
|
||||
declare const Dashboard: React.FC;
|
||||
export default Dashboard;
|
||||
3
web/@mf-types/kms/compiled-types/components/Login.d.ts
vendored
Normal file
3
web/@mf-types/kms/compiled-types/components/Login.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import React from 'react';
|
||||
declare const Login: React.FC;
|
||||
export default Login;
|
||||
3
web/@mf-types/kms/compiled-types/components/TokenTester.d.ts
vendored
Normal file
3
web/@mf-types/kms/compiled-types/components/TokenTester.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import React from 'react';
|
||||
declare const TokenTester: React.FC;
|
||||
export default TokenTester;
|
||||
3
web/@mf-types/kms/compiled-types/components/TokenTesterCallback.d.ts
vendored
Normal file
3
web/@mf-types/kms/compiled-types/components/TokenTesterCallback.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import React from 'react';
|
||||
declare const TokenTesterCallback: React.FC;
|
||||
export default TokenTesterCallback;
|
||||
3
web/@mf-types/kms/compiled-types/components/Tokens.d.ts
vendored
Normal file
3
web/@mf-types/kms/compiled-types/components/Tokens.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import React from 'react';
|
||||
declare const Tokens: React.FC;
|
||||
export default Tokens;
|
||||
3
web/@mf-types/kms/compiled-types/components/Users.d.ts
vendored
Normal file
3
web/@mf-types/kms/compiled-types/components/Users.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import React from 'react';
|
||||
declare const Users: React.FC;
|
||||
export default Users;
|
||||
17
web/@mf-types/kms/compiled-types/contexts/AuthContext.d.ts
vendored
Normal file
17
web/@mf-types/kms/compiled-types/contexts/AuthContext.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
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 {};
|
||||
3
web/@mf-types/kms/compiled-types/federated/KMSApp.d.ts
vendored
Normal file
3
web/@mf-types/kms/compiled-types/federated/KMSApp.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import React from 'react';
|
||||
declare const KMSApp: React.FC;
|
||||
export default KMSApp;
|
||||
9
web/@mf-types/kms/compiled-types/federated/SearchProvider.d.ts
vendored
Normal file
9
web/@mf-types/kms/compiled-types/federated/SearchProvider.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
interface SearchResult {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
appId: string;
|
||||
action?: () => void;
|
||||
}
|
||||
export declare const kmsSearchProvider: (query: string) => Promise<SearchResult[]>;
|
||||
export default kmsSearchProvider;
|
||||
152
web/@mf-types/kms/compiled-types/services/apiService.d.ts
vendored
Normal file
152
web/@mf-types/kms/compiled-types/services/apiService.d.ts
vendored
Normal file
@ -0,0 +1,152 @@
|
||||
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 {};
|
||||
179
web/README.md
Normal file
179
web/README.md
Normal file
@ -0,0 +1,179 @@
|
||||
# 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
|
||||
49
web/craco.config.js
Normal file
49
web/craco.config.js
Normal file
@ -0,0 +1,49 @@
|
||||
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
Normal file
35751
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
web/package.json
Normal file
41
web/package.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
18
web/public/index.html
Normal file
18
web/public/index.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!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>
|
||||
36
web/src/App.tsx
Normal file
36
web/src/App.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
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;
|
||||
23
web/src/bootstrap.tsx
Normal file
23
web/src/bootstrap.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
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>
|
||||
);
|
||||
96
web/src/components/AppContainer.tsx
Normal file
96
web/src/components/AppContainer.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
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;
|
||||
98
web/src/components/GlobalSearch.tsx
Normal file
98
web/src/components/GlobalSearch.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
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;
|
||||
66
web/src/components/LoadKMS.tsx
Normal file
66
web/src/components/LoadKMS.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
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;
|
||||
41
web/src/components/ShellHeader.tsx
Normal file
41
web/src/components/ShellHeader.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
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;
|
||||
28
web/src/components/TimeZoneClock.tsx
Normal file
28
web/src/components/TimeZoneClock.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
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;
|
||||
44
web/src/components/UserDropdown.tsx
Normal file
44
web/src/components/UserDropdown.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
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;
|
||||
108
web/src/contexts/AppContext.tsx
Normal file
108
web/src/contexts/AppContext.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
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;
|
||||
};
|
||||
101
web/src/index.css
Normal file
101
web/src/index.css
Normal file
@ -0,0 +1,101 @@
|
||||
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%;
|
||||
}
|
||||
3
web/src/index.tsx
Normal file
3
web/src/index.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
import('./bootstrap');
|
||||
|
||||
export {};
|
||||
10
web/src/types/federated.d.ts
vendored
Normal file
10
web/src/types/federated.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
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[]>;
|
||||
}
|
||||
26
web/src/types/index.ts
Normal file
26
web/src/types/index.ts
Normal file
@ -0,0 +1,26 @@
|
||||
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;
|
||||
}
|
||||
26
web/tsconfig.json
Normal file
26
web/tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user