@khester/create-dynamics-app 2.1.0 → 2.3.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 (122) 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 +122 -12
  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-dataset/package.json +3 -1
  38. package/templates/pcf-field/_variants/ValueInput.boolean.tsx +2 -0
  39. package/templates/pcf-field/_variants/ValueInput.date.tsx +2 -0
  40. package/templates/pcf-field/_variants/ValueInput.number.tsx +2 -0
  41. package/templates/pcf-field/_variants/ValueInput.optionset.tsx +77 -0
  42. package/templates/pcf-field/_variants/ValueInput.text.tsx +2 -0
  43. package/templates/pcf-field/index.ts +1 -1
  44. package/templates/pcf-field/package.json +3 -1
  45. package/templates/pcf-field/{{componentName}}Component.tsx +2 -0
  46. package/templates/react-custom-page/ARCHITECTURE.md +75 -0
  47. package/templates/react-custom-page/README.md +74 -568
  48. package/templates/react-custom-page/env.example +16 -0
  49. package/templates/react-custom-page/gitignore +1 -0
  50. package/templates/react-custom-page/index.html +16 -0
  51. package/templates/react-custom-page/package.json +21 -49
  52. package/templates/react-custom-page/src/App.tsx +26 -0
  53. package/templates/react-custom-page/src/core/recordContext.test.ts +30 -0
  54. package/templates/react-custom-page/src/core/recordContext.ts +51 -0
  55. package/templates/react-custom-page/src/core/services/FetchApiService.ts +117 -0
  56. package/templates/react-custom-page/src/core/services/IApiService.ts +37 -0
  57. package/templates/react-custom-page/src/core/services/MockApiService.ts +73 -0
  58. package/templates/react-custom-page/src/core/services/ServiceFactory.ts +58 -0
  59. package/templates/react-custom-page/src/core/services/XrmApiService.ts +135 -0
  60. package/templates/react-custom-page/src/core/services/crudLogging.ts +52 -0
  61. package/templates/react-custom-page/src/dev-tools/DevPanel.tsx +238 -0
  62. package/templates/react-custom-page/src/domain/diff.test.ts +87 -0
  63. package/templates/react-custom-page/src/domain/diff.ts +38 -0
  64. package/templates/react-custom-page/src/example/ExamplePage.tsx +140 -0
  65. package/templates/react-custom-page/src/example/exampleError.ts +36 -0
  66. package/templates/react-custom-page/src/example/hooks/useExampleData.ts +40 -0
  67. package/templates/react-custom-page/src/example/hooks/useExampleForm.ts +99 -0
  68. package/templates/react-custom-page/src/example/mappers/accountMapper.test.ts +38 -0
  69. package/templates/react-custom-page/src/example/mappers/accountMapper.ts +55 -0
  70. package/templates/react-custom-page/src/example/models/Account.ts +74 -0
  71. package/templates/react-custom-page/src/index.tsx +18 -128
  72. package/templates/react-custom-page/src/vite-env.d.ts +15 -0
  73. package/templates/react-custom-page/tools/deploy/deploy-webresource.cjs +117 -0
  74. package/templates/react-custom-page/tsconfig.json +12 -22
  75. package/templates/react-custom-page/vite.config.ts +76 -0
  76. package/templates/starter-page/README.md +38 -0
  77. package/templates/starter-page/_variants/App.dashboard.v8.tsx +46 -0
  78. package/templates/starter-page/_variants/App.form.v8.tsx +59 -0
  79. package/templates/starter-page/_variants/App.master-detail.v8.tsx +61 -0
  80. package/templates/starter-page/_variants/App.panel.v8.tsx +99 -0
  81. package/templates/starter-page/gitignore +5 -0
  82. package/templates/starter-page/package.json +27 -0
  83. package/templates/starter-page/public/index.html +11 -0
  84. package/templates/starter-page/src/index.tsx +10 -0
  85. package/templates/starter-page/src/services/dataverse.ts +30 -0
  86. package/templates/starter-page/tsconfig.json +15 -0
  87. package/templates/starter-page/webpack.config.js +17 -0
  88. package/templates/react-custom-page/deployment/README.md +0 -484
  89. package/templates/react-custom-page/docs/ARCHITECTURE_OVERVIEW.md +0 -506
  90. package/templates/react-custom-page/docs/BEST_PRACTICES.md +0 -723
  91. package/templates/react-custom-page/docs/MIGRATION_GUIDE.md +0 -447
  92. package/templates/react-custom-page/public/index.html +0 -15
  93. package/templates/react-custom-page/scripts/custom-build.js +0 -255
  94. package/templates/react-custom-page/src/components/AccountForm.css +0 -71
  95. package/templates/react-custom-page/src/components/AccountForm.tsx +0 -541
  96. package/templates/react-custom-page/src/components/AccountManagement.css +0 -86
  97. package/templates/react-custom-page/src/components/AccountManagement.tsx +0 -370
  98. package/templates/react-custom-page/src/components/ContactForm.css +0 -48
  99. package/templates/react-custom-page/src/components/ContactForm.tsx +0 -327
  100. package/templates/react-custom-page/src/components/ContactManagement.css +0 -86
  101. package/templates/react-custom-page/src/components/ContactManagement.tsx +0 -357
  102. package/templates/react-custom-page/src/components/Logging/LogDialog.tsx +0 -291
  103. package/templates/react-custom-page/src/components/Logging/LoggingContext.tsx +0 -166
  104. package/templates/react-custom-page/src/components/Logging/LoggingDebugPanel.css +0 -192
  105. package/templates/react-custom-page/src/components/Logging/LoggingDebugPanel.tsx +0 -177
  106. package/templates/react-custom-page/src/components/Logging/LoggingProvider.tsx +0 -3
  107. package/templates/react-custom-page/src/components/Logging/logger.ts +0 -193
  108. package/templates/react-custom-page/src/constants/account.ts +0 -410
  109. package/templates/react-custom-page/src/constants/contact.ts +0 -362
  110. package/templates/react-custom-page/src/models/Account.ts +0 -480
  111. package/templates/react-custom-page/src/models/BaseEntity.ts +0 -204
  112. package/templates/react-custom-page/src/models/Contact.ts +0 -580
  113. package/templates/react-custom-page/src/pcf/ContactControlWrapper.tsx +0 -107
  114. package/templates/react-custom-page/src/pcf/MultiEntityControlWrapper.tsx +0 -205
  115. package/templates/react-custom-page/src/providers/DynamicsProvider.tsx +0 -353
  116. package/templates/react-custom-page/src/services/MockApiService.ts +0 -260
  117. package/templates/react-custom-page/src/services/ServiceFactory.ts +0 -65
  118. package/templates/react-custom-page/src/services/XrmApiService.ts +0 -213
  119. package/templates/react-custom-page/src/styles/index.css +0 -171
  120. package/templates/react-custom-page/tools/metadata-sync/index.js +0 -152
  121. package/templates/react-custom-page/webpack.config.js +0 -57
  122. /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
- };