datajunction-ui 0.0.151 → 0.0.152

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datajunction-ui",
3
- "version": "0.0.151",
3
+ "version": "0.0.152",
4
4
  "description": "DataJunction UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -1143,21 +1143,38 @@ export function NamespacePage() {
1143
1143
  namespace={namespace}
1144
1144
  onGitConfigLoaded={setGitConfig}
1145
1145
  >
1146
- <a
1147
- href={`${getDJUrl()}/namespaces/${namespace}/export/yaml`}
1148
- download
1146
+ <button
1147
+ type="button"
1148
+ onClick={async () => {
1149
+ const response = await fetch(
1150
+ `${getDJUrl()}/namespaces/${namespace}/export/yaml`,
1151
+ { method: 'POST', credentials: 'include' },
1152
+ );
1153
+ if (!response.ok) {
1154
+ return;
1155
+ }
1156
+ const blob = await response.blob();
1157
+ const url = URL.createObjectURL(blob);
1158
+ const link = document.createElement('a');
1159
+ const safeName = namespace.replace(/\./g, '_');
1160
+ link.href = url;
1161
+ link.download = `${safeName}_export.zip`;
1162
+ document.body.appendChild(link);
1163
+ link.click();
1164
+ document.body.removeChild(link);
1165
+ URL.revokeObjectURL(url);
1166
+ }}
1149
1167
  style={{
1150
1168
  display: 'inline-flex',
1151
1169
  alignItems: 'center',
1152
1170
  gap: '4px',
1153
- // padding: '6px 12px',
1154
1171
  fontSize: '13px',
1155
1172
  fontWeight: '500',
1156
1173
  color: '#475569',
1157
- // backgroundColor: '#f8fafc',
1158
- // border: '1px solid #e2e8f0',
1174
+ background: 'none',
1175
+ border: 'none',
1176
+ padding: 0,
1159
1177
  borderRadius: '6px',
1160
- textDecoration: 'none',
1161
1178
  cursor: 'pointer',
1162
1179
  transition: 'all 0.15s ease',
1163
1180
  margin: '0.5em 0px 0px 1em',
@@ -1184,7 +1201,7 @@ export function NamespacePage() {
1184
1201
  <polyline points="7 10 12 15 17 10"></polyline>
1185
1202
  <line x1="12" y1="15" x2="12" y2="3"></line>
1186
1203
  </svg>
1187
- </a>
1204
+ </button>
1188
1205
  {showEditControls && <AddNodeDropdown namespace={namespace} />}
1189
1206
  </NamespaceHeader>
1190
1207
 
@@ -61,26 +61,74 @@ export default function NodeColumnTab({ node, djClient }) {
61
61
  };
62
62
 
63
63
  const showColumnPartition = col => {
64
- if (col.partition) {
65
- return (
66
- <>
67
- <span className="node_type badge node_type__blank">
68
- <span className="partition_value badge">
69
- <b>Type:</b> {col.partition.type_}
70
- </span>
71
- <br />
72
- <span className="partition_value badge">
73
- <b>Format:</b> <code>{col.partition.format}</code>
74
- </span>
75
- <br />
76
- <span className="partition_value badge">
77
- <b>Granularity:</b> <code>{col.partition.granularity}</code>
78
- </span>
79
- </span>
80
- </>
81
- );
64
+ if (!col.partition) return '';
65
+ const isTemporal = col.partition.type_ === 'temporal';
66
+ const typeLabel =
67
+ col.partition.type_.charAt(0).toUpperCase() +
68
+ col.partition.type_.slice(1);
69
+ const details = [];
70
+ if (isTemporal) {
71
+ if (col.partition.granularity) details.push(col.partition.granularity);
72
+ if (col.partition.format) details.push(col.partition.format);
82
73
  }
83
- return '';
74
+ const icon = isTemporal ? (
75
+ <svg
76
+ className="partition-cell__icon"
77
+ viewBox="0 0 24 24"
78
+ width="14"
79
+ height="14"
80
+ fill="none"
81
+ stroke="currentColor"
82
+ strokeWidth="2"
83
+ strokeLinecap="round"
84
+ strokeLinejoin="round"
85
+ aria-hidden="true"
86
+ >
87
+ <rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
88
+ <line x1="16" y1="2" x2="16" y2="6" />
89
+ <line x1="8" y1="2" x2="8" y2="6" />
90
+ <line x1="3" y1="10" x2="21" y2="10" />
91
+ </svg>
92
+ ) : (
93
+ <svg
94
+ className="partition-cell__icon"
95
+ viewBox="0 0 24 24"
96
+ width="14"
97
+ height="14"
98
+ fill="none"
99
+ stroke="currentColor"
100
+ strokeWidth="2"
101
+ strokeLinecap="round"
102
+ strokeLinejoin="round"
103
+ aria-hidden="true"
104
+ >
105
+ <line x1="8" y1="6" x2="21" y2="6" />
106
+ <line x1="8" y1="12" x2="21" y2="12" />
107
+ <line x1="8" y1="18" x2="21" y2="18" />
108
+ <line x1="3" y1="6" x2="3.01" y2="6" />
109
+ <line x1="3" y1="12" x2="3.01" y2="12" />
110
+ <line x1="3" y1="18" x2="3.01" y2="18" />
111
+ </svg>
112
+ );
113
+ return (
114
+ <div className="partition-cell">
115
+ {icon}
116
+ <span className="partition-cell__type">{typeLabel}</span>
117
+ {details.length > 0 ? (
118
+ <>
119
+ <span className="partition-cell__dash">—</span>
120
+ <span className="partition-cell__details">
121
+ {details.map((d, i) => (
122
+ <React.Fragment key={i}>
123
+ {i > 0 ? ', ' : ''}
124
+ <code>{d}</code>
125
+ </React.Fragment>
126
+ ))}
127
+ </span>
128
+ </>
129
+ ) : null}
130
+ </div>
131
+ );
84
132
  };
85
133
 
86
134
  const columnList = columns => {
@@ -2,9 +2,8 @@ import { useContext, useEffect, useRef, useState } from 'react';
2
2
  import * as React from 'react';
3
3
  import DJClientContext from '../../providers/djclient';
4
4
  import { Field, Form, Formik } from 'formik';
5
- import { FormikSelect } from '../AddEditNodePage/FormikSelect';
6
5
  import EditIcon from '../../icons/EditIcon';
7
- import { displayMessageAfterSubmit, labelize } from '../../../utils/form';
6
+ import { displayMessageAfterSubmit } from '../../../utils/form';
8
7
 
9
8
  export default function PartitionColumnPopover({ column, node, onSubmit }) {
10
9
  const djClient = useContext(DJClientContext).DataJunctionAPI;
@@ -43,7 +42,17 @@ export default function PartitionColumnPopover({ column, node, onSubmit }) {
43
42
  });
44
43
  }
45
44
  onSubmit();
46
- // window.location.reload();
45
+ };
46
+
47
+ const removePartition = async setStatus => {
48
+ const response = await djClient.removePartition(node.name, column.name);
49
+ if (response.status === 200 || response.status === 201) {
50
+ setStatus({ success: 'Partition removed' });
51
+ onSubmit();
52
+ setPopoverAnchor(false);
53
+ } else {
54
+ setStatus({ failure: `${response.json.message}` });
55
+ }
47
56
  };
48
57
 
49
58
  return (
@@ -59,7 +68,7 @@ export default function PartitionColumnPopover({ column, node, onSubmit }) {
59
68
  <EditIcon />
60
69
  </button>
61
70
  <div
62
- className="popover"
71
+ className="popover partition-popover"
63
72
  role="dialog"
64
73
  aria-label="client-code"
65
74
  style={{ display: popoverAnchor === false ? 'none' : 'block' }}
@@ -69,17 +78,23 @@ export default function PartitionColumnPopover({ column, node, onSubmit }) {
69
78
  initialValues={{
70
79
  column: column.name,
71
80
  node: node.name,
72
- partition_type: '',
73
- format: 'yyyyMMdd',
74
- granularity: 'day',
81
+ partition_type: column.partition?.type_ ?? '',
82
+ format: column.partition?.format ?? 'yyyyMMdd',
83
+ granularity: column.partition?.granularity ?? 'day',
75
84
  }}
76
85
  onSubmit={savePartition}
77
86
  >
78
- {function Render({ values, isSubmitting, status, setFieldValue }) {
87
+ {function Render({ values, isSubmitting, status, setStatus }) {
79
88
  return (
80
89
  <Form>
90
+ <div className="popover-header">
91
+ <div className="popover-title">Partition column</div>
92
+ <div className="popover-subtitle">
93
+ {popoverAnchor ? column.name : null}
94
+ </div>
95
+ </div>
81
96
  {displayMessageAfterSubmit(status)}
82
- <span data-testid="edit-partition">
97
+ <div className="field-group" data-testid="edit-partition">
83
98
  <label htmlFor="partitionType">Partition Type</label>
84
99
  <Field
85
100
  as="select"
@@ -91,7 +106,7 @@ export default function PartitionColumnPopover({ column, node, onSubmit }) {
91
106
  <option value="temporal">Temporal</option>
92
107
  <option value="categorical">Categorical</option>
93
108
  </Field>
94
- </span>
109
+ </div>
95
110
  <input
96
111
  hidden={true}
97
112
  name="column"
@@ -104,70 +119,60 @@ export default function PartitionColumnPopover({ column, node, onSubmit }) {
104
119
  value={node.name}
105
120
  readOnly={true}
106
121
  />
107
- <br />
108
- <br />
109
122
  {values.partition_type === 'temporal' ? (
110
123
  <>
111
- <label htmlFor="partitionFormat">Partition Format</label>
112
- <Field
113
- type="text"
114
- name="format"
115
- id="partitionFormat"
116
- placeholder="Optional temporal partition format (ex: yyyyMMdd)"
117
- />
118
- <br />
119
- <br />
120
- <label htmlFor="partitionGranularity">
121
- Partition Granularity
122
- </label>
123
- <Field
124
- as="select"
125
- name="granularity"
126
- id="partitionGranularity"
127
- placeholder="Granularity"
128
- >
129
- <option value="day">Day</option>
130
- <option value="hour">Hour</option>
131
- </Field>
124
+ <div className="field-group">
125
+ <label htmlFor="partitionFormat">Partition Format</label>
126
+ <Field
127
+ type="text"
128
+ name="format"
129
+ id="partitionFormat"
130
+ placeholder="e.g. yyyyMMdd"
131
+ />
132
+ </div>
133
+ <div className="field-group">
134
+ <label htmlFor="partitionGranularity">
135
+ Partition Granularity
136
+ </label>
137
+ <Field
138
+ as="select"
139
+ name="granularity"
140
+ id="partitionGranularity"
141
+ placeholder="Granularity"
142
+ >
143
+ <option value="day">Day</option>
144
+ <option value="hour">Hour</option>
145
+ </Field>
146
+ </div>
132
147
  </>
133
- ) : (
134
- ''
135
- )}
136
- <button
137
- className="add_node"
138
- type="submit"
139
- aria-label="SaveEditColumn"
140
- aria-hidden="false"
141
- >
142
- Save
143
- </button>
144
- <button
145
- className="delete_button"
146
- type="button"
147
- aria-label="RemovePartition"
148
- aria-hidden="false"
149
- onClick={() => {
150
- setFieldValue('partition_type', '');
151
- setFieldValue('format', '');
152
- setFieldValue('granularity', '');
153
- savePartition(
154
- {
155
- node: node.name,
156
- column: column.name,
157
- partition_type: '',
158
- format: '',
159
- granularity: '',
160
- },
161
- { setSubmitting: () => {}, setStatus: s => {} },
162
- );
163
- }}
148
+ ) : null}
149
+ <div
150
+ className="button-row"
164
151
  style={{
165
- marginLeft: '10px',
166
- backgroundColor: '#dc3545',
152
+ justifyContent: column.partition
153
+ ? 'space-between'
154
+ : 'flex-end',
167
155
  }}
168
156
  >
169
- Remove Partition
170
- </button>
157
+ {column.partition ? (
158
+ <button
159
+ className="remove-link"
160
+ type="button"
161
+ aria-label="RemovePartition"
162
+ onClick={() => removePartition(setStatus)}
163
+ >
164
+ Remove partition
165
+ </button>
166
+ ) : null}
167
+ <button
168
+ className="add_node"
169
+ type="submit"
170
+ aria-label="SaveEditColumn"
171
+ aria-hidden="false"
172
+ >
173
+ Save
174
+ </button>
175
+ </div>
171
176
  </Form>
172
177
  );
173
178
  }}
@@ -1999,6 +1999,16 @@ export const DataJunctionAPI = {
1999
1999
  );
2000
2000
  return { status: response.status, json: await response.json() };
2001
2001
  },
2002
+ removePartition: async function (nodeName, columnName) {
2003
+ const response = await fetch(
2004
+ `${DJ_URL}/nodes/${nodeName}/columns/${columnName}/partition`,
2005
+ {
2006
+ method: 'DELETE',
2007
+ credentials: 'include',
2008
+ },
2009
+ );
2010
+ return { status: response.status, json: await response.json() };
2011
+ },
2002
2012
  materialize: async function (nodeName, jobType, strategy, schedule, config) {
2003
2013
  const response = await fetch(
2004
2014
  `${DJ_URL}/nodes/${nodeName}/materialization`,
@@ -568,6 +568,47 @@ tbody th {
568
568
  background-color: #ccf7e525;
569
569
  color: #00b368;
570
570
  margin: 0.25rem;
571
+ font-size: 0.85rem;
572
+ text-transform: none;
573
+ padding: 0.35em 0.6em;
574
+ }
575
+ .partition_value code {
576
+ font-size: 0.9em;
577
+ color: inherit;
578
+ background: none;
579
+ padding: 0;
580
+ }
581
+ .partition-cell {
582
+ display: inline-flex;
583
+ align-items: center;
584
+ gap: 6px;
585
+ font-size: 0.85rem;
586
+ color: #334155;
587
+ text-transform: none;
588
+ letter-spacing: 0;
589
+ line-height: 1.4;
590
+ white-space: nowrap;
591
+ }
592
+ .partition-cell__icon {
593
+ color: #00794a;
594
+ flex-shrink: 0;
595
+ }
596
+ .partition-cell__type {
597
+ font-weight: 600;
598
+ color: #1e293b;
599
+ }
600
+ .partition-cell__dash {
601
+ color: #cbd5e1;
602
+ }
603
+ .partition-cell__details {
604
+ color: #64748b;
605
+ }
606
+ .partition-cell__details code {
607
+ font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
608
+ font-size: 0.92em;
609
+ background: none;
610
+ padding: 0;
611
+ color: #475569;
571
612
  }
572
613
 
573
614
  .partition_value_highlight {
@@ -1096,6 +1137,87 @@ pre {
1096
1137
  background-color: #f3eeff !important;
1097
1138
  }
1098
1139
 
1140
+ .partition-popover {
1141
+ min-width: 260px;
1142
+ }
1143
+ .partition-popover .popover-header {
1144
+ margin: -4px 0 14px 0;
1145
+ padding-bottom: 10px;
1146
+ border-bottom: 1px solid #f1f5f9;
1147
+ }
1148
+ .partition-popover .popover-title {
1149
+ font-size: 0.7rem;
1150
+ font-weight: 600;
1151
+ text-transform: uppercase;
1152
+ letter-spacing: 0.06em;
1153
+ color: #94a3b8;
1154
+ margin-bottom: 2px;
1155
+ }
1156
+ .partition-popover .popover-subtitle {
1157
+ font-size: 0.95rem;
1158
+ font-weight: 600;
1159
+ color: #1e293b;
1160
+ font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
1161
+ }
1162
+ .partition-popover label {
1163
+ display: block;
1164
+ text-transform: none;
1165
+ font-size: 0.8rem;
1166
+ font-weight: 600;
1167
+ color: #475569;
1168
+ margin-bottom: 4px;
1169
+ letter-spacing: 0.02em;
1170
+ }
1171
+ .partition-popover select,
1172
+ .partition-popover input[type='text'] {
1173
+ width: 100%;
1174
+ background-color: #fff;
1175
+ border: 1px solid #d4d4d8;
1176
+ border-radius: 6px;
1177
+ padding: 6px 10px;
1178
+ font-size: 0.875rem;
1179
+ font-family: inherit;
1180
+ box-shadow: none;
1181
+ box-sizing: border-box;
1182
+ }
1183
+ .partition-popover select:focus,
1184
+ .partition-popover input[type='text']:focus {
1185
+ outline: none;
1186
+ border-color: #5d2f86;
1187
+ }
1188
+ .partition-popover .field-group {
1189
+ margin-bottom: 12px;
1190
+ }
1191
+ .partition-popover .button-row {
1192
+ display: flex;
1193
+ align-items: center;
1194
+ justify-content: space-between;
1195
+ margin-top: 16px;
1196
+ padding-top: 8px;
1197
+ border-top: 1px solid #f1f5f9;
1198
+ }
1199
+ .partition-popover .button-row .add_node {
1200
+ margin: 0 !important;
1201
+ padding: 6px 14px !important;
1202
+ font-size: 0.875rem !important;
1203
+ border-radius: 6px !important;
1204
+ }
1205
+ .partition-popover .remove-link {
1206
+ background: none !important;
1207
+ border: none;
1208
+ color: #b91c1c;
1209
+ font-size: 0.8125rem;
1210
+ font-weight: 500;
1211
+ cursor: pointer;
1212
+ padding: 4px 6px !important;
1213
+ text-transform: none;
1214
+ border-radius: 4px;
1215
+ }
1216
+ .partition-popover .remove-link:hover {
1217
+ background-color: #fef2f2 !important;
1218
+ color: #991b1b;
1219
+ }
1220
+
1099
1221
  .add_node {
1100
1222
  background-color: #5d2f86 !important;
1101
1223
  color: #fff;