@khester/create-dynamics-app 2.1.0 → 2.2.0

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 (121) hide show
  1. package/dist/artifacts/registry.d.ts +4 -3
  2. package/dist/artifacts/registry.d.ts.map +1 -1
  3. package/dist/artifacts/registry.js +121 -11
  4. package/dist/artifacts/registry.js.map +1 -1
  5. package/dist/artifacts/types.d.ts +1 -1
  6. package/dist/artifacts/types.d.ts.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/injectDevTools.d.ts.map +1 -1
  10. package/dist/injectDevTools.js +4 -2
  11. package/dist/injectDevTools.js.map +1 -1
  12. package/dist/scaffold.d.ts +1 -0
  13. package/dist/scaffold.d.ts.map +1 -1
  14. package/dist/scaffold.js +3 -1
  15. package/dist/scaffold.js.map +1 -1
  16. package/package.json +3 -2
  17. package/templates/grid-starter/ARCHITECTURE.md +66 -0
  18. package/templates/grid-starter/README.md +122 -0
  19. package/templates/grid-starter/env.example +16 -0
  20. package/templates/grid-starter/gitignore +6 -0
  21. package/templates/grid-starter/index.html +16 -0
  22. package/templates/grid-starter/package.json +39 -0
  23. package/templates/grid-starter/src/App.tsx +23 -0
  24. package/templates/grid-starter/src/core/services/FetchApiService.ts +117 -0
  25. package/templates/grid-starter/src/core/services/IApiService.ts +37 -0
  26. package/templates/grid-starter/src/core/services/MockApiService.ts +72 -0
  27. package/templates/grid-starter/src/core/services/ServiceFactory.ts +58 -0
  28. package/templates/grid-starter/src/core/services/XrmApiService.ts +135 -0
  29. package/templates/grid-starter/src/core/services/crudLogging.ts +52 -0
  30. package/templates/grid-starter/src/dev-tools/DevPanel.tsx +239 -0
  31. package/templates/grid-starter/src/grid/GridPage.tsx +119 -0
  32. package/templates/grid-starter/src/index.tsx +18 -0
  33. package/templates/grid-starter/src/vite-env.d.ts +15 -0
  34. package/templates/grid-starter/tools/deploy/deploy-webresource.cjs +117 -0
  35. package/templates/grid-starter/tsconfig.json +19 -0
  36. package/templates/grid-starter/vite.config.ts +76 -0
  37. package/templates/pcf-field/_variants/ValueInput.boolean.tsx +2 -0
  38. package/templates/pcf-field/_variants/ValueInput.date.tsx +2 -0
  39. package/templates/pcf-field/_variants/ValueInput.number.tsx +2 -0
  40. package/templates/pcf-field/_variants/ValueInput.optionset.tsx +77 -0
  41. package/templates/pcf-field/_variants/ValueInput.text.tsx +2 -0
  42. package/templates/pcf-field/index.ts +1 -1
  43. package/templates/pcf-field/package.json +3 -1
  44. package/templates/pcf-field/{{componentName}}Component.tsx +2 -0
  45. package/templates/react-custom-page/ARCHITECTURE.md +75 -0
  46. package/templates/react-custom-page/README.md +74 -568
  47. package/templates/react-custom-page/env.example +16 -0
  48. package/templates/react-custom-page/gitignore +1 -0
  49. package/templates/react-custom-page/index.html +16 -0
  50. package/templates/react-custom-page/package.json +21 -49
  51. package/templates/react-custom-page/src/App.tsx +26 -0
  52. package/templates/react-custom-page/src/core/recordContext.test.ts +30 -0
  53. package/templates/react-custom-page/src/core/recordContext.ts +51 -0
  54. package/templates/react-custom-page/src/core/services/FetchApiService.ts +117 -0
  55. package/templates/react-custom-page/src/core/services/IApiService.ts +37 -0
  56. package/templates/react-custom-page/src/core/services/MockApiService.ts +73 -0
  57. package/templates/react-custom-page/src/core/services/ServiceFactory.ts +58 -0
  58. package/templates/react-custom-page/src/core/services/XrmApiService.ts +135 -0
  59. package/templates/react-custom-page/src/core/services/crudLogging.ts +52 -0
  60. package/templates/react-custom-page/src/dev-tools/DevPanel.tsx +238 -0
  61. package/templates/react-custom-page/src/domain/diff.test.ts +87 -0
  62. package/templates/react-custom-page/src/domain/diff.ts +38 -0
  63. package/templates/react-custom-page/src/example/ExamplePage.tsx +140 -0
  64. package/templates/react-custom-page/src/example/exampleError.ts +36 -0
  65. package/templates/react-custom-page/src/example/hooks/useExampleData.ts +40 -0
  66. package/templates/react-custom-page/src/example/hooks/useExampleForm.ts +99 -0
  67. package/templates/react-custom-page/src/example/mappers/accountMapper.test.ts +38 -0
  68. package/templates/react-custom-page/src/example/mappers/accountMapper.ts +55 -0
  69. package/templates/react-custom-page/src/example/models/Account.ts +74 -0
  70. package/templates/react-custom-page/src/index.tsx +18 -128
  71. package/templates/react-custom-page/src/vite-env.d.ts +15 -0
  72. package/templates/react-custom-page/tools/deploy/deploy-webresource.cjs +117 -0
  73. package/templates/react-custom-page/tsconfig.json +12 -22
  74. package/templates/react-custom-page/vite.config.ts +76 -0
  75. package/templates/starter-page/README.md +38 -0
  76. package/templates/starter-page/_variants/App.dashboard.v8.tsx +46 -0
  77. package/templates/starter-page/_variants/App.form.v8.tsx +59 -0
  78. package/templates/starter-page/_variants/App.master-detail.v8.tsx +61 -0
  79. package/templates/starter-page/_variants/App.panel.v8.tsx +99 -0
  80. package/templates/starter-page/gitignore +5 -0
  81. package/templates/starter-page/package.json +27 -0
  82. package/templates/starter-page/public/index.html +11 -0
  83. package/templates/starter-page/src/index.tsx +10 -0
  84. package/templates/starter-page/src/services/dataverse.ts +30 -0
  85. package/templates/starter-page/tsconfig.json +15 -0
  86. package/templates/starter-page/webpack.config.js +17 -0
  87. package/templates/react-custom-page/deployment/README.md +0 -484
  88. package/templates/react-custom-page/docs/ARCHITECTURE_OVERVIEW.md +0 -506
  89. package/templates/react-custom-page/docs/BEST_PRACTICES.md +0 -723
  90. package/templates/react-custom-page/docs/MIGRATION_GUIDE.md +0 -447
  91. package/templates/react-custom-page/public/index.html +0 -15
  92. package/templates/react-custom-page/scripts/custom-build.js +0 -255
  93. package/templates/react-custom-page/src/components/AccountForm.css +0 -71
  94. package/templates/react-custom-page/src/components/AccountForm.tsx +0 -541
  95. package/templates/react-custom-page/src/components/AccountManagement.css +0 -86
  96. package/templates/react-custom-page/src/components/AccountManagement.tsx +0 -370
  97. package/templates/react-custom-page/src/components/ContactForm.css +0 -48
  98. package/templates/react-custom-page/src/components/ContactForm.tsx +0 -327
  99. package/templates/react-custom-page/src/components/ContactManagement.css +0 -86
  100. package/templates/react-custom-page/src/components/ContactManagement.tsx +0 -357
  101. package/templates/react-custom-page/src/components/Logging/LogDialog.tsx +0 -291
  102. package/templates/react-custom-page/src/components/Logging/LoggingContext.tsx +0 -166
  103. package/templates/react-custom-page/src/components/Logging/LoggingDebugPanel.css +0 -192
  104. package/templates/react-custom-page/src/components/Logging/LoggingDebugPanel.tsx +0 -177
  105. package/templates/react-custom-page/src/components/Logging/LoggingProvider.tsx +0 -3
  106. package/templates/react-custom-page/src/components/Logging/logger.ts +0 -193
  107. package/templates/react-custom-page/src/constants/account.ts +0 -410
  108. package/templates/react-custom-page/src/constants/contact.ts +0 -362
  109. package/templates/react-custom-page/src/models/Account.ts +0 -480
  110. package/templates/react-custom-page/src/models/BaseEntity.ts +0 -204
  111. package/templates/react-custom-page/src/models/Contact.ts +0 -580
  112. package/templates/react-custom-page/src/pcf/ContactControlWrapper.tsx +0 -107
  113. package/templates/react-custom-page/src/pcf/MultiEntityControlWrapper.tsx +0 -205
  114. package/templates/react-custom-page/src/providers/DynamicsProvider.tsx +0 -353
  115. package/templates/react-custom-page/src/services/MockApiService.ts +0 -260
  116. package/templates/react-custom-page/src/services/ServiceFactory.ts +0 -65
  117. package/templates/react-custom-page/src/services/XrmApiService.ts +0 -213
  118. package/templates/react-custom-page/src/styles/index.css +0 -171
  119. package/templates/react-custom-page/tools/metadata-sync/index.js +0 -152
  120. package/templates/react-custom-page/webpack.config.js +0 -57
  121. /package/templates/_shared/dev-tools/auth/{get-token.js → get-token.cjs} +0 -0
