@khester/create-dynamics-app 1.0.8 → 1.1.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 (107) hide show
  1. package/bin/create-dynamics-app.js +1 -1
  2. package/dist/index.js +140 -15
  3. package/dist/index.js.map +1 -1
  4. package/dist/utils/consultingHelpers.d.ts +13 -0
  5. package/dist/utils/consultingHelpers.d.ts.map +1 -0
  6. package/dist/utils/consultingHelpers.js +569 -0
  7. package/dist/utils/consultingHelpers.js.map +1 -0
  8. package/dist/utils/copyTemplate.d.ts.map +1 -1
  9. package/dist/utils/copyTemplate.js.map +1 -1
  10. package/dist/utils/initGit.d.ts.map +1 -1
  11. package/dist/utils/initGit.js.map +1 -1
  12. package/dist/utils/installDependencies.d.ts.map +1 -1
  13. package/dist/utils/installDependencies.js +3 -2
  14. package/dist/utils/installDependencies.js.map +1 -1
  15. package/dist/utils/updatePackageJson.d.ts +1 -1
  16. package/dist/utils/updatePackageJson.d.ts.map +1 -1
  17. package/dist/utils/updatePackageJson.js +11 -1
  18. package/dist/utils/updatePackageJson.js.map +1 -1
  19. package/package.json +1 -1
  20. package/templates/dynamics-365-starter/INTEGRATION_TEST_RESULTS.md +302 -0
  21. package/templates/dynamics-365-starter/PHASE_4_COMPLETION_SUMMARY.md +305 -0
  22. package/templates/dynamics-365-starter/README.md +566 -137
  23. package/templates/dynamics-365-starter/deployment/QUICKSTART-MAC.md +507 -0
  24. package/templates/dynamics-365-starter/deployment/QUICKSTART-WINDOWS.md +372 -0
  25. package/templates/dynamics-365-starter/deployment/README.md +484 -0
  26. package/templates/dynamics-365-starter/deployment/pipelines/README.md +375 -0
  27. package/templates/dynamics-365-starter/deployment/pipelines/azure-pipelines.yml +330 -0
  28. package/templates/dynamics-365-starter/deployment/pipelines/github-actions.yml +422 -0
  29. package/templates/dynamics-365-starter/deployment/pipelines/jenkins.groovy +636 -0
  30. package/templates/dynamics-365-starter/deployment/scripts/deploy.ps1 +417 -0
  31. package/templates/dynamics-365-starter/deployment/scripts/deploy.sh +582 -0
  32. package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.ps1 +486 -0
  33. package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.sh +567 -0
  34. package/templates/dynamics-365-starter/deployment/scripts/validate-setup.ps1 +703 -0
  35. package/templates/dynamics-365-starter/deployment/scripts/validate-setup.sh +671 -0
  36. package/templates/dynamics-365-starter/docs/ARCHITECTURE_OVERVIEW.md +506 -0
  37. package/templates/dynamics-365-starter/docs/BEST_PRACTICES.md +723 -0
  38. package/templates/dynamics-365-starter/docs/MIGRATION_GUIDE.md +447 -0
  39. package/templates/dynamics-365-starter/docs/team-standards/README.md +273 -0
  40. package/templates/dynamics-365-starter/docs/team-standards/client-onboarding.md +577 -0
  41. package/templates/dynamics-365-starter/docs/team-standards/code-review-checklist.md +359 -0
  42. package/templates/dynamics-365-starter/docs/team-standards/coding-standards.md +700 -0
  43. package/templates/dynamics-365-starter/docs/team-standards/cross-platform-team-guide.md +736 -0
  44. package/templates/dynamics-365-starter/docs/team-standards/development-workflows.md +727 -0
  45. package/templates/dynamics-365-starter/docs/troubleshooting/common-errors.md +758 -0
  46. package/templates/dynamics-365-starter/docs/troubleshooting/platform-specific-issues.md +878 -0
  47. package/templates/dynamics-365-starter/package.json +22 -1
  48. package/templates/dynamics-365-starter/public/index.html +8 -11
  49. package/templates/dynamics-365-starter/scripts/custom-build.js +255 -0
  50. package/templates/dynamics-365-starter/src/client-project-template/README.md +234 -0
  51. package/templates/dynamics-365-starter/src/client-project-template/config/client.template.json +114 -0
  52. package/templates/dynamics-365-starter/src/client-project-template/config/environments/template.json +186 -0
  53. package/templates/dynamics-365-starter/src/client-project-template/scripts/client-setup.js +667 -0
  54. package/templates/dynamics-365-starter/src/components/AccountForm.css +71 -0
  55. package/templates/dynamics-365-starter/src/components/AccountForm.tsx +541 -0
  56. package/templates/dynamics-365-starter/src/components/AccountManagement.css +86 -0
  57. package/templates/dynamics-365-starter/src/components/AccountManagement.tsx +370 -0
  58. package/templates/dynamics-365-starter/src/components/ContactForm.tsx +149 -63
  59. package/templates/dynamics-365-starter/src/components/ContactManagement.tsx +153 -63
  60. package/templates/dynamics-365-starter/src/components/Logging/LogDialog.tsx +291 -0
  61. package/templates/dynamics-365-starter/src/components/Logging/LoggingContext.tsx +166 -0
  62. package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.css +192 -0
  63. package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.tsx +177 -0
  64. package/templates/dynamics-365-starter/src/components/Logging/LoggingProvider.tsx +3 -0
  65. package/templates/dynamics-365-starter/src/components/Logging/logger.ts +193 -0
  66. package/templates/dynamics-365-starter/src/constants/account.ts +410 -0
  67. package/templates/dynamics-365-starter/src/constants/contact.ts +362 -0
  68. package/templates/dynamics-365-starter/src/examples/README.md +52 -0
  69. package/templates/dynamics-365-starter/src/examples/component-examples/opportunity-management.tsx +625 -0
  70. package/templates/dynamics-365-starter/src/examples/entity-examples/opportunity-model.ts +545 -0
  71. package/templates/dynamics-365-starter/src/examples/integration-examples/custom-pcf-wrapper.tsx +722 -0
  72. package/templates/dynamics-365-starter/src/examples/workflow-examples/sales-workflow.ts +662 -0
  73. package/templates/dynamics-365-starter/src/index.tsx +107 -19
  74. package/templates/dynamics-365-starter/src/models/Account.ts +480 -0
  75. package/templates/dynamics-365-starter/src/models/BaseEntity.ts +204 -0
  76. package/templates/dynamics-365-starter/src/models/Contact.ts +580 -0
  77. package/templates/dynamics-365-starter/src/page-templates/EntityDashboard.tsx +519 -0
  78. package/templates/dynamics-365-starter/src/page-templates/EntityDetailPage.tsx +456 -0
  79. package/templates/dynamics-365-starter/src/page-templates/EntityListPage.tsx +406 -0
  80. package/templates/dynamics-365-starter/src/page-templates/RelatedEntitiesPage.tsx +578 -0
  81. package/templates/dynamics-365-starter/src/page-templates/SearchPage.tsx +629 -0
  82. package/templates/dynamics-365-starter/src/pcf/ContactControlWrapper.tsx +75 -22
  83. package/templates/dynamics-365-starter/src/pcf/MultiEntityControlWrapper.tsx +205 -0
  84. package/templates/dynamics-365-starter/src/providers/DynamicsProvider.tsx +297 -80
  85. package/templates/dynamics-365-starter/src/services/MockApiService.ts +260 -0
  86. package/templates/dynamics-365-starter/src/services/ServiceFactory.ts +65 -0
  87. package/templates/dynamics-365-starter/src/services/XrmApiService.ts +213 -0
  88. package/templates/dynamics-365-starter/src/styles/index.css +74 -7
  89. package/templates/dynamics-365-starter/tools/entity-generator/index.js +168 -0
  90. package/templates/dynamics-365-starter/tools/entity-generator/templates/constants.template.ts +124 -0
  91. package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.css +283 -0
  92. package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.tsx +275 -0
  93. package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.css +204 -0
  94. package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.tsx +413 -0
  95. package/templates/dynamics-365-starter/tools/entity-generator/templates/model.template.ts +250 -0
  96. package/templates/dynamics-365-starter/tools/metadata-sync/d365-client.js +410 -0
  97. package/templates/dynamics-365-starter/tools/metadata-sync/index.js +512 -0
  98. package/templates/dynamics-365-starter/tools/metadata-sync/type-generator.js +675 -0
  99. package/templates/dynamics-365-starter/tsconfig.json +11 -8
  100. package/templates/dynamics-365-starter/webpack.config.js +8 -9
  101. package/templates/power-pages-starter/README.md +7 -1
  102. package/templates/power-pages-starter/public/index.html +8 -11
  103. package/templates/power-pages-starter/src/components/ContactForm.tsx +60 -41
  104. package/templates/power-pages-starter/src/index.tsx +3 -3
  105. package/templates/power-pages-starter/src/providers/PowerPagesProvider.tsx +46 -23
  106. package/templates/power-pages-starter/tsconfig.json +3 -9
  107. package/templates/power-pages-starter/webpack.config.js +8 -3
