module federation
This commit is contained in:
@ -5,3 +5,4 @@ REACT_APP_VERSION=1.0.0
|
|||||||
|
|
||||||
# Development settings
|
# Development settings
|
||||||
GENERATE_SOURCEMAP=true
|
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.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
# dependencies
|
# 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",
|
"typescript": "^4.9.5",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@craco/craco": "^7.1.0",
|
||||||
|
"craco-module-federation": "^1.1.0",
|
||||||
|
"webpack": "^5.96.1"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "craco start",
|
||||||
"build": "react-scripts build",
|
"build": "craco build",
|
||||||
"test": "react-scripts test",
|
"test": "craco test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "craco eject"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"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('./bootstrap');
|
||||||
import ReactDOM from 'react-dom/client';
|
|
||||||
import './index.css';
|
|
||||||
import App from './App';
|
|
||||||
import reportWebVitals from './reportWebVitals';
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
export {};
|
||||||
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();
|
|
||||||
|
|||||||
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