graphxr-database-proxy 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. graphxr_database_proxy/__init__.py +16 -0
  2. graphxr_database_proxy/api/__init__.py +1 -0
  3. graphxr_database_proxy/api/database.py +266 -0
  4. graphxr_database_proxy/api/google.py +437 -0
  5. graphxr_database_proxy/api/projects.py +99 -0
  6. graphxr_database_proxy/common/util.py +38 -0
  7. graphxr_database_proxy/drivers/__init__.py +1 -0
  8. graphxr_database_proxy/drivers/base.py +56 -0
  9. graphxr_database_proxy/drivers/factory.py +36 -0
  10. graphxr_database_proxy/drivers/spanner.py +814 -0
  11. graphxr_database_proxy/main.py +110 -0
  12. graphxr_database_proxy/models/__init__.py +1 -0
  13. graphxr_database_proxy/models/google.py +50 -0
  14. graphxr_database_proxy/models/project.py +170 -0
  15. graphxr_database_proxy/proxy.py +495 -0
  16. graphxr_database_proxy/proxyForDev.py +290 -0
  17. graphxr_database_proxy/services/__init__.py +1 -0
  18. graphxr_database_proxy/services/project_service.py +150 -0
  19. graphxr_database_proxy/static/favicon.ico +0 -0
  20. graphxr_database_proxy/static/index.html +1 -0
  21. graphxr_database_proxy/static/main.7391ee9773c403483393.css +175 -0
  22. graphxr_database_proxy/static/main.7391ee9773c403483393.css.map +1 -0
  23. graphxr_database_proxy/static/main.ce3fbb85a7bc9452edb9.js +2 -0
  24. graphxr_database_proxy/static/main.ce3fbb85a7bc9452edb9.js.map +1 -0
  25. graphxr_database_proxy/static/vendors.70542a99f336a8021013.js +3 -0
  26. graphxr_database_proxy/static/vendors.70542a99f336a8021013.js.LICENSE.txt +95 -0
  27. graphxr_database_proxy/static/vendors.70542a99f336a8021013.js.map +1 -0
  28. graphxr_database_proxy-1.0.0.dist-info/METADATA +180 -0
  29. graphxr_database_proxy-1.0.0.dist-info/RECORD +33 -0
  30. graphxr_database_proxy-1.0.0.dist-info/WHEEL +5 -0
  31. graphxr_database_proxy-1.0.0.dist-info/entry_points.txt +2 -0
  32. graphxr_database_proxy-1.0.0.dist-info/licenses/LICENSE +21 -0
  33. graphxr_database_proxy-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,290 @@