@@ -1,55 +1,52 @@
1
1
  import React, { useState, useEffect, useCallback } from 'react';
2
- import {
3
- Button,
4
- TextField,
2
+ import {
3
+ Button,
4
+ TextField,
5
5
  DetailsList,
6
6
  Dialog,
7
- Panel
7
+ Panel,
8
8
  } from '@khester/dynamics-ui-components';
9
- import { SelectionMode, DetailsListLayoutMode, CheckboxVisibility } from '@fluentui/react';
9
+ import {
10
+ SelectionMode,
11
+ DetailsListLayoutMode,
12
+ CheckboxVisibility,
13
+ } from '@fluentui/react';
10
14
  import { useDynamicsApi } from '../providers/DynamicsProvider';
15
+ import { Contact } from '../models/Contact';
16
+ import { ContactConstants } from '../constants/contact';
17
+ import { Logger } from './Logging/logger';
11
18
  import { ContactForm } from './ContactForm';
12
19
  import './ContactManagement.css';
13
20
 
14
- interface Contact {
15
- contactid: string;
16
- firstname: string;
17
- lastname: string;
18
- emailaddress1: string;
19
- telephone1: string;
20
- createdon: string;
21
- fullname?: string;
22
- }
23
-
24
21
  const columns = [
25
22
  {
26
- key: 'fullname',
23
+ key: ContactConstants.PrimaryName,
27
24
  name: 'Name',
28
- fieldName: 'fullname',
25
+ fieldName: ContactConstants.PrimaryName,
29
26
  minWidth: 150,
30
27
  maxWidth: 200,
31
28
  isResizable: true,
32
29
  },
33
30
  {
34
- key: 'emailaddress1',
31
+ key: ContactConstants.EMailAddress1,
35
32
  name: 'Email',
36
- fieldName: 'emailaddress1',
33
+ fieldName: ContactConstants.EMailAddress1,
37
34
  minWidth: 200,
38
35
  maxWidth: 300,
39
36
  isResizable: true,
40
37
  },
41
38
  {
42
- key: 'telephone1',
39
+ key: ContactConstants.BusinessPhone,
43
40
  name: 'Phone',
44
- fieldName: 'telephone1',
41
+ fieldName: ContactConstants.BusinessPhone,
45
42
  minWidth: 150,
46
43
  maxWidth: 200,
47
44
  isResizable: true,
48
45
  },
49
46
  {
50
- key: 'createdon',
47
+ key: ContactConstants.CreatedOn,
51
48
  name: 'Created',
52
- fieldName: 'createdon',
49
+ fieldName: ContactConstants.CreatedOn,
53
50
  minWidth: 150,
54
51
  maxWidth: 200,
55
52
  isResizable: true,
@@ -57,8 +54,8 @@ const columns = [
57
54
  ];
58
55
 
59
56
  export const ContactManagement: React.FC = () => {
60
- const { retrieveMultiple, deleteRecord } = useDynamicsApi();
61
-
57
+ const { apiService, isEnvironmentMock, environmentType } = useDynamicsApi();
58
+
62
59
  const [contacts, setContacts] = useState<Contact[]>([]);
63
60
  const [filteredContacts, setFilteredContacts] = useState<Contact[]>([]);
64
61
  const [loading, setLoading] = useState(false);
@@ -69,26 +66,58 @@ export const ContactManagement: React.FC = () => {
69
66
  const [showDeleteDialog, setShowDeleteDialog] = useState(false);
70
67
  const [contactToDelete, setContactToDelete] = useState<Contact | null>(null);
71
68
 
69
+ useEffect(() => {
70
+ Logger.userAction(
71
+ 'ContactManagement loaded',
72
+ { environmentType, isEnvironmentMock },
73
+ 'ContactManagement'
74
+ );
75
+ }, [environmentType, isEnvironmentMock]);
76
+
72
77
  const loadContacts = useCallback(async () => {
78
+ if (!apiService) {
79
+ Logger.error(
80
+ 'API service not available',
81
+ 'ContactManagement.loadContacts'
82
+ );
83
+ return;
84
+ }
85
+
73
86
  setLoading(true);
87
+ Logger.log('Loading contacts', 'ContactManagement.loadContacts');
88
+
74
89
  try {
75
- const query = '$select=contactid,firstname,lastname,emailaddress1,telephone1,createdon&$orderby=createdon desc';
76
- const response = await retrieveMultiple('contacts', query);
77
-
78
- const contactsWithFullName = response.value.map((contact: Contact) => ({
79
- ...contact,
80
- fullname: `${contact.firstname || ''} ${contact.lastname || ''}`.trim(),
81
- createdon: new Date(contact.createdon).toLocaleDateString()
82
- }));
83
-
84
- setContacts(contactsWithFullName);
85
- setFilteredContacts(contactsWithFullName);
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
+ );
86
111
  } catch (error) {
87
- console.error('Error loading contacts:', error);
112
+ Logger.error(
113
+ 'Failed to load contacts',
114
+ 'ContactManagement.loadContacts',
115
+ error
116
+ );
88
117
  } finally {
89
118
  setLoading(false);
90
119
  }
91
- }, [retrieveMultiple]);
120
+ }, [apiService]);
92
121
 
93
122
  useEffect(() => {
94
123
  loadContacts();
@@ -98,62 +127,118 @@ export const ContactManagement: React.FC = () => {
98
127
  if (!searchText) {
99
128
  setFilteredContacts(contacts);
100
129
  } else {
101
- const filtered = contacts.filter(contact =>
102
- contact.fullname?.toLowerCase().includes(searchText.toLowerCase()) ||
103
- contact.emailaddress1?.toLowerCase().includes(searchText.toLowerCase()) ||
104
- contact.telephone1?.includes(searchText)
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)
105
137
  );
106
138
  setFilteredContacts(filtered);
107
139
  }
108
140
  }, [contacts, searchText]);
109
141
 
110
- const handleItemInvoked = useCallback((item: Contact) => {
111
- if (selectedContact?.contactid === item.contactid) {
112
- setShowEditContactPanel(true);
113
- } else {
114
- setSelectedContact(item);
115
- }
116
- }, [selectedContact]);
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
+ );
117
158
 
118
159
  const handleNewContact = useCallback(() => {
160
+ Logger.userAction(
161
+ 'New contact panel opened',
162
+ {},
163
+ 'ContactManagement.handleNewContact'
164
+ );
119
165
  setShowNewContactPanel(true);
120
166
  }, []);
121
167
 
122
168
  const handleEditContact = useCallback(() => {
123
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
+ );
124
178
  setShowEditContactPanel(true);
125
179
  }
126
180
  }, [selectedContact]);