@@ -1,357 +0,0 @@
1
- import React, { useState, useEffect, useCallback } from 'react';
2
- import {
3
- Button,
4
- TextField,
5
- DetailsList,
6
- Dialog,
7
- Panel,
8
- } from '@khester/dynamics-ui-components';
9
- import {
10
- SelectionMode,
11
- DetailsListLayoutMode,
12
- CheckboxVisibility,
13
- } from '@fluentui/react';
14
- import { useDynamicsApi } from '../providers/DynamicsProvider';
15
- import { Contact } from '../models/Contact';
16
- import { ContactConstants } from '../constants/contact';
17
- import { Logger } from './Logging/logger';
18
- import { ContactForm } from './ContactForm';
19
- import './ContactManagement.css';
20
-
21
- const columns = [
22
- {
23
- key: ContactConstants.PrimaryName,
24
- name: 'Name',
25
- fieldName: ContactConstants.PrimaryName,
26
- minWidth: 150,
27
- maxWidth: 200,
28
- isResizable: true,
29
- },
30
- {
31
- key: ContactConstants.EMailAddress1,
32
- name: 'Email',
33
- fieldName: ContactConstants.EMailAddress1,
34
- minWidth: 200,
35
- maxWidth: 300,
36
- isResizable: true,
37
- },
38
- {
39
- key: ContactConstants.BusinessPhone,
40
- name: 'Phone',
41
- fieldName: ContactConstants.BusinessPhone,
42
- minWidth: 150,
43
- maxWidth: 200,
44
- isResizable: true,
45
- },
46
- {
47
- key: ContactConstants.CreatedOn,
48
- name: 'Created',
49
- fieldName: ContactConstants.CreatedOn,
50
- minWidth: 150,
51
- maxWidth: 200,
52
- isResizable: true,
53
- },
54
- ];
55
-
56
- export const ContactManagement: React.FC = () => {
57
- const { apiService, isEnvironmentMock, environmentType } = useDynamicsApi();
58
-
59
- const [contacts, setContacts] = useState<Contact[]>([]);
60
- const [filteredContacts, setFilteredContacts] = useState<Contact[]>([]);
61
- const [loading, setLoading] = useState(false);
62
- const [searchText, setSearchText] = useState('');
63
- const [selectedContact, setSelectedContact] = useState<Contact | null>(null);
64
- const [showNewContactPanel, setShowNewContactPanel] = useState(false);
65
- const [showEditContactPanel, setShowEditContactPanel] = useState(false);
66
- const [showDeleteDialog, setShowDeleteDialog] = useState(false);
67
- const [contactToDelete, setContactToDelete] = useState<Contact | null>(null);
68
-
69
- useEffect(() => {
70
- Logger.userAction(
71
- 'ContactManagement loaded',
72
- { environmentType, isEnvironmentMock },
73
- 'ContactManagement'
74
- );
75
- }, [environmentType, isEnvironmentMock]);
76
-
77
- const loadContacts = useCallback(async () => {
78
- if (!apiService) {
79
- Logger.error(
80
- 'API service not available',
81
- 'ContactManagement.loadContacts'
82
- );
83
- return;
84
- }
85
-
86
- setLoading(true);
87
- Logger.log('Loading contacts', 'ContactManagement.loadContacts');
88
-
89
- try {
90
- const contactsData = await Contact.retrieveActiveContacts(apiService);
91
-
92
- const contactsWithDisplayData = contactsData.map((contact) => {
93
- const contactInstance = new Contact(contact);
94
- return {
95
- ...contactInstance,
96
- fullname:
97
- `${contact.firstname || ''} ${contact.lastname || ''}`.trim(),
98
- createdon: contact.createdon
99
- ? new Date(contact.createdon).toLocaleDateString()
100
- : '',
101
- } as Contact;
102
- });
103
-
104
- setContacts(contactsWithDisplayData);
105
- setFilteredContacts(contactsWithDisplayData);
106
-
107
- Logger.log(
108
- `Successfully loaded ${contactsWithDisplayData.length} contacts`,
109
- 'ContactManagement.loadContacts'
110
- );
111
- } catch (error) {
112
- Logger.error(
113
- 'Failed to load contacts',
114
- 'ContactManagement.loadContacts',
115
- error
116
- );
117
- } finally {
118
- setLoading(false);
119
- }
120
- }, [apiService]);
121
-
122
- useEffect(() => {
123
- loadContacts();
124
- }, [loadContacts]);
125
-
126
- useEffect(() => {
127
- if (!searchText) {
128
- setFilteredContacts(contacts);
129
- } else {
130
- const filtered = contacts.filter(
131
- (contact) =>
132
- contact.fullname?.toLowerCase().includes(searchText.toLowerCase()) ||
133
- contact.emailaddress1
134
- ?.toLowerCase()
135
- .includes(searchText.toLowerCase()) ||
136
- contact.telephone1?.includes(searchText)
137
- );
138
- setFilteredContacts(filtered);
139
- }
140
- }, [contacts, searchText]);
141
-
142
- const handleItemInvoked = useCallback(
143
- (item: Contact) => {
144
- Logger.userAction(
145
- 'Contact item invoked',
146
- { contactId: item.contactid, contactName: item.fullname },
147
- 'ContactManagement.handleItemInvoked'
148
- );
149
-
150
- if (selectedContact?.contactid === item.contactid) {
151
- setShowEditContactPanel(true);
152
- } else {
153
- setSelectedContact(item);
154
- }
155
- },
156
- [selectedContact]
157
- );
158
-
159
- const handleNewContact = useCallback(() => {
160
- Logger.userAction(
161
- 'New contact panel opened',
162
- {},
163
- 'ContactManagement.handleNewContact'
164
- );
165
- setShowNewContactPanel(true);
166
- }, []);
167
-
168
- const handleEditContact = useCallback(() => {
169
- if (selectedContact) {
170
- Logger.userAction(
171
- 'Edit contact panel opened',
172
- {
173
- contactId: selectedContact.contactid,
174
- contactName: selectedContact.fullname,
175
- },
176
- 'ContactManagement.handleEditContact'
177
- );
178
- setShowEditContactPanel(true);
179
- }
180
- }, [selectedContact]);
181
-
182
- const handleDeleteContact = useCallback(() => {
183
- if (selectedContact) {
184
- Logger.userAction(
185
- 'Delete contact dialog opened',
186
- {
187
- contactId: selectedContact.contactid,
188
- contactName: selectedContact.fullname,
189
- },
190
- 'ContactManagement.handleDeleteContact'
191
- );
192
- setContactToDelete(selectedContact);
193
- setShowDeleteDialog(true);
194
- }
195
- }, [selectedContact]);
196
-
197
- const confirmDelete = useCallback(async () => {
198
- if (contactToDelete && apiService) {
199
- try {
200
- Logger.userAction(
201
- 'Contact deletion confirmed',
202
- {
203
- contactId: contactToDelete.contactid,
204
- contactName: contactToDelete.fullname,
205
- },
206
- 'ContactManagement.confirmDelete'
207
- );
208
-
209
- await Contact.delete(apiService, contactToDelete.contactid!);
210
- await loadContacts();
211
-
212
- setShowDeleteDialog(false);
213
- setContactToDelete(null);
214
- setSelectedContact(null);
215
-
216
- Logger.log(
217
- `Successfully deleted contact: ${contactToDelete.fullname}`,
218
- 'ContactManagement.confirmDelete'
219
- );
220
- } catch (error) {
221
- Logger.error(
222
- `Failed to delete contact: ${contactToDelete.fullname}`,
223
- 'ContactManagement.confirmDelete',
224
- error
225
- );
226
- }
227
- }
228
- }, [contactToDelete, apiService, loadContacts]);
229
-
230
- const handleContactSaved = useCallback(() => {
231
- Logger.userAction(
232
- 'Contact saved, refreshing list',
233
- {},
234
- 'ContactManagement.handleContactSaved'
235
- );
236
- loadContacts();
237
- setShowNewContactPanel(false);
238
- setShowEditContactPanel(false);
239
- setSelectedContact(null);
240
- }, [loadContacts]);
241
-
242
- return (
243
- <div className="contact-management">
244
- <div className="contact-management__header">
245
- <h2>Contact Management</h2>
246
- <div className="contact-management__actions">
247
- <TextField
248
- placeholder="Search contacts..."
249
- value={searchText}
250
- onChange={(_, value) => setSearchText(value || '')}
251
- iconProps={{ iconName: 'Search' }}
252
- />
253
- <Button
254
- text="New Contact"
255
- variant="primary"
256
- onClick={handleNewContact}
257
- iconProps={{ iconName: 'Add' }}
258
- />
259
- </div>
260
- </div>
261
-
262
- <div className="contact-management__toolbar">
263
- <Button
264
- text="Edit"
265
- onClick={handleEditContact}
266
- disabled={!selectedContact}
267
- iconProps={{ iconName: 'Edit' }}
268
- />
269
- <Button
270
- text="Delete"
271
- onClick={handleDeleteContact}
272
- disabled={!selectedContact}
273
- iconProps={{ iconName: 'Delete' }}
274
- />
275
- <Button
276
- text="Refresh"
277
- onClick={loadContacts}
278
- iconProps={{ iconName: 'Refresh' }}
279
- />
280
- </div>
281
-
282
- <div className="contact-management__list">
283
- <DetailsList
284
- items={filteredContacts}
285
- columns={columns}
286
- onItemInvoked={handleItemInvoked}
287
- selectionMode={SelectionMode.single}
288
- layoutMode={DetailsListLayoutMode.justified}
289
- checkboxVisibility={CheckboxVisibility.onHover}
290
- />
291
-
292
- {loading && (
293
- <div className="contact-management__loading">Loading contacts...</div>
294
- )}
295
-
296
- {!loading && filteredContacts.length === 0 && (
297
- <div className="contact-management__empty">
298
- {searchText
299
- ? 'No contacts found matching your search.'
300
- : 'No contacts found. Create your first contact!'}
301
- </div>
302
- )}
303
- </div>
304
-
305
- {/* New Contact Panel */}
306
- <Panel
307
- isOpen={showNewContactPanel}
308
- onDismiss={() => setShowNewContactPanel(false)}
309
- headerText="New Contact"
310
- >
311
- <ContactForm
312
- onSave={handleContactSaved}
313
- onCancel={() => setShowNewContactPanel(false)}
314
- />
315
- </Panel>
316
-
317
- {/* Edit Contact Panel */}
318
- <Panel
319
- isOpen={showEditContactPanel}
320
- onDismiss={() => {
321
- setShowEditContactPanel(false);
322
- setSelectedContact(null);
323
- }}
324
- headerText="Edit Contact"
325
- >
326
- {selectedContact && (
327
- <ContactForm
328
- contactId={selectedContact.contactid}
329
- initialData={{
330
- ...selectedContact,
331
- birthdate: selectedContact.birthdate
332
- ? new Date(selectedContact.birthdate)
333
- : undefined,
334
- }}
335
- onSave={handleContactSaved}
336
- onCancel={() => {
337
- setShowEditContactPanel(false);
338
- setSelectedContact(null);
339
- }}
340
- />
341
- )}
342
- </Panel>
343
-
344
- {/* Delete Confirmation Dialog */}
345
- <Dialog
346
- hidden={!showDeleteDialog}
347
- onDismiss={() => setShowDeleteDialog(false)}
348
- title="Confirm Delete"
349
- content={`Are you sure you want to delete ${contactToDelete?.fullname}? This action cannot be undone.`}
350
- actions={[
351
- { text: 'Delete', onClick: confirmDelete, primary: true },
352
- { text: 'Cancel', onClick: () => setShowDeleteDialog(false) },
353
- ]}
354
- />
355
- </div>
356
- );
357
- };
@@ -1,291 +0,0 @@
1
- import React from 'react';
2
- import {
3
- Dialog,
4
- DynamicsPrimaryButton,
5
- DynamicsDefaultButton,
6
- } from '@khester/dynamics-ui-components';
7
- import { useLogging, LogEntry } from './LoggingContext';
8
-
9
- /**
10
- * Props for LogDialog component
11
- */
12
- interface LogDialogProps {
13
- /** Whether the dialog is open */
14
- isOpen: boolean;
15
- /** Callback when dialog should be closed */
16
- onClose: () => void;
17
- /** Optional title for the dialog */
18
- title?: string;
19
- /** Optional maximum height for the log content */
20
- maxHeight?: string;
21
- }
22
-
23
- /**
24
- * Dialog component for displaying logs in a modal
25
- */
26
- export const LogDialog: React.FC<LogDialogProps> = ({
27
- isOpen,
28
- onClose,
29
- title = 'Application Logs',
30
- maxHeight = '400px',
31
- }) => {
32
- const { logs, clearLogs } = useLogging();
33
-
34
- /**
35
- * Handle clearing all logs
36
- */
37
- const handleClearLogs = () => {
38
- clearLogs();
39
- };
40
-
41
- /**
42
- * Handle copying logs to clipboard
43
- */
44
- const handleCopyLogs = async () => {
45
- const logText = logs.map(formatLogEntry).join('\n');
46
- try {
47
- await navigator.clipboard.writeText(logText);
48
- // Could add a notification here if needed
49
- } catch (error) {
50
- // Fallback for browsers that don't support clipboard API
51
- console.warn('Failed to copy logs to clipboard:', error);
52
- }
53
- };
54
-
55
- /**
56
- * Format a log entry for display
57
- */
58
- const formatLogEntry = (log: LogEntry): string => {
59
- const timestamp = new Date(log.timestamp).toLocaleString();
60
- const source = log.source ? ` [${log.source}]` : '';
61
- return `${timestamp}${source}: ${log.message}`;
62
- };
63
-
64
- /**
65
- * Format a log entry for JSX display
66
- */
67
- const formatLogEntryJSX = (log: LogEntry, index: number): JSX.Element => {
68
- const timestamp = new Date(log.timestamp).toLocaleString();
69
- const source = log.source ? ` [${log.source}]` : '';
70
-
71
- return (
72
- <div key={index} style={{ marginBottom: '8px', fontSize: '12px' }}>
73
- <span style={{ color: '#666', fontFamily: 'monospace' }}>
74
- {timestamp}
75
- </span>
76
- {source && (
77
- <span
78
- style={{
79
- color: '#0078d4',
80
- fontFamily: 'monospace',
81
- marginLeft: '4px',
82
- }}
83
- >
84
- {source}
85
- </span>
86
- )}
87
- <span style={{ fontFamily: 'monospace', marginLeft: '4px' }}>
88
- : {log.message}
89
- </span>
90
- </div>
91
- );
92
- };
93
-
94
- const dialogActions = [
95
- {
96
- text: 'Close',
97
- onClick: onClose,
98
- primary: true,
99
- },
100
- {
101
- text: 'Clear Logs',
102
- onClick: handleClearLogs,
103
- disabled: logs.length === 0,
104
- },
105
- {
106
- text: 'Copy to Clipboard',
107
- onClick: handleCopyLogs,
108
- disabled: logs.length === 0,
109
- },
110
- ];
111
-
112
- return (
113
- <Dialog
114
- hidden={!isOpen}
115
- onDismiss={onClose}
116
- title={title}
117
- subText={`${logs.length} log ${logs.length === 1 ? 'entry' : 'entries'}`}
118
- actions={dialogActions}
119
- content={
120
- <div
121
- style={{
122
- maxHeight: maxHeight,
123
- overflowY: 'auto',
124
- padding: '8px',
125
- backgroundColor: '#f8f8f8',
126
- border: '1px solid #ddd',
127
- borderRadius: '4px',
128
- fontFamily: 'Consolas, "Courier New", monospace',
129
- minWidth: '600px',
130
- maxWidth: '800px',
131
- }}
132
- >
133
- {logs.length === 0 ? (
134
- <div style={{ fontStyle: 'italic', color: '#666' }}>
135
- No logs available
136
- </div>
137
- ) : (
138
- logs.map((log, index) => formatLogEntryJSX(log, index))
139
- )}
140
- </div>
141
- }
142
- />
143
- );
144
- };
145
-
146
- /**
147
- * Simple log viewer component for inline display
148
- */
149
- interface LogViewerProps {
150
- /** Maximum height for the log container */
151
- maxHeight?: string;
152
- /** Whether to show timestamps */
153
- showTimestamps?: boolean;
154
- /** Whether to show source information */
155
- showSources?: boolean;
156
- }
157
-
158
- export const LogViewer: React.FC<LogViewerProps> = ({
159
- maxHeight = '200px',
160
- showTimestamps = true,
161
- showSources = true,
162
- }) => {
163
- const { logs } = useLogging();
164
-
165
- /**
166
- * Format a log entry for inline display
167
- */
168
- const formatLogEntry = (log: LogEntry, index: number): JSX.Element => {
169
- const timestamp = showTimestamps
170
- ? new Date(log.timestamp).toLocaleString()
171
- : '';
172
- const source = showSources && log.source ? ` [${log.source}]` : '';
173
-
174
- return (
175
- <div
176
- key={index}
177
- style={{
178
- marginBottom: '4px',
179
- fontSize: '11px',
180
- fontFamily: 'Consolas, "Courier New", monospace',
181
- lineHeight: '1.4',
182
- }}
183
- >
184
- {showTimestamps && (
185
- <span style={{ color: '#666', marginRight: '8px' }}>{timestamp}</span>
186
- )}
187
- {showSources && source && (
188
- <span style={{ color: '#0078d4', marginRight: '4px' }}>{source}</span>
189
- )}
190
- <span>{log.message}</span>
191
- </div>
192
- );
193
- };
194
-
195
- return (
196
- <div
197
- style={{
198
- maxHeight: maxHeight,
199
- overflowY: 'auto',
200
- padding: '8px',
201
- backgroundColor: '#f8f8f8',
202
- border: '1px solid #ddd',
203
- borderRadius: '4px',
204
- fontSize: '12px',
205
- }}
206
- >
207
- {logs.length === 0 ? (
208
- <div style={{ fontStyle: 'italic', color: '#666', fontSize: '12px' }}>
209
- No logs available
210
- </div>
211
- ) : (
212
- logs.map((log, index) => formatLogEntry(log, index))
213
- )}
214
- </div>
215
- );
216
- };
217
-
218
- /**
219
- * Floating log viewer button that opens the log dialog
220
- */
221
- interface LogViewerButtonProps {
222
- /** Position from bottom in pixels */
223
- bottom?: number;
224
- /** Position from right in pixels */
225
- right?: number;
226
- /** Whether to show log count badge */
227
- showCount?: boolean;
228
- }
229
-
230
- export const LogViewerButton: React.FC<LogViewerButtonProps> = ({
231
- bottom = 20,
232
- right = 20,
233
- showCount = true,
234
- }) => {
235
- const { logs } = useLogging();
236
- const [isDialogOpen, setIsDialogOpen] = React.useState(false);
237
-
238
- const buttonStyle: React.CSSProperties = {
239
- position: 'fixed',
240
- bottom: `${bottom}px`,
241
- right: `${right}px`,
242
- zIndex: 1000,
243
- borderRadius: '50%',
244
- width: '50px',
245
- height: '50px',
246
- backgroundColor: '#0078d4',
247
- border: 'none',
248
- color: 'white',
249
- cursor: 'pointer',
250
- boxShadow: '0 4px 8px rgba(0,0,0,0.2)',
251
- display: 'flex',
252
- alignItems: 'center',
253
- justifyContent: 'center',
254
- fontSize: '20px',
255
- };
256
-
257
- const badgeStyle: React.CSSProperties = {
258
- position: 'absolute',
259
- top: '-5px',
260
- right: '-5px',
261
- backgroundColor: '#d83b01',
262
- color: 'white',
263
- borderRadius: '50%',
264
- width: '20px',
265
- height: '20px',
266
- fontSize: '12px',
267
- display: 'flex',
268
- alignItems: 'center',
269
- justifyContent: 'center',
270
- fontWeight: 'bold',
271
- };
272
-
273
- return (
274
- <>
275
- <button
276
- style={buttonStyle}
277
- onClick={() => setIsDialogOpen(true)}
278
- title={`View logs (${logs.length} entries)`}
279
- >
280
- 📋
281
- {showCount && logs.length > 0 && (
282
- <span style={badgeStyle}>
283
- {logs.length > 99 ? '99+' : logs.length}
284
- </span>
285
- )}
286
- </button>
287
-
288
- <LogDialog isOpen={isDialogOpen} onClose={() => setIsDialogOpen(false)} />
289
- </>
290
- );
291
- };