1
+ """
2
+ Development Proxy Configuration Module
3
+ Auto-detect proxy settings and intercept all Python network requests
4
+ """
5
+
6
+ import os
7
+ import socket
8
+ import urllib.parse
9
+ import urllib.request
10
+ import urllib.error
11
+ from typing import List, Optional, Dict, Any
12
+ import logging
13
+
14
+ # Configure logging
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger = logging.getLogger(__name__)
17
+
18
+ class ProxyConfig:
19
+ """Proxy configuration class"""
20
+
21
+ def __init__(self):
22
+ self.http_proxy = os.environ.get('http_proxy') or os.environ.get('HTTP_PROXY')
23
+ self.https_proxy = os.environ.get('https_proxy') or os.environ.get('HTTPS_PROXY')
24
+ self.no_proxy = os.environ.get('no_proxy') or os.environ.get('NO_PROXY')
25
+
26
+ # List of domains that need proxy (supports wildcards)
27
+ self.proxy_domains = [
28
+ 'googleapis.com',
29
+ '*.googleapis.com',
30
+ 'google.com',
31
+ '*.google.com',
32
+ 'accounts.google.com',
33
+ 'oauth2.googleapis.com',
34
+ 'www.googleapis.com',
35
+ 'cloudresourcemanager.googleapis.com',
36
+ 'spanner.googleapis.com',
37
+ # Add more domains that need proxy
38
+ ]
39
+
40
+ # List of domains that don't need proxy
41
+ self.no_proxy_domains = []
42
+ if self.no_proxy:
43
+ self.no_proxy_domains = [domain.strip() for domain in self.no_proxy.split(',')]
44
+
45
+ # Default domains that don't need proxy
46
+ self.no_proxy_domains.extend([
47
+ 'localhost',
48
+ '127.0.0.1',
49
+ '::1',
50
+ '.local'
51
+ ])
52
+
53
+ def should_use_proxy(self, url: str) -> bool:
54
+ """Check if URL needs to use proxy"""
55
+ if not (self.http_proxy or self.https_proxy):
56
+ return False
57
+
58
+ parsed = urllib.parse.urlparse(url)
59
+ hostname = parsed.hostname or parsed.netloc
60
+
61
+ # Check if it's in the no-proxy domain list
62
+ for no_proxy_domain in self.no_proxy_domains:
63
+ if self._match_domain(hostname, no_proxy_domain):
64
+ logger.debug(f"Skip proxy (no_proxy): {hostname}")
65
+ return False
66
+
67
+ # Check if it's in the proxy domain list
68
+ for proxy_domain in self.proxy_domains:
69
+ if self._match_domain(hostname, proxy_domain):
70
+ logger.debug(f"Use proxy: {hostname}")
71
+ return True
72
+
73
+ # Default: don't use proxy
74
+ return False
75
+
76
+ def _match_domain(self, hostname: str, pattern: str) -> bool:
77
+ """Match domain pattern with wildcard support"""
78
+ if not hostname:
79
+ return False
80
+
81
+ if pattern.startswith('*.'):
82
+ # Wildcard matching
83
+ suffix = pattern[2:]
84
+ return hostname.endswith(suffix) or hostname == suffix
85
+ else:
86
+ # Exact matching
87
+ return hostname == pattern
88
+
89
+ def get_proxy_url(self, scheme: str) -> Optional[str]:
90
+ """Get proxy URL for corresponding protocol"""
91
+ if scheme.lower() == 'https':
92
+ return self.https_proxy or self.http_proxy
93
+ elif scheme.lower() == 'http':
94
+ return self.http_proxy
95
+ return None
96
+
97
+ # Global proxy configuration instance
98
+ proxy_config = ProxyConfig()
99
+
100
+ # Backup of original network functions
101
+ _original_urlopen = urllib.request.urlopen
102
+ _original_socket_create_connection = socket.create_connection
103
+
104
+ def _patched_urlopen(url, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, **kwargs):
105
+ """Proxy-intercepted urlopen function"""
106
+
107
+ # If it's a string URL, check if proxy is needed
108
+ if isinstance(url, str):
109
+ if proxy_config.should_use_proxy(url):
110
+ parsed = urllib.parse.urlparse(url)
111
+ proxy_url = proxy_config.get_proxy_url(parsed.scheme)
112
+
113
+ if proxy_url:
114
+ logger.info(f"Accessing via proxy: {url}")
115
+ logger.debug(f"Using proxy: {proxy_url}")
116
+
117
+ # Create proxy handler
118
+ proxy_handler = urllib.request.ProxyHandler({
119
+ 'http': proxy_url,
120
+ 'https': proxy_url
121
+ })
122
+ opener = urllib.request.build_opener(proxy_handler)
123
+
124
+ # Create request
125
+ if isinstance(url, str):
126
+ request = urllib.request.Request(url, data=data)
127
+ else:
128
+ request = url
129
+
130
+ return opener.open(request, timeout=timeout)
131
+
132
+ # For other cases, use original function
133
+ return _original_urlopen(url, data=data, timeout=timeout, **kwargs)
134
+
135
+ def _patched_socket_create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, source_address=None):
136
+ """Proxy-intercepted socket connection function"""
137
+
138
+ # Check if proxy is needed
139
+ if len(address) >= 2:
140
+ hostname, port = address[0], address[1]
141
+
142
+ # Construct URL for checking
143
+ scheme = 'https' if port == 443 else 'http'
144
+ url = f"{scheme}://{hostname}:{port}"
145
+
146
+ if proxy_config.should_use_proxy(url):
147
+ proxy_url = proxy_config.get_proxy_url(scheme)
148
+ if proxy_url:
149
+ logger.debug(f"Socket connection via proxy: {hostname}:{port}")
150
+ # Note: This may require more complex proxy socket implementation
151
+ # For simple cases, we let it fall back to original connection
152
+
153
+ return _original_socket_create_connection(address, timeout, source_address)
154
+
155
+ def setup_requests_proxy():
156
+ """Set up proxy for requests library"""
157
+ try:
158
+ import requests
159
+
160
+ # Get or create default session
161
+ if not hasattr(requests, '_original_get'):
162
+ requests._original_get = requests.get
163
+ requests._original_post = requests.post
164
+ requests._original_put = requests.put
165
+ requests._original_delete = requests.delete
166
+ requests._original_patch = requests.patch
167
+
168
+ def _proxy_request(method, url, **kwargs):
169
+ """Proxy request wrapper"""
170
+ if proxy_config.should_use_proxy(url):
171
+ proxy_url = proxy_config.get_proxy_url(urllib.parse.urlparse(url).scheme)
172
+ if proxy_url:
173
+ if 'proxies' not in kwargs:
174
+ kwargs['proxies'] = {}
175
+ kwargs['proxies'].update({
176
+ 'http': proxy_url,
177
+ 'https': proxy_url
178
+ })
179
+ logger.info(f"Requests accessing via proxy: {url}")
180
+
181
+ # Call original method
182
+ original_method = getattr(requests, f'_original_{method}')
183
+ return original_method(url, **kwargs)
184
+
185
+ # Wrap requests methods
186
+ requests.get = lambda url, **kwargs: _proxy_request('get', url, **kwargs)
187
+ requests.post = lambda url, **kwargs: _proxy_request('post', url, **kwargs)
188
+ requests.put = lambda url, **kwargs: _proxy_request('put', url, **kwargs)
189
+ requests.delete = lambda url, **kwargs: _proxy_request('delete', url, **kwargs)
190
+ requests.patch = lambda url, **kwargs: _proxy_request('patch', url, **kwargs)
191
+
192
+ logger.info("Proxy interception set up for requests library")
193
+
194
+ except ImportError:
195
+ logger.debug("requests library not installed, skipping setup")
196
+
197
+ def setup_google_cloud_proxy():
198
+ """Set up proxy for Google Cloud client libraries"""
199
+ try:
200
+ # Set Google Cloud related environment variables
201
+ if proxy_config.http_proxy:
202
+ os.environ.setdefault('GOOGLE_CLOUD_PROXY', proxy_config.http_proxy)
203
+ os.environ.setdefault('grpc_proxy', proxy_config.http_proxy)
204
+ os.environ.setdefault('GRPC_PROXY', proxy_config.http_proxy)
205
+
206
+ logger.info("Proxy environment variables set for Google Cloud clients")
207
+
208
+ except Exception as e:
209
+ logger.warning(f"Error setting up Google Cloud proxy: {e}")
210
+
211
+ def install_proxy_interceptor():
212
+ """Install proxy interceptor"""
213
+ if not (proxy_config.http_proxy or proxy_config.https_proxy):
214
+ logger.info("No proxy settings detected, skipping proxy interceptor installation")
215
+ return False
216
+
217
+ logger.info("Proxy settings detected, installing proxy interceptor...")
218
+ logger.info(f"HTTP proxy: {proxy_config.http_proxy}")
219
+ logger.info(f"HTTPS proxy: {proxy_config.https_proxy}")
220
+ logger.info(f"Domains requiring proxy: {proxy_config.proxy_domains}")
221
+ logger.info(f"Domains not requiring proxy: {proxy_config.no_proxy_domains}")
222
+
223
+ # Install urllib interceptor
224
+ urllib.request.urlopen = _patched_urlopen
225
+
226
+ # Install socket interceptor (optional, may affect performance)
227
+ # socket.create_connection = _patched_socket_create_connection
228
+
229
+ # Install requests interceptor
230
+ setup_requests_proxy()
231
+
232
+ # Set up Google Cloud proxy
233
+ setup_google_cloud_proxy()
234
+
235
+ logger.info("Proxy interceptor installation complete!")
236
+ return True
237
+
238
+ def uninstall_proxy_interceptor():
239
+ """Uninstall proxy interceptor"""
240
+ logger.info("Uninstalling proxy interceptor...")
241
+
242
+ # Restore original functions
243
+ urllib.request.urlopen = _original_urlopen
244
+ socket.create_connection = _original_socket_create_connection
245
+
246
+ # Restore requests original methods
247
+ try:
248
+ import requests
249
+ if hasattr(requests, '_original_get'):
250
+ requests.get = requests._original_get
251
+ requests.post = requests._original_post
252
+ requests.put = requests._original_put
253
+ requests.delete = requests._original_delete
254
+ requests.patch = requests._original_patch
255
+
256
+ except ImportError:
257
+ pass
258
+
259
+ logger.info("Proxy interceptor uninstallation complete!")
260
+
261
+ def test_proxy_connection():
262
+ """Test proxy connection"""
263
+ test_urls = [
264
+ 'https://www.googleapis.com',
265
+ 'https://accounts.google.com',
266
+ 'https://www.google.com'
267
+ ]
268
+
269
+ for url in test_urls:
270
+ try:
271
+ logger.info(f"Testing connection: {url}")
272
+
273
+ if proxy_config.should_use_proxy(url):
274
+ logger.info(f" -> Will use proxy")
275
+ else:
276
+ logger.info(f" -> Direct connection")
277
+
278
+ response = urllib.request.urlopen(url, timeout=10)
279
+ logger.info(f" -> Success (status code: {response.getcode()})")
280
+
281
+ except Exception as e:
282
+ logger.error(f" -> Failed: {e}")
283
+
284
+ # Auto-install proxy interceptor
285
+ if __name__ == "__main__":
286
+ install_proxy_interceptor()
287
+ test_proxy_connection()
288
+ else:
289
+ # Auto-install when imported as module
290
+ install_proxy_interceptor()
@@ -0,0 +1 @@
1
+ # Services package
@@ -0,0 +1,150 @@
1
+ """
2
+ Project service for managing projects and their configurations
3
+ """
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import uuid
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Dict, List, Optional
12
+
13
+ from ..models.project import Project, ProjectCreate, ProjectUpdate
14
+
15
+
16
+ class ProjectService:
17
+ """Service for managing projects"""
18
+
19
+ def __init__(self, config_dir: str = "config"):
20
+ self.config_dir = Path(config_dir)
21
+ self.projects_file = self.config_dir / "projects.json"
22
+ self._ensure_config_dir()
23
+
24
+ def _ensure_config_dir(self) -> None:
25
+ """Ensure config directory exists"""
26
+ self.config_dir.mkdir(parents=True, exist_ok=True)
27
+ if not self.projects_file.exists():
28
+ self._save_projects({})
29
+
30
+ def _load_projects(self) -> Dict[str, Dict]:
31
+ """Load projects from config file"""
32
+ try:
33
+ with open(self.projects_file, 'r', encoding='utf-8') as f:
34
+ return json.load(f)
35
+ except (FileNotFoundError, json.JSONDecodeError):
36
+ return {}
37
+
38
+ def _save_projects(self, projects: Dict[str, Dict]) -> None:
39
+ """Save projects to config file"""
40
+ with open(self.projects_file, 'w', encoding='utf-8') as f:
41
+ json.dump(projects, f, indent=2, ensure_ascii=False, default=str)
42
+
43
+ async def create_project(self, project_data: ProjectCreate) -> Project:
44
+ """Create a new project"""
45
+ name = project_data.name or str(uuid.uuid4())
46
+ project_id = str(uuid.uuid4())
47
+ now = datetime.utcnow()
48
+
49
+ ## name should be unique and only allow alphanumeric, underscore, hyphen
50
+ ## Auto remove invalid characters
51
+ name = re.sub(r'[^a-zA-Z0-9_-]', '', name)
52
+ exist_project = await self.get_project_by_name(name)
53
+ if exist_project:
54
+ #update existing project
55
+ await self.update_project(exist_project.id, ProjectUpdate(
56
+ name=project_data.name,
57
+ database_type=project_data.database_type,
58
+ database_config=project_data.database_config
59
+ ))
60
+ return exist_project
61
+
62
+ project = Project(
63
+ id=project_id,
64
+ name=name,
65
+ database_type=project_data.database_type,
66
+ database_config=project_data.database_config,
67
+ create_time=now,
68
+ update_time=now
69
+ )
70
+
71
+ projects = self._load_projects()
72
+ projects[project_id] = project.model_dump()
73
+ self._save_projects(projects)
74
+
75
+ return project
76
+
77
+ async def get_project(self, project_id: str) -> Optional[Project]:
78
+ """Get a project by ID"""
79
+ projects = self._load_projects()
80
+ project_data = projects.get(project_id)
81
+
82
+ if project_data:
83
+ return Project(**project_data)
84
+ return None
85
+
86
+ async def get_project_by_name(self, name: str) -> Optional[Project]:
87
+ """Get a project by name"""
88
+ projects = self._load_projects()
89
+ for project_data in projects.values():
90
+ if project_data.get('name') == name:
91
+ return Project(**project_data)
92
+ return None
93
+
94
+ async def list_projects(self) -> List[Project]:
95
+ """List all projects"""
96
+ projects = self._load_projects()
97
+ return [Project(**project_data) for project_data in projects.values()]
98
+
99
+ async def update_project(self, project_id: str, update_data: ProjectUpdate) -> Optional[Project]:
100
+ """Update a project"""
101
+ projects = self._load_projects()
102
+ project_data = projects.get(project_id)
103
+
104
+ if not project_data:
105
+ return None
106
+
107
+ # Update fields
108
+ if update_data.name is not None:
109
+ project_data['name'] = update_data.name
110
+ if update_data.database_config is not None:
111
+ project_data['database_config'] = update_data.database_config.model_dump()
112
+
113
+ project_data['update_time'] = datetime.utcnow().isoformat()
114
+
115
+ projects[project_id] = project_data
116
+ self._save_projects(projects)
117
+
118
+ return Project(**project_data)
119
+
120
+ async def update_project_token(self, project_id: str, token: str, last_refreshed: float, expires_in: Optional[int] = None) -> Optional[Project]:
121
+ """Update project OAuth token and last refreshed timestamp"""
122
+ projects = self._load_projects()
123
+ project_data = projects.get(project_id)
124
+
125
+ if not project_data:
126
+ return None
127
+
128
+ # Update OAuth config token fields
129
+ if 'database_config' in project_data and 'oauth_config' in project_data['database_config']:
130
+ oauth_config = project_data['database_config']['oauth_config']
131
+ oauth_config['token'] = token
132
+ oauth_config['last_refreshed'] = last_refreshed
133
+ if expires_in is not None:
134
+ oauth_config['expires_in'] = expires_in
135
+
136
+ project_data['update_time'] = datetime.utcnow().isoformat()
137
+
138
+ projects[project_id] = project_data
139
+ self._save_projects(projects)
140
+
141
+ return Project(**project_data)
142
+
143
+ async def delete_project(self, project_id: str) -> bool:
144
+ """Delete a project"""
145
+ projects = self._load_projects()
146
+ if project_id in projects:
147
+ del projects[project_id]
148
+ self._save_projects(projects)
149
+ return True
150
+ return False
@@ -0,0 +1 @@
1
+ <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/static/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="GraphXR Database Proxy - Secure middleware for connecting GraphXR to databases"/><title>GraphXR Database Proxy</title><link rel="icon" href="/static/favicon.ico"><script defer="defer" src="/static/vendors.70542a99f336a8021013.js"></script><script defer="defer" src="/static/main.ce3fbb85a7bc9452edb9.js"></script><link href="/static/main.7391ee9773c403483393.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
@@ -0,0 +1,175 @@
1
+ /* App layout styles */
2
+ .app-layout {
3
+ min-height: 100vh;
4
+ }
5
+
6
+ .app-content {
7
+ padding: 0;
8
+ background-color: #f5f5f5;
9
+ }
10
+
11
+ .content-wrapper {
12
+ padding: 24px;
13
+ max-width: 1200px;
14
+ margin: 0 auto;
15
+ }
16
+
17
+ /* Header styles */
18
+ .app-header {
19
+ background: #001529;
20
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
21
+ padding: 0;
22
+ }
23
+
24
+ .header-content {
25
+ display: flex;
26
+ justify-content: space-between;
27
+ align-items: center;
28
+ padding: 0 24px;
29
+ max-width: 1200px;
30
+ margin: 0 auto;
31
+ height: 64px;
32
+ }
33
+
34
+ .logo-icon {
35
+ font-size: 24px;
36
+ color: #1890ff;
37
+ }
38
+
39
+ .app-title {
40
+ margin: 0 !important;
41
+ color: #fff;
42
+ font-weight: 600;
43
+ }
44
+
45
+ .header-button {
46
+ color: rgba(255, 255, 255, 0.65);
47
+ }
48
+
49
+ .header-button:hover {
50
+ color: #fff;
51
+ }
52
+
53
+ /* Project management styles */
54
+ .project-management {
55
+ width: 100%;
56
+ }
57
+
58
+ .page-header {
59
+ display: flex;
60
+ justify-content: space-between;
61
+ align-items: center;
62
+ margin-bottom: 24px;
63
+ }
64
+
65
+ .page-header h2 {
66
+ margin: 0;
67
+ }
68
+
69
+ /* Project form styles */
70
+ .ant-form-item-label > label.ant-form-item-required:not(.ant-form-item-required-mark-optional) {
71
+ font-weight: 500;
72
+ }
73
+
74
+ .ant-card .ant-card {
75
+ border: 1px solid #d9d9d9;
76
+ }
77
+
78
+ .ant-card-head {
79
+ border-bottom: 1px solid #d9d9d9;
80
+ }
81
+
82
+ /* API URLs modal styles */
83
+ .ant-input-group {
84
+ display: flex;
85
+ }
86
+
87
+ .ant-input-group .ant-input {
88
+ border-radius: 6px 0 0 6px;
89
+ }
90
+
91
+ .ant-input-group .ant-btn {
92
+ border-radius: 0 6px 6px 0;
93
+ border-left: 0;
94
+ }
95
+
96
+ /* Responsive design */
97
+ @media (max-width: 768px) {
98
+ .content-wrapper {
99
+ padding: 16px;
100
+ }
101
+
102
+ .header-content {
103
+ padding: 0 16px;
104
+ }
105
+
106
+ .page-header {
107
+ flex-direction: column;
108
+ align-items: flex-start;
109
+ gap: 16px;
110
+ }
111
+
112
+ .ant-table {
113
+ font-size: 12px;
114
+ }
115
+
116
+ .ant-table-tbody > tr > td {
117
+ padding: 8px;
118
+ }
119
+ }
120
+ /* Global styles */
121
+ * {
122
+ box-sizing: border-box;
123
+ }
124
+
125
+ body {
126
+ margin: 0;
127
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
128
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
129
+ sans-serif;
130
+ -webkit-font-smoothing: antialiased;
131
+ -moz-osx-font-smoothing: grayscale;
132
+ background-color: #f5f5f5;
133
+ }
134
+
135
+ code {
136
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
137
+ monospace;
138
+ }
139
+
140
+ /* Ant Design customizations */
141
+ .ant-layout {
142
+ min-height: 100vh;
143
+ }
144
+
145
+ .ant-table-thead > tr > th {
146
+ background-color: #fafafa;
147
+ font-weight: 600;
148
+ }
149
+
150
+ .ant-btn {
151
+ border-radius: 6px;
152
+ }
153
+
154
+ .ant-card {
155
+ border-radius: 8px;
156
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px rgba(0, 0, 0, 0.02);
157
+ }
158
+
159
+ .ant-modal {
160
+ border-radius: 8px;
161
+ }
162
+
163
+ .ant-modal-header {
164
+ border-radius: 8px 8px 0 0;
165
+ }
166
+
167
+ .ant-input, .ant-select-selector {
168
+ border-radius: 6px;
169
+ }
170
+
171
+ .ant-form-item-label > label {
172
+ font-weight: 500;
173
+ }
174
+
175
+ /*# sourceMappingURL=main.7391ee9773c403483393.css.map*/
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.7391ee9773c403483393.css","mappings":"AAAA,sBAAsB;AACtB;EACE,iBAAiB;AACnB;;AAEA;EACE,UAAU;EACV,yBAAyB;AAC3B;;AAEA;EACE,aAAa;EACb,iBAAiB;EACjB,cAAc;AAChB;;AAEA,kBAAkB;AAClB;EACE,mBAAmB;EACnB,yCAAyC;EACzC,UAAU;AACZ;;AAEA;EACE,aAAa;EACb,8BAA8B;EAC9B,mBAAmB;EACnB,eAAe;EACf,iBAAiB;EACjB,cAAc;EACd,YAAY;AACd;;AAEA;EACE,eAAe;EACf,cAAc;AAChB;;AAEA;EACE,oBAAoB;EACpB,WAAW;EACX,gBAAgB;AAClB;;AAEA;EACE,gCAAgC;AAClC;;AAEA;EACE,WAAW;AACb;;AAEA,8BAA8B;AAC9B;EACE,WAAW;AACb;;AAEA;EACE,aAAa;EACb,8BAA8B;EAC9B,mBAAmB;EACnB,mBAAmB;AACrB;;AAEA;EACE,SAAS;AACX;;AAEA,wBAAwB;AACxB;EACE,gBAAgB;AAClB;;AAEA;EACE,yBAAyB;AAC3B;;AAEA;EACE,gCAAgC;AAClC;;AAEA,0BAA0B;AAC1B;EACE,aAAa;AACf;;AAEA;EACE,0BAA0B;AAC5B;;AAEA;EACE,0BAA0B;EAC1B,cAAc;AAChB;;AAEA,sBAAsB;AACtB;EACE;IACE,aAAa;EACf;;EAEA;IACE,eAAe;EACjB;;EAEA;IACE,sBAAsB;IACtB,uBAAuB;IACvB,SAAS;EACX;;EAEA;IACE,eAAe;EACjB;;EAEA;IACE,YAAY;EACd;AACF,C;ACtHA,kBAAkB;AAClB;EACE,sBAAsB;AACxB;;AAEA;EACE,SAAS;EACT;;cAEY;EACZ,mCAAmC;EACnC,kCAAkC;EAClC,yBAAyB;AAC3B;;AAEA;EACE;aACW;AACb;;AAEA,8BAA8B;AAC9B;EACE,iBAAiB;AACnB;;AAEA;EACE,yBAAyB;EACzB,gBAAgB;AAClB;;AAEA;EACE,kBAAkB;AACpB;;AAEA;EACE,kBAAkB;EAClB,4GAA4G;AAC9G;;AAEA;EACE,kBAAkB;AACpB;;AAEA;EACE,0BAA0B;AAC5B;;AAEA;EACE,kBAAkB;AACpB;;AAEA;EACE,gBAAgB;AAClB,C","sources":["webpack://graphxr-database-proxy-frontend/./src/styles/App.css","webpack://graphxr-database-proxy-frontend/./src/styles/index.css"],"sourcesContent":["/* App layout styles */\r\n.app-layout {\r\n min-height: 100vh;\r\n}\r\n\r\n.app-content {\r\n padding: 0;\r\n background-color: #f5f5f5;\r\n}\r\n\r\n.content-wrapper {\r\n padding: 24px;\r\n max-width: 1200px;\r\n margin: 0 auto;\r\n}\r\n\r\n/* Header styles */\r\n.app-header {\r\n background: #001529;\r\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\r\n padding: 0;\r\n}\r\n\r\n.header-content {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n padding: 0 24px;\r\n max-width: 1200px;\r\n margin: 0 auto;\r\n height: 64px;\r\n}\r\n\r\n.logo-icon {\r\n font-size: 24px;\r\n color: #1890ff;\r\n}\r\n\r\n.app-title {\r\n margin: 0 !important;\r\n color: #fff;\r\n font-weight: 600;\r\n}\r\n\r\n.header-button {\r\n color: rgba(255, 255, 255, 0.65);\r\n}\r\n\r\n.header-button:hover {\r\n color: #fff;\r\n}\r\n\r\n/* Project management styles */\r\n.project-management {\r\n width: 100%;\r\n}\r\n\r\n.page-header {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-bottom: 24px;\r\n}\r\n\r\n.page-header h2 {\r\n margin: 0;\r\n}\r\n\r\n/* Project form styles */\r\n.ant-form-item-label > label.ant-form-item-required:not(.ant-form-item-required-mark-optional) {\r\n font-weight: 500;\r\n}\r\n\r\n.ant-card .ant-card {\r\n border: 1px solid #d9d9d9;\r\n}\r\n\r\n.ant-card-head {\r\n border-bottom: 1px solid #d9d9d9;\r\n}\r\n\r\n/* API URLs modal styles */\r\n.ant-input-group {\r\n display: flex;\r\n}\r\n\r\n.ant-input-group .ant-input {\r\n border-radius: 6px 0 0 6px;\r\n}\r\n\r\n.ant-input-group .ant-btn {\r\n border-radius: 0 6px 6px 0;\r\n border-left: 0;\r\n}\r\n\r\n/* Responsive design */\r\n@media (max-width: 768px) {\r\n .content-wrapper {\r\n padding: 16px;\r\n }\r\n \r\n .header-content {\r\n padding: 0 16px;\r\n }\r\n \r\n .page-header {\r\n flex-direction: column;\r\n align-items: flex-start;\r\n gap: 16px;\r\n }\r\n \r\n .ant-table {\r\n font-size: 12px;\r\n }\r\n \r\n .ant-table-tbody > tr > td {\r\n padding: 8px;\r\n }\r\n}","/* Global styles */\r\n* {\r\n box-sizing: border-box;\r\n}\r\n\r\nbody {\r\n margin: 0;\r\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\r\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\r\n sans-serif;\r\n -webkit-font-smoothing: antialiased;\r\n -moz-osx-font-smoothing: grayscale;\r\n background-color: #f5f5f5;\r\n}\r\n\r\ncode {\r\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\r\n monospace;\r\n}\r\n\r\n/* Ant Design customizations */\r\n.ant-layout {\r\n min-height: 100vh;\r\n}\r\n\r\n.ant-table-thead > tr > th {\r\n background-color: #fafafa;\r\n font-weight: 600;\r\n}\r\n\r\n.ant-btn {\r\n border-radius: 6px;\r\n}\r\n\r\n.ant-card {\r\n border-radius: 8px;\r\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px rgba(0, 0, 0, 0.02);\r\n}\r\n\r\n.ant-modal {\r\n border-radius: 8px;\r\n}\r\n\r\n.ant-modal-header {\r\n border-radius: 8px 8px 0 0;\r\n}\r\n\r\n.ant-input, .ant-select-selector {\r\n border-radius: 6px;\r\n}\r\n\r\n.ant-form-item-label > label {\r\n font-weight: 500;\r\n}"],"names":[],"sourceRoot":""}