127
181
 
128
182
  const handleDeleteContact = useCallback(() => {
129
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
+ );
130
192
  setContactToDelete(selectedContact);
131
193
  setShowDeleteDialog(true);
132
194
  }
133
195
  }, [selectedContact]);
134
196
 
135
197
  const confirmDelete = useCallback(async () => {
136
- if (contactToDelete) {
198
+ if (contactToDelete && apiService) {
137
199
  try {
138
- await deleteRecord('contacts', contactToDelete.contactid);
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!);
139
210
  await loadContacts();
211
+
140
212
  setShowDeleteDialog(false);
141
213
  setContactToDelete(null);
142
214
  setSelectedContact(null);
215
+
216
+ Logger.log(
217
+ `Successfully deleted contact: ${contactToDelete.fullname}`,
218
+ 'ContactManagement.confirmDelete'
219
+ );
143
220
  } catch (error) {
144
- console.error('Error deleting contact:', error);
221
+ Logger.error(
222
+ `Failed to delete contact: ${contactToDelete.fullname}`,
223
+ 'ContactManagement.confirmDelete',
224
+ error
225
+ );
145
226
  }
146
227
  }
147
- }, [contactToDelete, deleteRecord, loadContacts]);
228
+ }, [contactToDelete, apiService, loadContacts]);
148
229
 
