datajunction-ui 0.0.1-a45 → 0.0.1-a45.dev5

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.
@@ -1,223 +1,201 @@
1
1
  import { useEffect, useState } from 'react';
2
- import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
3
- import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
4
2
  import * as React from 'react';
3
+ import DiffIcon from '../../icons/DiffIcon';
4
+ import { labelize } from '../../../utils/form';
5
+ import CommitIcon from '../../icons/CommitIcon';
5
6
 
6
7
  export default function NodeHistory({ node, djClient }) {
7
8
  const [history, setHistory] = useState([]);
8
- const [revisions, setRevisions] = useState([]);
9
9
 
10
10
  useEffect(() => {
11
11
  const fetchData = async () => {
12
12
  if (node) {
13
13
  const data = await djClient.history('node', node.name);
14
- const revisions = await djClient.revisions(node.name);
15
- setHistory(data);
16
- setRevisions(revisions);
14
+ setHistory(data.reverse());
17
15
  }
18
16
  };
19
17
  fetchData().catch(console.error);
20
18
  }, [djClient, node]);
21
19
 
22
20
  const eventData = event => {
21
+ const standard = (
22
+ <>
23
+ <a href={'#'} className={'highlight-svg'} title="Browse Details">
24
+ <CommitIcon /> Details
25
+ </a>
26
+ </>
27
+ );
28
+
29
+ if (event.activity_type === 'update' && event.entity_type === 'node') {
30
+ return (
31
+ <>
32
+ <a href={`/nodes/${event.node}/revisions/${event.details.version}`}>
33
+ <span className={`badge version`}>{event.details.version}</span>
34
+ </a>
35
+ <a
36
+ href={`/nodes/${event.node}/revisions/${event.details.version}`}
37
+ className={'highlight-svg'}
38
+ title="View Diff"
39
+ >
40
+ <DiffIcon /> Diff
41
+ </a>
42
+ </>
43
+ );
44
+ }
45
+ return '';
46
+ };
47
+
48
+ const eventDescription = event => {
49
+ if (event.activity_type === 'create' && event.entity_type === 'node') {
50
+ return (
51
+ <div className="history-left">
52
+ <b style={{ textTransform: 'capitalize' }}>{event.activity_type}</b>{' '}
53
+ {event.entity_type}{' '}
54
+ <b>
55
+ <a href={'/nodes/' + event.entity_name}>{event.entity_name}</a>
56
+ </b>
57
+ </div>
58
+ );
59
+ }
60
+ if (event.activity_type === 'create' && event.entity_type === 'link') {
61
+ return (
62
+ <div className="history-left">
63
+ <b style={{ textTransform: 'capitalize' }}>{event.activity_type}</b>{' '}
64
+ {event.entity_type} from{' '}
65
+ <b>
66
+ <a href={'/nodes/' + event.entity_name}>{event.entity_name}</a>
67
+ </b>{' '}
68
+ to{' '}
69
+ <b>
70
+ <a href={'/nodes/' + event.details.dimension}>
71
+ {event.details.dimension}
72
+ </a>
73
+ </b>
74
+ </div>
75
+ );
76
+ }
77
+ if (event.activity_type === 'update' && event.entity_type === 'node') {
78
+ return (
79
+ <div className="history-left">
80
+ <b style={{ textTransform: 'capitalize' }}>{event.activity_type}</b>{' '}
81
+ {event.entity_type}{' '}
82
+ <b>
83
+ <a href={'/nodes/' + event.entity_name}>{event.entity_name}</a>
84
+ </b>
85
+ </div>
86
+ );
87
+ }
88
+ if (event.activity_type === 'tag' && event.entity_type === 'node') {
89
+ return (
90
+ <div className="history-left">
91
+ Add tag{event.details.tags.length > 1 ? 's' : ''}{' '}
92
+ {event.details.tags.map(tag => (
93
+ <span className={'badge version'}>
94
+ <a href={`/tags/${tag}`}>{tag}</a>
95
+ </span>
96
+ ))}
97
+ </div>
98
+ );
99
+ }
100
+ if (event.activity_type === 'create' && event.entity_type === 'partition') {
101
+ return (
102
+ <div className="history-left">
103
+ Set <b>{event.details.partition.type_} partition</b> on{' '}
104
+ <a href={'/nodes/' + event.node}>{event.details.column}</a>
105
+ </div>
106
+ );
107
+ }
108
+
23
109
  if (
24
110
  event.activity_type === 'set_attribute' &&
25
111
  event.entity_type === 'column_attribute'
26
112
  ) {
27
- return event.details.attributes
28
- .map(attr => (
29
- <div
30
- key={event.id}
31
- role="cell"
32
- aria-label="HistoryAttribute"
33
- aria-hidden="false"
34
- >
35
- Set{' '}
36
- <span className={`badge partition_value`}>
37
- {event.details.column}
38
- </span>{' '}
39
- as{' '}
40
- <span className={`badge partition_value_highlight`}>
41
- {attr.name}
42
- </span>
43
- </div>
44
- ))
45
- .reduce((prev, curr) => [prev, <br />, curr], []);
113
+ return (
114
+ <div className="history-left">
115
+ <b>Set column attributes</b> on{' '}
116
+ <b>
117
+ <a href={'/nodes/' + event.node}>{event.node}</a>
118
+ </b>
119
+ </div>
120
+ );
46
121
  }
47
- if (event.activity_type === 'create' && event.entity_type === 'link') {
122
+
123
+ if (event.entity_type === 'materialization') {
48
124
  return (
49
- <div
50
- key={event.id}
51
- role="cell"
52
- aria-label="HistoryCreateLink"
53
- aria-hidden="false"
54
- >
55
- Linked{' '}
56
- <span className={`badge partition_value`}>
57
- {event.details.column}
58
- </span>{' '}
59
- to
60
- <span className={`badge partition_value_highlight`}>
61
- {event.details.dimension}
125
+ <div className="history-left">
126
+ <span style={{ textTransform: 'capitalize' }}>
127
+ {event.activity_type}
62
128
  </span>{' '}
63
- via
64
- <span className={`badge partition_value`}>
65
- {event.details.dimension_column}
66
- </span>
129
+ <b>{event.entity_type}</b>{' '}
130
+ <a href={`/nodes/${event.node}/materializations`}>
131
+ {event.entity_name}
132
+ </a>
67
133
  </div>
68
134
  );
69
135
  }
136
+
70
137
  if (
71
- event.activity_type === 'create' &&
72
- event.entity_type === 'materialization'
138
+ event.activity_type === 'status_change' &&
139
+ event.entity_type === 'node'
73
140
  ) {
74
141
  return (
75
- <div
76
- key={event.id}
77
- role="cell"
78
- aria-label="HistoryCreateMaterialization"
79
- aria-hidden="false"
80
- >
81
- Initialized materialization{' '}
82
- <span className={`badge partition_value`}>
83
- {event.details.materialization}
84
- </span>
142
+ <div className="history-left">
143
+ <b style={{ textTransform: 'capitalize' }}>
144
+ {labelize(event.activity_type)}
145
+ </b>{' '}
146
+ on <a href={`/nodes/${event.node}`}>{event.node}</a> from{' '}
147
+ <b>{event.pre.status}</b> to <b>{event.post.status}</b>
85
148
  </div>
86
149
  );
87
150
  }
151
+
88
152
  if (
89
153
  event.activity_type === 'create' &&
90
154
  event.entity_type === 'availability'
91
155
  ) {
92
156
  return (
93
- <div
94
- key={event.id}
95
- role="cell"
96
- aria-label="HistoryCreateAvailability"
97
- aria-hidden="false"
98
- >
99
- Materialized at{' '}
100
- <span className={`badge partition_value_highlight`}>
157
+ <div className="history-left">
158
+ Materialized table{' '}
159
+ <code>
101
160
  {event.post.catalog}.{event.post.schema_}.{event.post.table}
102
- </span>
103
- from{' '}
104
- <span className={`badge partition_value`}>
105
- {event.post.min_temporal_partition}
106
- </span>{' '}
107
- to
108
- <span className={`badge partition_value`}>
109
- {event.post.max_temporal_partition}
110
- </span>
161
+ </code>{' '}
162
+ with partitions between {event.post.min_temporal_partition} and{' '}
163
+ {event.post.max_temporal_partition} available for{' '}
164
+ <a href={'/nodes/' + event.node}>{event.node}</a>.
111
165
  </div>
112
166
  );
113
167
  }
114
- if (
115
- event.activity_type === 'status_change' &&
116
- event.entity_type === 'node'
117
- ) {
118
- const expr = (
119
- <div>
120
- Caused by a change in upstream{' '}
121
- <a href={`/nodes/${event.details['upstream_node']}`}>
122
- {event.details['upstream_node']}
123
- </a>
124
- </div>
125
- );
168
+
169
+ if (event.activity_type === 'create' && event.entity_type === 'backfill') {
126
170
  return (
127
- <div
128
- key={event.id}
129
- role="cell"
130
- aria-label="HistoryNodeStatusChange"
131
- aria-hidden="false"
132
- >
133
- Status changed from{' '}
134
- <span className={`status__${event.pre['status']}`}>
135
- {event.pre['status']}
136
- </span>{' '}
137
- to{' '}
138
- <span className={`status__${event.post['status']}`}>
139
- {event.post['status']}
140
- </span>{' '}
141
- {event.details['upstream_node'] !== undefined ? expr : ''}
171
+ <div className="history-left">
172
+ Backfill created for materialization {event.details.materialization}{' '}
173
+ for partition {event.details.partition.column_name} from{' '}
174
+ {event.details.partition.range[0]} to{' '}
175
+ {event.details.partition.range[1]}
142
176
  </div>
143
177
  );
144
178
  }
145
- return (
146
- <div key={event.id}>
147
- {JSON.stringify(event.details) === '{}'
148
- ? ''
149
- : JSON.stringify(event.details)}
150
- </div>
151
- );
152
179
  };
153
180
 
154
- const tableData = history => {
155
- return history.map(event => (
156
- <tr key={`history-row-${event.id}`}>
157
- <td className="text-start">
158
- <span
159
- className={`history_type__${event.activity_type} badge node_type`}
160
- >
161
- {event.activity_type}
162
- </span>
163
- </td>
164
- <td>{event.entity_type}</td>
165
- <td>{event.entity_name}</td>
166
- <td>{event.user ? event.user : 'unknown'}</td>
167
- <td>{event.created_at}</td>
168
- <td>{eventData(event)}</td>
169
- </tr>
170
- ));
171
- };
181
+ const removeTagNodeEventsWithoutTags = event =>
182
+ event.activity_type !== 'tag' ||
183
+ (event.activity_type === 'tag' &&
184
+ event.entity_type === 'node' &&
185
+ event.details.tags.length > 0);
172
186
 
173
- const revisionsTable = revisions => {
174
- return revisions.map(revision => (
175
- <tr key={revision.version}>
176
- <td className="text-start">
177
- <span className={`badge node_type__source`}>{revision.version}</span>
178
- </td>
179
- <td>{revision.display_name}</td>
180
- <td>{revision.description}</td>
181
- <td>
182
- <SyntaxHighlighter
183
- language="sql"
184
- style={foundation}
185
- wrapLongLines={true}
186
- >
187
- {revision.query}
188
- </SyntaxHighlighter>
189
- </td>
190
- <td>{revision.tags}</td>
191
- </tr>
192
- ));
193
- };
194
187
  return (
195
- <div className="table-vertical">
196
- <table className="card-inner-table table" aria-label="Revisions">
197
- <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
198
- <tr>
199
- <th className="text-start">Version</th>
200
- <th>Display Name</th>
201
- <th>Description</th>
202
- <th>Query</th>
203
- <th>Tags</th>
204
- </tr>
205
- </thead>
206
- <tbody>{revisionsTable(revisions)}</tbody>
207
- </table>
208
- <table className="card-inner-table table" aria-label="Activity">
209
- <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
210
- <tr>
211
- <th className="text-start">Activity</th>
212
- <th>Type</th>
213
- <th>Name</th>
214
- <th>User</th>
215
- <th>Timestamp</th>
216
- <th>Details</th>
217
- </tr>
218
- </thead>
219
- <tbody>{tableData(history)}</tbody>
220
- </table>
221
- </div>
188
+ <ul className="history-border" role="list" aria-label="Activity">
189
+ {history.filter(removeTagNodeEventsWithoutTags).map(event => (
190
+ <li key={`history-row-${event.id}`} className="history">
191
+ {eventDescription(event)}
192
+ <div className={'history-small'}>
193
+ done by <a href="#">{event.user ? event.user : 'unknown'}</a> on{' '}
194
+ {new Date(Date.parse(event.created_at)).toLocaleString()}
195
+ </div>
196
+ <div className="history-right">{eventData(event)}</div>
197
+ </li>
198
+ ))}
199
+ </ul>
222
200
  );
223
201
  }
@@ -0,0 +1,200 @@
1
+ import { useContext, useEffect, useState } from 'react';
2
+ import * as React from 'react';
3
+ import { diffLines, formatLines } from 'unidiff';
4
+ import { parseDiff, Diff, Hunk } from 'react-diff-view';
5
+
6
+ import { useParams } from 'react-router-dom';
7
+ import DJClientContext from '../../providers/djclient';
8
+ import NamespaceHeader from '../../components/NamespaceHeader';
9
+ import { labelize } from '../../../utils/form';
10
+ import DiffIcon from '../../icons/DiffIcon';
11
+ import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
12
+ import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
13
+ import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql';
14
+
15
+ SyntaxHighlighter.registerLanguage('sql', sql);
16
+ foundation.hljs['padding'] = '2rem';
17
+
18
+ export default function RevisionDiff() {
19
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
20
+ const [revisions, setRevisions] = useState([]);
21
+
22
+ const { name, revision } = useParams();
23
+
24
+ useEffect(() => {
25
+ const fetchData = async () => {
26
+ const revisions = await djClient.revisions(name);
27
+ setRevisions(revisions);
28
+ };
29
+ fetchData().catch(console.error);
30
+ }, [djClient, name]);
31
+
32
+ const thisRevision = revisions
33
+ .map((rev, idx) => [idx, rev])
34
+ .filter((rev, idx) => {
35
+ return rev[1].version === revision;
36
+ });
37
+ const prevRevision = revisions.filter(
38
+ (rev, idx) => idx + 1 === thisRevision[0][0],
39
+ );
40
+
41
+ const EMPTY_HUNKS = [];
42
+
43
+ const revisionDiff = (older, newer) => {
44
+ const diffObj = {};
45
+ const fields = [
46
+ 'display_name',
47
+ 'version',
48
+ 'query',
49
+ 'mode',
50
+ 'status',
51
+ 'description',
52
+ 'columns',
53
+ // 'catalog',
54
+ 'schema',
55
+ 'table',
56
+ 'updated_at',
57
+ ];
58
+ if (older) {
59
+ for (const key of fields) {
60
+ if (older[key] && (key !== 'columns' || older.type === 'cube')) {
61
+ diffObj[key] = {};
62
+ diffObj[key].diffText = formatLines(
63
+ diffLines(
64
+ older
65
+ ? key === 'columns'
66
+ ? older[key].map(col => col.name).join('\n')
67
+ : older[key].toString()
68
+ : '',
69
+ newer
70
+ ? key === 'columns'
71
+ ? newer[key].map(col => col.name).join('\n')
72
+ : newer[key].toString()
73
+ : '',
74
+ ),
75
+ {
76
+ context: 5000,
77
+ },
78
+ );
79
+ const [diff] = parseDiff(diffObj[key].diffText, {
80
+ nearbySequences: 'zip',
81
+ });
82
+ diffObj[key].diff = diff;
83
+ }
84
+ }
85
+ }
86
+ return diffObj;
87
+ };
88
+
89
+ const diffObjects = revisionDiff(
90
+ prevRevision[0],
91
+ thisRevision[0] ? thisRevision[0][1] : thisRevision[0],
92
+ );
93
+
94
+ return (
95
+ <div className="node__header">
96
+ <NamespaceHeader
97
+ namespace={(prevRevision[0] ? prevRevision[0].name : '')
98
+ .split('.')
99
+ .slice(0, -1)
100
+ .join('.')}
101
+ />
102
+ <div className="card">
103
+ <div className="card-header">
104
+ <h3
105
+ className="card-title align-items-start flex-column"
106
+ style={{ display: 'inline-block' }}
107
+ >
108
+ <span
109
+ className="card-label fw-bold text-gray-800"
110
+ role="dialog"
111
+ aria-hidden="false"
112
+ aria-label="DisplayName"
113
+ >
114
+ <a
115
+ href={'/nodes/' + prevRevision[0]?.name}
116
+ className=""
117
+ role="dialog"
118
+ aria-hidden="false"
119
+ aria-label="NodeName"
120
+ >
121
+ {prevRevision[0]?.name}
122
+ </a>{' '}
123
+ <span
124
+ className={
125
+ 'node_type__' + prevRevision[0]?.type + ' badge node_type'
126
+ }
127
+ role="dialog"
128
+ aria-hidden="false"
129
+ aria-label="NodeType"
130
+ >
131
+ {prevRevision[0]?.type}
132
+ </span>
133
+ </span>
134
+ </h3>
135
+ <div>
136
+ <span
137
+ className="rounded-pill badge version"
138
+ style={{ marginLeft: '0.5rem', fontSize: '16px' }}
139
+ >
140
+ {prevRevision[0]?.version}
141
+ </span>
142
+ <DiffIcon />
143
+ <span
144
+ className="rounded-pill badge version"
145
+ style={{ marginLeft: '0.3rem', fontSize: '16px' }}
146
+ >
147
+ {thisRevision[0] ? thisRevision[0][1].version : ''}
148
+ </span>{' '}
149
+ </div>
150
+ {Object.keys(diffObjects).map(field => {
151
+ return (
152
+ <div className="diff">
153
+ <h4>
154
+ {labelize(field)}{' '}
155
+ <small className="no-change-banner">
156
+ {diffObjects[field]?.diff.hunks.length > 0
157
+ ? ''
158
+ : 'no change'}
159
+ </small>
160
+ </h4>
161
+ {diffObjects[field]?.diff.hunks.length > 0 ? (
162
+ <Diff
163
+ viewType="split"
164
+ diffType=""
165
+ hunks={diffObjects[field]?.diff.hunks || EMPTY_HUNKS}
166
+ tokens={diffObjects[field]?.tokens}
167
+ >
168
+ {hunks =>
169
+ hunks.map(hunk => <Hunk key={hunk.content} hunk={hunk} />)
170
+ }
171
+ </Diff>
172
+ ) : (
173
+ <div className="no-change">
174
+ {prevRevision[0] ? (
175
+ field === 'query' ? (
176
+ <>
177
+ <SyntaxHighlighter
178
+ language="sql"
179
+ style={foundation}
180
+ wrapLongLines={true}
181
+ >
182
+ {prevRevision[0].query}
183
+ </SyntaxHighlighter>
184
+ </>
185
+ ) : (
186
+ prevRevision[0][field].toString()
187
+ )
188
+ ) : (
189
+ ''
190
+ )}
191
+ </div>
192
+ )}
193
+ </div>
194
+ );
195
+ })}
196
+ </div>
197
+ </div>
198
+ </div>
199
+ );
200
+ }
@@ -501,15 +501,9 @@ describe('<NodePage />', () => {
501
501
  'node',
502
502
  mocks.mockMetricNode.name,
503
503
  );
504
- expect(djClient.DataJunctionAPI.revisions).toHaveBeenCalledWith(
505
- mocks.mockMetricNode.name,
506
- );
507
504
  expect(
508
- screen.getByRole('table', { name: 'Revisions' }),
509
- ).toMatchSnapshot();
510
- expect(screen.getByRole('table', { name: 'Activity' })).toHaveTextContent(
511
- 'ActivityTypeNameUserTimestampDetailscreatenodedefault.avg_repair_priceunknown2023-08-21T16:48:56.950482+00:00',
512
- );
505
+ screen.getByRole('list', { name: 'Activity' }),
506
+ ).toBeInTheDocument();
513
507
  screen
514
508
  .queryAllByRole('cell', {
515
509
  name: 'HistoryAttribute',