retry
This commit is contained in:
1
web/.gitignore
vendored
1
web/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
node_modules
|
|
||||||
23
web/@mf-types/index.d.ts
vendored
23
web/@mf-types/index.d.ts
vendored
@ -1,23 +0,0 @@
|
|||||||
import type { PackageType as PackageType_0,RemoteKeys as RemoteKeys_0 } from './kms/apis.d.ts';
|
|
||||||
declare module "@module-federation/runtime" {
|
|
||||||
type RemoteKeys = RemoteKeys_0;
|
|
||||||
type PackageType<T, Y=any> = T extends RemoteKeys_0 ? PackageType_0<T> :
|
|
||||||
Y ;
|
|
||||||
export function loadRemote<T extends RemoteKeys,Y>(packageName: T): Promise<PackageType<T, Y>>;
|
|
||||||
export function loadRemote<T extends string,Y>(packageName: T): Promise<PackageType<T, Y>>;
|
|
||||||
}
|
|
||||||
declare module "@module-federation/enhanced/runtime" {
|
|
||||||
type RemoteKeys = RemoteKeys_0;
|
|
||||||
type PackageType<T, Y=any> = T extends RemoteKeys_0 ? PackageType_0<T> :
|
|
||||||
Y ;
|
|
||||||
export function loadRemote<T extends RemoteKeys,Y>(packageName: T): Promise<PackageType<T, Y>>;
|
|
||||||
export function loadRemote<T extends string,Y>(packageName: T): Promise<PackageType<T, Y>>;
|
|
||||||
}
|
|
||||||
declare module "@module-federation/runtime-tools" {
|
|
||||||
type RemoteKeys = RemoteKeys_0;
|
|
||||||
type PackageType<T, Y=any> = T extends RemoteKeys_0 ? PackageType_0<T> :
|
|
||||||
Y ;
|
|
||||||
export function loadRemote<T extends RemoteKeys,Y>(packageName: T): Promise<PackageType<T, Y>>;
|
|
||||||
export function loadRemote<T extends string,Y>(packageName: T): Promise<PackageType<T, Y>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
2
web/@mf-types/kms/App.d.ts
vendored
2
web/@mf-types/kms/App.d.ts
vendored
@ -1,2 +0,0 @@
|
|||||||
export * from './compiled-types/federated/KMSApp';
|
|
||||||
export { default } from './compiled-types/federated/KMSApp';
|
|
||||||
2
web/@mf-types/kms/SearchProvider.d.ts
vendored
2
web/@mf-types/kms/SearchProvider.d.ts
vendored
@ -1,2 +0,0 @@
|
|||||||
export * from './compiled-types/federated/SearchProvider';
|
|
||||||
export { default } from './compiled-types/federated/SearchProvider';
|
|
||||||
3
web/@mf-types/kms/apis.d.ts
vendored
3
web/@mf-types/kms/apis.d.ts
vendored
@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
export type RemoteKeys = 'kms/App' | 'kms/SearchProvider';
|
|
||||||
type PackageType<T> = T extends 'kms/SearchProvider' ? typeof import('kms/SearchProvider') :T extends 'kms/App' ? typeof import('kms/App') :any;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
declare const Applications: React.FC;
|
|
||||||
export default Applications;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
declare const Audit: React.FC;
|
|
||||||
export default Audit;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
declare const Dashboard: React.FC;
|
|
||||||
export default Dashboard;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
declare const Login: React.FC;
|
|
||||||
export default Login;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
declare const TokenTester: React.FC;
|
|
||||||
export default TokenTester;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
declare const TokenTesterCallback: React.FC;
|
|
||||||
export default TokenTesterCallback;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
declare const Tokens: React.FC;
|
|
||||||
export default Tokens;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
declare const Users: React.FC;
|
|
||||||
export default Users;
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import React, { ReactNode } from 'react';
|
|
||||||
interface User {
|
|
||||||
email: string;
|
|
||||||
permissions: string[];
|
|
||||||
}
|
|
||||||
interface AuthContextType {
|
|
||||||
user: User | null;
|
|
||||||
login: (email: string) => Promise<boolean>;
|
|
||||||
logout: () => void;
|
|
||||||
loading: boolean;
|
|
||||||
}
|
|
||||||
export declare const useAuth: () => AuthContextType;
|
|
||||||
interface AuthProviderProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
export declare const AuthProvider: React.FC<AuthProviderProps>;
|
|
||||||
export {};
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
declare const KMSApp: React.FC;
|
|
||||||
export default KMSApp;
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
interface SearchResult {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
appId: string;
|
|
||||||
action?: () => void;
|
|
||||||
}
|
|
||||||
export declare const kmsSearchProvider: (query: string) => Promise<SearchResult[]>;
|
|
||||||
export default kmsSearchProvider;
|
|
||||||
@ -1,152 +0,0 @@
|
|||||||
export interface Application {
|
|
||||||
app_id: string;
|
|
||||||
app_link: string;
|
|
||||||
type: string[];
|
|
||||||
callback_url: string;
|
|
||||||
hmac_key: string;
|
|
||||||
token_prefix?: string;
|
|
||||||
token_renewal_duration: number;
|
|
||||||
max_token_duration: number;
|
|
||||||
owner: {
|
|
||||||
type: string;
|
|
||||||
name: string;
|
|
||||||
owner: string;
|
|
||||||
};
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
}
|
|
||||||
export interface StaticToken {
|
|
||||||
id: string;
|
|
||||||
app_id: string;
|
|
||||||
owner: {
|
|
||||||
type: string;
|
|
||||||
name: string;
|
|
||||||
owner: string;
|
|
||||||
};
|
|
||||||
type: string;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
}
|
|
||||||
export interface CreateApplicationRequest {
|
|
||||||
app_id: string;
|
|
||||||
app_link: string;
|
|
||||||
type: string[];
|
|
||||||
callback_url: string;
|
|
||||||
token_prefix?: string;
|
|
||||||
token_renewal_duration: string;
|
|
||||||
max_token_duration: string;
|
|
||||||
owner: {
|
|
||||||
type: string;
|
|
||||||
name: string;
|
|
||||||
owner: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
export interface CreateTokenRequest {
|
|
||||||
owner: {
|
|
||||||
type: string;
|
|
||||||
name: string;
|
|
||||||
owner: string;
|
|
||||||
};
|
|
||||||
permissions: string[];
|
|
||||||
}
|
|
||||||
export interface CreateTokenResponse {
|
|
||||||
id: string;
|
|
||||||
token: string;
|
|
||||||
permissions: string[];
|
|
||||||
created_at: string;
|
|
||||||
}
|
|
||||||
export interface PaginatedResponse<T> {
|
|
||||||
data: T[];
|
|
||||||
limit: number;
|
|
||||||
offset: number;
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
export interface VerifyRequest {
|
|
||||||
app_id: string;
|
|
||||||
user_id?: string;
|
|
||||||
token: string;
|
|
||||||
permissions?: string[];
|
|
||||||
}
|
|
||||||
export interface VerifyResponse {
|
|
||||||
valid: boolean;
|
|
||||||
permitted: boolean;
|
|
||||||
user_id?: string;
|
|
||||||
permissions: string[];
|
|
||||||
permission_results?: Record<string, boolean>;
|
|
||||||
expires_at?: string;
|
|
||||||
max_valid_at?: string;
|
|
||||||
token_type: string;
|
|
||||||
claims?: Record<string, string>;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
export interface AuditEvent {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
status: string;
|
|
||||||
timestamp: string;
|
|
||||||
actor_id?: string;
|
|
||||||
actor_ip?: string;
|
|
||||||
user_agent?: string;
|
|
||||||
resource_id?: string;
|
|
||||||
resource_type?: string;
|
|
||||||
action: string;
|
|
||||||
description: string;
|
|
||||||
details?: Record<string, any>;
|
|
||||||
request_id?: string;
|
|
||||||
session_id?: string;
|
|
||||||
}
|
|
||||||
export interface AuditQueryParams {
|
|
||||||
event_types?: string[];
|
|
||||||
statuses?: string[];
|
|
||||||
actor_id?: string;
|
|
||||||
resource_id?: string;
|
|
||||||
resource_type?: string;
|
|
||||||
start_time?: string;
|
|
||||||
end_time?: string;
|
|
||||||
limit?: number;
|
|
||||||
offset?: number;
|
|
||||||
order_by?: string;
|
|
||||||
order_desc?: boolean;
|
|
||||||
}
|
|
||||||
export interface AuditResponse {
|
|
||||||
events: AuditEvent[];
|
|
||||||
total: number;
|
|
||||||
limit: number;
|
|
||||||
offset: number;
|
|
||||||
}
|
|
||||||
export interface AuditStats {
|
|
||||||
total_events: number;
|
|
||||||
by_type: Record<string, number>;
|
|
||||||
by_severity: Record<string, number>;
|
|
||||||
by_status: Record<string, number>;
|
|
||||||
by_time?: Record<string, number>;
|
|
||||||
}
|
|
||||||
export interface AuditStatsParams {
|
|
||||||
event_types?: string[];
|
|
||||||
start_time?: string;
|
|
||||||
end_time?: string;
|
|
||||||
group_by?: string;
|
|
||||||
}
|
|
||||||
declare class ApiService {
|
|
||||||
private api;
|
|
||||||
private baseURL;
|
|
||||||
constructor();
|
|
||||||
healthCheck(): Promise<any>;
|
|
||||||
readinessCheck(): Promise<any>;
|
|
||||||
getApplications(limit?: number, offset?: number): Promise<PaginatedResponse<Application>>;
|
|
||||||
getApplication(appId: string): Promise<Application>;
|
|
||||||
createApplication(data: CreateApplicationRequest): Promise<Application>;
|
|
||||||
updateApplication(appId: string, data: Partial<CreateApplicationRequest>): Promise<Application>;
|
|
||||||
deleteApplication(appId: string): Promise<void>;
|
|
||||||
getTokensForApplication(appId: string, limit?: number, offset?: number): Promise<PaginatedResponse<StaticToken>>;
|
|
||||||
createToken(appId: string, data: CreateTokenRequest): Promise<CreateTokenResponse>;
|
|
||||||
deleteToken(tokenId: string): Promise<void>;
|
|
||||||
verifyToken(data: VerifyRequest): Promise<VerifyResponse>;
|
|
||||||
login(appId: string, permissions: string[], redirectUri?: string, tokenDelivery?: string): Promise<any>;
|
|
||||||
renewToken(appId: string, userId: string, token: string): Promise<any>;
|
|
||||||
getAuditEvents(params?: AuditQueryParams): Promise<AuditResponse>;
|
|
||||||
getAuditEvent(eventId: string): Promise<AuditEvent>;
|
|
||||||
getAuditStats(params?: AuditStatsParams): Promise<AuditStats>;
|
|
||||||
}
|
|
||||||
export declare const apiService: ApiService;
|
|
||||||
export {};
|
|
||||||
179
web/README.md
179
web/README.md
@ -1,179 +0,0 @@
|
|||||||
# Skybridge Platform Shell
|
|
||||||
|
|
||||||
A module federated React platform that provides a unified shell for multiple applications, similar to AWS Console.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Module Federation**: Apps are loaded as federated modules
|
|
||||||
- **Tab-based Navigation**: Each application appears as a tab
|
|
||||||
- **Global Search**: Federated search across all applications
|
|
||||||
- **Timezone Clocks**: PST, EST, CST, and IST clocks in the header
|
|
||||||
- **User Management**: User dropdown with logout functionality
|
|
||||||
- **Extensible**: Easy to add new federated applications
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- Node.js 24+
|
|
||||||
- npm 11+
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd web
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
Start the shell (runs on port 3000):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
Start the KMS federated app (runs on port 3001):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ../kms/kms-frontend
|
|
||||||
npm install
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Shell Application (`web/`)
|
|
||||||
|
|
||||||
- **Port**: 3000
|
|
||||||
- **Role**: Host application that loads federated modules
|
|
||||||
- **Components**:
|
|
||||||
- `ShellHeader`: Global navigation with search and timezone clocks
|
|
||||||
- `AppContainer`: Tab-based container for federated applications
|
|
||||||
- `GlobalSearch`: Federated search across all applications
|
|
||||||
- `TimeZoneClock`: Multi-timezone display
|
|
||||||
- `UserDropdown`: User management interface
|
|
||||||
|
|
||||||
### KMS Application (`kms/kms-frontend/`)
|
|
||||||
|
|
||||||
- **Port**: 3001
|
|
||||||
- **Role**: Federated module providing KMS functionality
|
|
||||||
- **Exposes**:
|
|
||||||
- `./App`: Main KMS application component
|
|
||||||
- `./SearchProvider`: Search functionality for KMS data
|
|
||||||
|
|
||||||
## Adding New Applications
|
|
||||||
|
|
||||||
To add a new federated application:
|
|
||||||
|
|
||||||
1. **Create the application** with module federation configuration
|
|
||||||
2. **Expose components** in `craco.config.js`:
|
|
||||||
```javascript
|
|
||||||
exposes: {
|
|
||||||
'./App': './src/YourApp',
|
|
||||||
'./SearchProvider': './src/SearchProvider', // Optional
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Update shell configuration** in `web/craco.config.js`:
|
|
||||||
```javascript
|
|
||||||
remotes: {
|
|
||||||
kms: 'kms@http://localhost:3001/remoteEntry.js',
|
|
||||||
yourapp: 'yourapp@http://localhost:3002/remoteEntry.js',
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Create a loader component** similar to `LoadKMS.tsx`:
|
|
||||||
```typescript
|
|
||||||
const YourApp = React.lazy(() => import('yourapp/App'));
|
|
||||||
|
|
||||||
const LoadYourApp: React.FC = () => {
|
|
||||||
const { registerApp } = useApp();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
registerApp({
|
|
||||||
id: 'yourapp',
|
|
||||||
name: 'Your App',
|
|
||||||
path: '/yourapp',
|
|
||||||
component: YourApp,
|
|
||||||
searchProvider: yourSearchProvider, // Optional
|
|
||||||
});
|
|
||||||
}, [registerApp]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Include the loader** in the main App component
|
|
||||||
|
|
||||||
## Search Integration
|
|
||||||
|
|
||||||
Applications can provide search functionality by exposing a search provider:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export const yourSearchProvider = async (query: string): Promise<SearchResult[]> => {
|
|
||||||
// Implement your search logic
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'result-1',
|
|
||||||
title: 'Search Result',
|
|
||||||
description: 'Description of the result',
|
|
||||||
appId: 'Your App',
|
|
||||||
action: () => {
|
|
||||||
// Navigate to result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Ports
|
|
||||||
|
|
||||||
- Shell: 3000
|
|
||||||
- KMS: 3001
|
|
||||||
- Future apps: 3002+
|
|
||||||
|
|
||||||
### Environment
|
|
||||||
|
|
||||||
The shell includes:
|
|
||||||
|
|
||||||
- **React 19** with TypeScript
|
|
||||||
- **Ant Design 5.27** for UI components
|
|
||||||
- **Module Federation** for app loading
|
|
||||||
- **React Router** for routing
|
|
||||||
- **Moment Timezone** for clock functionality
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
web/
|
|
||||||
├── src/
|
|
||||||
│ ├── components/
|
|
||||||
│ │ ├── AppContainer.tsx # Tab container
|
|
||||||
│ │ ├── GlobalSearch.tsx # Federated search
|
|
||||||
│ │ ├── ShellHeader.tsx # Main header
|
|
||||||
│ │ ├── TimeZoneClock.tsx # Timezone display
|
|
||||||
│ │ ├── UserDropdown.tsx # User menu
|
|
||||||
│ │ └── LoadKMS.tsx # KMS app loader
|
|
||||||
│ ├── contexts/
|
|
||||||
│ │ └── AppContext.tsx # Global app state
|
|
||||||
│ ├── types/
|
|
||||||
│ │ ├── index.ts # Type definitions
|
|
||||||
│ │ └── federated.d.ts # Module federation types
|
|
||||||
│ ├── App.tsx # Main app component
|
|
||||||
│ └── index.tsx # Entry point
|
|
||||||
├── public/
|
|
||||||
├── craco.config.js # Module federation config
|
|
||||||
├── package.json
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Notes
|
|
||||||
|
|
||||||
- Applications run on separate ports and are loaded dynamically
|
|
||||||
- Shared dependencies (React, Ant Design) are singleton across all apps
|
|
||||||
- Each application maintains its own routing under its path prefix
|
|
||||||
- Global search aggregates results from all registered applications
|
|
||||||
- User state is managed globally in the shell
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
const { ModuleFederationPlugin } = require("webpack").container;
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
devServer: {
|
|
||||||
port: 3000,
|
|
||||||
historyApiFallback: true,
|
|
||||||
},
|
|
||||||
webpack: {
|
|
||||||
plugins: {
|
|
||||||
add: [
|
|
||||||
new ModuleFederationPlugin({
|
|
||||||
name: 'shell',
|
|
||||||
filename: 'remoteEntry.js',
|
|
||||||
remotes: {
|
|
||||||
kms: 'kms@http://localhost:3001/remoteEntry.js',
|
|
||||||
},
|
|
||||||
shared: {
|
|
||||||
react: {
|
|
||||||
singleton: true,
|
|
||||||
requiredVersion: '^19.1.1'
|
|
||||||
},
|
|
||||||
'react-dom': {
|
|
||||||
singleton: true,
|
|
||||||
requiredVersion: '^19.1.1'
|
|
||||||
},
|
|
||||||
'react-router-dom': {
|
|
||||||
singleton: true,
|
|
||||||
requiredVersion: '^7.8.2'
|
|
||||||
},
|
|
||||||
antd: {
|
|
||||||
singleton: true,
|
|
||||||
requiredVersion: '^5.27.1'
|
|
||||||
},
|
|
||||||
axios: {
|
|
||||||
singleton: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
configure: (webpackConfig) => ({
|
|
||||||
...webpackConfig,
|
|
||||||
output: {
|
|
||||||
...webpackConfig.output,
|
|
||||||
publicPath: "auto",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
35751
web/package-lock.json
generated
35751
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "skybridge-shell",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@ant-design/icons": "^6.0.0",
|
|
||||||
"@types/react": "^19.1.11",
|
|
||||||
"@types/react-dom": "^19.1.7",
|
|
||||||
"antd": "^5.27.1",
|
|
||||||
"axios": "^1.11.0",
|
|
||||||
"moment-timezone": "^0.5.45",
|
|
||||||
"react": "^19.1.1",
|
|
||||||
"react-dom": "^19.1.1",
|
|
||||||
"react-router-dom": "^7.8.2",
|
|
||||||
"typescript": "^4.9.5"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@craco/craco": "^7.1.0",
|
|
||||||
"@types/moment-timezone": "^0.5.30",
|
|
||||||
"craco-module-federation": "^1.1.0",
|
|
||||||
"webpack": "^5.96.1"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"start": "craco start",
|
|
||||||
"build": "craco build",
|
|
||||||
"test": "craco test",
|
|
||||||
"eject": "craco eject"
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Skybridge Cloud Platform"
|
|
||||||
/>
|
|
||||||
<title>Skybridge Platform</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
|
||||||
import { Layout } from 'antd';
|
|
||||||
import ShellHeader from './components/ShellHeader';
|
|
||||||
import AppContainer from './components/AppContainer';
|
|
||||||
import LoadKMS from './components/LoadKMS';
|
|
||||||
import { useApp } from './contexts/AppContext';
|
|
||||||
|
|
||||||
const { Content } = Layout;
|
|
||||||
|
|
||||||
const App: React.FC = () => {
|
|
||||||
const { setUser } = useApp();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setUser({
|
|
||||||
email: 'admin@example.com',
|
|
||||||
name: 'Admin User',
|
|
||||||
});
|
|
||||||
}, [setUser]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout className="shell-layout">
|
|
||||||
<LoadKMS />
|
|
||||||
<ShellHeader />
|
|
||||||
<Content className="shell-content">
|
|
||||||
<Routes>
|
|
||||||
<Route path="/kms/*" element={<AppContainer />} />
|
|
||||||
<Route path="/" element={<Navigate to="/kms" replace />} />
|
|
||||||
<Route path="*" element={<AppContainer />} />
|
|
||||||
</Routes>
|
|
||||||
</Content>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom/client';
|
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
|
||||||
import { ConfigProvider } from 'antd';
|
|
||||||
import App from './App';
|
|
||||||
import { AppProvider } from './contexts/AppContext';
|
|
||||||
import './index.css';
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
|
||||||
document.getElementById('root') as HTMLElement
|
|
||||||
);
|
|
||||||
|
|
||||||
root.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<BrowserRouter>
|
|
||||||
<ConfigProvider>
|
|
||||||
<AppProvider>
|
|
||||||
<App />
|
|
||||||
</AppProvider>
|
|
||||||
</ConfigProvider>
|
|
||||||
</BrowserRouter>
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
import React, { Suspense, useEffect } from 'react';
|
|
||||||
import { Tabs, Spin } from 'antd';
|
|
||||||
import { useApp } from '../contexts/AppContext';
|
|
||||||
import { FederatedApp } from '../types';
|
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
const AppContainer: React.FC = () => {
|
|
||||||
const { apps, activeAppId, setActiveApp } = useApp();
|
|
||||||
const location = useLocation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
// Auto-detect active app based on current URL path
|
|
||||||
useEffect(() => {
|
|
||||||
if (apps.length > 0) {
|
|
||||||
const currentPath = location.pathname;
|
|
||||||
|
|
||||||
// Find app that matches the current path
|
|
||||||
const matchingApp = apps.find(app => currentPath.startsWith(app.path));
|
|
||||||
|
|
||||||
if (matchingApp && activeAppId !== matchingApp.id) {
|
|
||||||
setActiveApp(matchingApp.id);
|
|
||||||
} else if (!activeAppId && !matchingApp) {
|
|
||||||
// Default to first app if no match
|
|
||||||
setActiveApp(apps[0].id);
|
|
||||||
navigate(apps[0].path, { replace: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [location.pathname, apps, activeAppId, setActiveApp, navigate]);
|
|
||||||
|
|
||||||
if (apps.length === 0) {
|
|
||||||
return (
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
height: '100%',
|
|
||||||
fontSize: '16px',
|
|
||||||
color: '#666'
|
|
||||||
}}>
|
|
||||||
No applications available
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTabChange = (key: string) => {
|
|
||||||
const selectedApp = apps.find(app => app.id === key);
|
|
||||||
if (selectedApp) {
|
|
||||||
setActiveApp(key);
|
|
||||||
navigate(selectedApp.path);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderAppContent = (app: FederatedApp) => {
|
|
||||||
const AppComponent = app.component;
|
|
||||||
return (
|
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
height: '400px'
|
|
||||||
}}>
|
|
||||||
<Spin size="large" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div style={{ height: '100%', overflow: 'auto' }}>
|
|
||||||
<AppComponent />
|
|
||||||
</div>
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabItems = apps.map(app => ({
|
|
||||||
key: app.id,
|
|
||||||
label: app.name,
|
|
||||||
children: renderAppContent(app)
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ height: '100%', padding: '0' }}>
|
|
||||||
<Tabs
|
|
||||||
activeKey={activeAppId || (apps.length > 0 ? apps[0].id : undefined)}
|
|
||||||
onChange={handleTabChange}
|
|
||||||
className="app-tabs"
|
|
||||||
type="card"
|
|
||||||
style={{ height: '100%' }}
|
|
||||||
items={tabItems}
|
|
||||||
tabBarStyle={{ margin: 0, padding: '0 16px' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppContainer;
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
|
||||||
import { Input, Dropdown, Spin, Empty, Typography } from 'antd';
|
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
|
||||||
import { useApp } from '../contexts/AppContext';
|
|
||||||
import { SearchResult } from '../types';
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
const GlobalSearch: React.FC = () => {
|
|
||||||
const [query, setQuery] = useState('');
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const { searchResults, isSearching, performSearch, clearSearch } = useApp();
|
|
||||||
|
|
||||||
const handleSearch = useCallback(
|
|
||||||
async (value: string) => {
|
|
||||||
setQuery(value);
|
|
||||||
if (value.trim()) {
|
|
||||||
await performSearch(value);
|
|
||||||
setIsOpen(true);
|
|
||||||
} else {
|
|
||||||
clearSearch();
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[performSearch, clearSearch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleResultClick = (result: SearchResult) => {
|
|
||||||
if (result.action) {
|
|
||||||
result.action();
|
|
||||||
}
|
|
||||||
setIsOpen(false);
|
|
||||||
setQuery('');
|
|
||||||
clearSearch();
|
|
||||||
};
|
|
||||||
|
|
||||||
const searchContent = (
|
|
||||||
<div className="search-dropdown" style={{ minWidth: 400 }}>
|
|
||||||
{isSearching ? (
|
|
||||||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
|
||||||
<Spin />
|
|
||||||
</div>
|
|
||||||
) : searchResults.length > 0 ? (
|
|
||||||
searchResults.map((result) => (
|
|
||||||
<div
|
|
||||||
key={result.id}
|
|
||||||
className="search-result-item"
|
|
||||||
onClick={() => handleResultClick(result)}
|
|
||||||
style={{ cursor: 'pointer' }}
|
|
||||||
>
|
|
||||||
<div className="search-result-title">{result.title}</div>
|
|
||||||
{result.description && (
|
|
||||||
<div className="search-result-description">{result.description}</div>
|
|
||||||
)}
|
|
||||||
<div className="search-result-app">
|
|
||||||
<Text type="secondary">{result.appId}</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : query ? (
|
|
||||||
<div style={{ padding: '20px' }}>
|
|
||||||
<Empty
|
|
||||||
description="No results found"
|
|
||||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropdown
|
|
||||||
open={isOpen && (isSearching || searchResults.length > 0 || query.length > 0)}
|
|
||||||
onOpenChange={setIsOpen}
|
|
||||||
dropdownRender={() => searchContent}
|
|
||||||
trigger={['click']}
|
|
||||||
placement="bottomLeft"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
size="middle"
|
|
||||||
placeholder="Search across all applications..."
|
|
||||||
prefix={<SearchOutlined />}
|
|
||||||
value={query}
|
|
||||||
onChange={(e) => handleSearch(e.target.value)}
|
|
||||||
onPressEnter={() => handleSearch(query)}
|
|
||||||
style={{ width: 400 }}
|
|
||||||
allowClear
|
|
||||||
onClear={() => {
|
|
||||||
setQuery('');
|
|
||||||
clearSearch();
|
|
||||||
setIsOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GlobalSearch;
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
|
||||||
import { useApp } from '../contexts/AppContext';
|
|
||||||
|
|
||||||
const LoadKMS: React.FC = () => {
|
|
||||||
const { registerApp } = useApp();
|
|
||||||
const registeredRef = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadKMS = async () => {
|
|
||||||
if (registeredRef.current) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('Loading KMS app...');
|
|
||||||
|
|
||||||
// Dynamically import the KMS app
|
|
||||||
const KMSApp = React.lazy(() => import('kms/App'));
|
|
||||||
|
|
||||||
// Try to load search provider separately
|
|
||||||
let kmsSearchProvider;
|
|
||||||
try {
|
|
||||||
const SearchModule = await import('kms/SearchProvider');
|
|
||||||
kmsSearchProvider = SearchModule.kmsSearchProvider;
|
|
||||||
} catch (searchError) {
|
|
||||||
console.warn('Failed to load KMS search provider:', searchError);
|
|
||||||
kmsSearchProvider = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerApp({
|
|
||||||
id: 'kms',
|
|
||||||
name: 'KMS',
|
|
||||||
path: '/kms',
|
|
||||||
component: KMSApp,
|
|
||||||
searchProvider: kmsSearchProvider,
|
|
||||||
});
|
|
||||||
|
|
||||||
registeredRef.current = true;
|
|
||||||
console.log('KMS app loaded successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load KMS app:', error);
|
|
||||||
|
|
||||||
// Try to register with just basic import as fallback
|
|
||||||
try {
|
|
||||||
const KMSApp = React.lazy(() => import('kms/App'));
|
|
||||||
|
|
||||||
registerApp({
|
|
||||||
id: 'kms',
|
|
||||||
name: 'KMS',
|
|
||||||
path: '/kms',
|
|
||||||
component: KMSApp,
|
|
||||||
});
|
|
||||||
|
|
||||||
registeredRef.current = true;
|
|
||||||
console.log('KMS app loaded with fallback');
|
|
||||||
} catch (fallbackError) {
|
|
||||||
console.error('Complete KMS loading failure:', fallbackError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadKMS();
|
|
||||||
}, [registerApp]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LoadKMS;
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Layout, Space, Typography } from 'antd';
|
|
||||||
import GlobalSearch from './GlobalSearch';
|
|
||||||
import TimeZoneClock from './TimeZoneClock';
|
|
||||||
import UserDropdown from './UserDropdown';
|
|
||||||
|
|
||||||
const { Header } = Layout;
|
|
||||||
const { Title } = Typography;
|
|
||||||
|
|
||||||
const ShellHeader: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<Header className="shell-header" style={{
|
|
||||||
padding: '0 24px',
|
|
||||||
background: '#fff',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
borderBottom: '1px solid #f0f0f0'
|
|
||||||
}}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
|
||||||
<Title level={4} style={{ margin: 0, marginRight: 32 }}>
|
|
||||||
Skybridge Platform
|
|
||||||
</Title>
|
|
||||||
<GlobalSearch />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Space size="large">
|
|
||||||
<div className="timezone-clocks">
|
|
||||||
<TimeZoneClock timezone="America/Los_Angeles" label="PST" />
|
|
||||||
<TimeZoneClock timezone="America/New_York" label="EST" />
|
|
||||||
<TimeZoneClock timezone="America/Chicago" label="CST" />
|
|
||||||
<TimeZoneClock timezone="Asia/Kolkata" label="IST" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UserDropdown />
|
|
||||||
</Space>
|
|
||||||
</Header>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ShellHeader;
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import moment from 'moment-timezone';
|
|
||||||
import { TimeZoneClockProps } from '../types';
|
|
||||||
|
|
||||||
const TimeZoneClock: React.FC<TimeZoneClockProps> = ({ timezone, label }) => {
|
|
||||||
const [time, setTime] = useState(moment.tz(timezone));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
setTime(moment.tz(timezone));
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [timezone]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="timezone-clock">
|
|
||||||
<div className="timezone-clock-time">
|
|
||||||
{time.format('HH:mm')}
|
|
||||||
</div>
|
|
||||||
<div className="timezone-clock-label">
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TimeZoneClock;
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Dropdown, Avatar, Typography, Space } from 'antd';
|
|
||||||
import { UserOutlined, LogoutOutlined } from '@ant-design/icons';
|
|
||||||
import type { MenuProps } from 'antd';
|
|
||||||
import { useApp } from '../contexts/AppContext';
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
const UserDropdown: React.FC = () => {
|
|
||||||
const { user, logout } = useApp();
|
|
||||||
|
|
||||||
if (!user) return null;
|
|
||||||
|
|
||||||
const menuItems: MenuProps['items'] = [
|
|
||||||
{
|
|
||||||
key: 'logout',
|
|
||||||
icon: <LogoutOutlined />,
|
|
||||||
label: 'Logout',
|
|
||||||
onClick: logout,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropdown menu={{ items: menuItems }} placement="bottomRight">
|
|
||||||
<Space style={{ cursor: 'pointer' }}>
|
|
||||||
<Avatar
|
|
||||||
size="small"
|
|
||||||
icon={<UserOutlined />}
|
|
||||||
src={user.avatar}
|
|
||||||
/>
|
|
||||||
<div style={{ textAlign: 'left' }}>
|
|
||||||
<div>{user.name || user.email}</div>
|
|
||||||
{user.name && (
|
|
||||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
|
||||||
{user.email}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Space>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UserDropdown;
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
|
||||||
import { FederatedApp, User, SearchResult } from '../types';
|
|
||||||
|
|
||||||
interface AppContextType {
|
|
||||||
user: User | null;
|
|
||||||
apps: FederatedApp[];
|
|
||||||
activeAppId: string | null;
|
|
||||||
searchResults: SearchResult[];
|
|
||||||
isSearching: boolean;
|
|
||||||
setUser: (user: User | null) => void;
|
|
||||||
registerApp: (app: FederatedApp) => void;
|
|
||||||
setActiveApp: (appId: string | null) => void;
|
|
||||||
performSearch: (query: string) => Promise<void>;
|
|
||||||
clearSearch: () => void;
|
|
||||||
logout: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppContext = createContext<AppContextType | undefined>(undefined);
|
|
||||||
|
|
||||||
interface AppProviderProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AppProvider: React.FC<AppProviderProps> = ({ children }) => {
|
|
||||||
const [user, setUser] = useState<User | null>(null);
|
|
||||||
const [apps, setApps] = useState<FederatedApp[]>([]);
|
|
||||||
const [activeAppId, setActiveAppId] = useState<string | null>(null);
|
|
||||||
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
|
||||||
|
|
||||||
const registerApp = (app: FederatedApp) => {
|
|
||||||
setApps(prev => {
|
|
||||||
const exists = prev.find(a => a.id === app.id);
|
|
||||||
if (exists) return prev;
|
|
||||||
return [...prev, app];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const setActiveApp = (appId: string | null) => {
|
|
||||||
setActiveAppId(appId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const performSearch = async (query: string) => {
|
|
||||||
if (!query.trim()) {
|
|
||||||
setSearchResults([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSearching(true);
|
|
||||||
const allResults: SearchResult[] = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all(
|
|
||||||
apps.map(async (app) => {
|
|
||||||
if (app.searchProvider) {
|
|
||||||
try {
|
|
||||||
const results = await app.searchProvider(query);
|
|
||||||
allResults.push(...results);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Search failed for app ${app.id}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setSearchResults(allResults);
|
|
||||||
} finally {
|
|
||||||
setIsSearching(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearSearch = () => {
|
|
||||||
setSearchResults([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
setUser(null);
|
|
||||||
setActiveAppId(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppContext.Provider
|
|
||||||
value={{
|
|
||||||
user,
|
|
||||||
apps,
|
|
||||||
activeAppId,
|
|
||||||
searchResults,
|
|
||||||
isSearching,
|
|
||||||
setUser,
|
|
||||||
registerApp,
|
|
||||||
setActiveApp,
|
|
||||||
performSearch,
|
|
||||||
clearSearch,
|
|
||||||
logout,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</AppContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useApp = (): AppContextType => {
|
|
||||||
const context = useContext(AppContext);
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error('useApp must be used within an AppProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
@ -1,101 +0,0 @@
|
|||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
|
||||||
monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
#root {
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shell-layout {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shell-header {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
background: #fff;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shell-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timezone-clocks {
|
|
||||||
display: flex;
|
|
||||||
gap: 24px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timezone-clock {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timezone-clock-time {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timezone-clock-label {
|
|
||||||
color: #666;
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-dropdown {
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-result-item {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-result-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-result-title {
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-result-description {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-result-app {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #999;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-tabs .ant-tabs-content-holder {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-tabs .ant-tabs-tabpane {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import('./bootstrap');
|
|
||||||
|
|
||||||
export {};
|
|
||||||
10
web/src/types/federated.d.ts
vendored
10
web/src/types/federated.d.ts
vendored
@ -1,10 +0,0 @@
|
|||||||
declare module 'kms/App' {
|
|
||||||
import React from 'react';
|
|
||||||
const KMSApp: React.ComponentType;
|
|
||||||
export default KMSApp;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module 'kms/SearchProvider' {
|
|
||||||
import { SearchResult } from './index';
|
|
||||||
export const kmsSearchProvider: (query: string) => Promise<SearchResult[]>;
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
export interface FederatedApp {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
component: React.ComponentType;
|
|
||||||
searchProvider?: (query: string) => Promise<SearchResult[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SearchResult {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
appId: string;
|
|
||||||
action?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface User {
|
|
||||||
email: string;
|
|
||||||
name?: string;
|
|
||||||
avatar?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TimeZoneClockProps {
|
|
||||||
timezone: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es5",
|
|
||||||
"lib": [
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx"
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user