149
230
  const handleContactSaved = useCallback(() => {
231
+ Logger.userAction(
232
+ 'Contact saved, refreshing list',
233
+ {},
234
+ 'ContactManagement.handleContactSaved'
235
+ );
150
236
  loadContacts();
151
237
  setShowNewContactPanel(false);
152
238
  setShowEditContactPanel(false);
153
239
  setSelectedContact(null);
154
240
  }, [loadContacts]);
155
241
 
156
-
157
242
  return (
158
243
  <div className="contact-management">
159
244
  <div className="contact-management__header">
@@ -203,16 +288,16 @@ export const ContactManagement: React.FC = () => {
203
288
  layoutMode={DetailsListLayoutMode.justified}
204
289
  checkboxVisibility={CheckboxVisibility.onHover}
205
290
  />
206
-
291
+
207
292
  {loading && (
208
- <div className="contact-management__loading">
209
- Loading contacts...
210
- </div>
293
+ <div className="contact-management__loading">Loading contacts...</div>
211
294
  )}
212
-
295
+
213
296
  {!loading && filteredContacts.length === 0 && (
214
297
  <div className="contact-management__empty">
215
- {searchText ? 'No contacts found matching your search.' : 'No contacts found. Create your first contact!'}
298
+ {searchText
299
+ ? 'No contacts found matching your search.'
300
+ : 'No contacts found. Create your first contact!'}
216
301
  </div>
217
302
  )}
218
303
  </div>
@@ -241,7 +326,12 @@ export const ContactManagement: React.FC = () => {
241
326
  {selectedContact && (
242
327
  <ContactForm
243
328
  contactId={selectedContact.contactid}
244
- initialData={selectedContact}
329
+ initialData={{
330
+ ...selectedContact,
331
+ birthdate: selectedContact.birthdate
332
+ ? new Date(selectedContact.birthdate)
333
+ : undefined,
334
+ }}
245
335
  onSave={handleContactSaved}
246
336
  onCancel={() => {
247
337
  setShowEditContactPanel(false);
@@ -259,9 +349,9 @@ export const ContactManagement: React.FC = () => {
259
349
  content={`Are you sure you want to delete ${contactToDelete?.fullname}? This action cannot be undone.`}
260
350
  actions={[
261
351
  { text: 'Delete', onClick: confirmDelete, primary: true },
262
- { text: 'Cancel', onClick: () => setShowDeleteDialog(false) }
352
+ { text: 'Cancel', onClick: () => setShowDeleteDialog(false) },
263
353
  ]}
264
354
  />
265
355
  </div>
266
356
  );
267
- };
357
+ };
@@ -0,0 +1,291 @@
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
+ };