datajunction-ui 0.0.93 → 0.0.94
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 +1 -1
- package/src/app/components/NodeComponents.jsx +4 -0
- package/src/app/components/Tab.jsx +11 -16
- package/src/app/components/__tests__/Tab.test.jsx +4 -2
- package/src/app/hooks/useWorkspaceData.js +226 -0
- package/src/app/index.tsx +17 -1
- package/src/app/pages/MyWorkspacePage/ActiveBranchesSection.jsx +38 -107
- package/src/app/pages/MyWorkspacePage/MyNodesSection.jsx +31 -6
- package/src/app/pages/MyWorkspacePage/MyWorkspacePage.css +5 -0
- package/src/app/pages/MyWorkspacePage/NeedsAttentionSection.jsx +86 -100
- package/src/app/pages/MyWorkspacePage/TypeGroupGrid.jsx +7 -11
- package/src/app/pages/MyWorkspacePage/__tests__/ActiveBranchesSection.test.jsx +79 -11
- package/src/app/pages/MyWorkspacePage/__tests__/CollectionsSection.test.jsx +22 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MaterializationsSection.test.jsx +57 -0
- package/src/app/pages/MyWorkspacePage/__tests__/MyNodesSection.test.jsx +60 -18
- package/src/app/pages/MyWorkspacePage/__tests__/MyWorkspacePage.test.jsx +156 -162
- package/src/app/pages/MyWorkspacePage/__tests__/NeedsAttentionSection.test.jsx +17 -18
- package/src/app/pages/MyWorkspacePage/__tests__/NotificationsSection.test.jsx +179 -0
- package/src/app/pages/MyWorkspacePage/__tests__/TypeGroupGrid.test.jsx +169 -49
- package/src/app/pages/MyWorkspacePage/index.jsx +41 -73
- package/src/app/pages/NodePage/NodeDataFlowTab.jsx +464 -0
- package/src/app/pages/NodePage/NodeDependenciesTab.jsx +1 -1
- package/src/app/pages/NodePage/NodeDimensionsTab.jsx +362 -0
- package/src/app/pages/NodePage/NodeLineageTab.jsx +1 -0
- package/src/app/pages/NodePage/NodesWithDimension.jsx +3 -3
- package/src/app/pages/NodePage/__tests__/NodeDataFlowTab.test.jsx +428 -0
- package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +18 -1
- package/src/app/pages/NodePage/__tests__/NodeDimensionsTab.test.jsx +362 -0
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +28 -3
- package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +2 -2
- package/src/app/pages/NodePage/index.jsx +15 -8
- package/src/app/services/DJService.js +73 -6
- package/src/app/services/__tests__/DJService.test.jsx +591 -0
- package/src/styles/index.css +32 -0
|
@@ -2937,4 +2937,595 @@ describe('DataJunctionAPI', () => {
|
|
|
2937
2937
|
expect(result._error).toBe(true);
|
|
2938
2938
|
expect(result._status).toBe(404);
|
|
2939
2939
|
});
|
|
2940
|
+
|
|
2941
|
+
// ===== cubeForPlanner — GraphQL error branch (lines 207-208) =====
|
|
2942
|
+
it('returns null from cubeForPlanner when GraphQL returns errors', async () => {
|
|
2943
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
2944
|
+
fetch.mockResponseOnce(
|
|
2945
|
+
JSON.stringify({ errors: [{ message: 'Not authorized' }] }),
|
|
2946
|
+
);
|
|
2947
|
+
const result = await DataJunctionAPI.cubeForPlanner('default.cube1');
|
|
2948
|
+
expect(result).toBeNull();
|
|
2949
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
2950
|
+
'GraphQL errors:',
|
|
2951
|
+
expect.any(Array),
|
|
2952
|
+
);
|
|
2953
|
+
consoleSpy.mockRestore();
|
|
2954
|
+
});
|
|
2955
|
+
|
|
2956
|
+
// ===== querySystemMetric — !res.ok branch (line 273) =====
|
|
2957
|
+
it('throws from querySystemMetric when response is not ok', async () => {
|
|
2958
|
+
fetch.mockResolvedValueOnce({ ok: false, status: 500 });
|
|
2959
|
+
await expect(
|
|
2960
|
+
DataJunctionAPI.querySystemMetric({ metric: 'system.dj.count' }),
|
|
2961
|
+
).rejects.toThrow('Failed to fetch metric data system.dj.count: 500');
|
|
2962
|
+
});
|
|
2963
|
+
|
|
2964
|
+
// ===== findCubesWithMetrics — non-empty cubeNames (lines 906-932) =====
|
|
2965
|
+
it('calls findCubesWithMetrics with non-empty names', async () => {
|
|
2966
|
+
fetch.mockResponseOnce(
|
|
2967
|
+
JSON.stringify({
|
|
2968
|
+
data: {
|
|
2969
|
+
findNodes: [
|
|
2970
|
+
{
|
|
2971
|
+
name: 'default.cube1',
|
|
2972
|
+
current: {
|
|
2973
|
+
displayName: 'Cube One',
|
|
2974
|
+
cubeMetrics: [{ name: 'default.metric1' }],
|
|
2975
|
+
},
|
|
2976
|
+
},
|
|
2977
|
+
],
|
|
2978
|
+
},
|
|
2979
|
+
}),
|
|
2980
|
+
);
|
|
2981
|
+
const result = await DataJunctionAPI.findCubesWithMetrics([
|
|
2982
|
+
'default.cube1',
|
|
2983
|
+
]);
|
|
2984
|
+
expect(result).toHaveLength(1);
|
|
2985
|
+
expect(result[0].name).toBe('default.cube1');
|
|
2986
|
+
expect(result[0].parents).toEqual([{ name: 'default.metric1' }]);
|
|
2987
|
+
expect(result[0].type).toBe('cube');
|
|
2988
|
+
});
|
|
2989
|
+
|
|
2990
|
+
it('returns empty array from findCubesWithMetrics for empty input', async () => {
|
|
2991
|
+
const result = await DataJunctionAPI.findCubesWithMetrics([]);
|
|
2992
|
+
expect(result).toEqual([]);
|
|
2993
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
2994
|
+
});
|
|
2995
|
+
|
|
2996
|
+
// ===== metricsV3 — useMaterialized=false branch (lines 1224-1225) =====
|
|
2997
|
+
it('calls metricsV3 with useMaterialized=false (spark dialect)', async () => {
|
|
2998
|
+
fetch.mockResponseOnce(JSON.stringify({ sql: 'SELECT ...' }));
|
|
2999
|
+
await DataJunctionAPI.metricsV3(['metric1'], ['dim1'], '', false);
|
|
3000
|
+
const url = fetch.mock.calls[0][0];
|
|
3001
|
+
expect(url).toContain('use_materialized=false');
|
|
3002
|
+
expect(url).toContain('dialect=spark');
|
|
3003
|
+
});
|
|
3004
|
+
|
|
3005
|
+
// ===== data — filters non-empty branch (line 1240) =====
|
|
3006
|
+
it('calls data with filters array', async () => {
|
|
3007
|
+
fetch.mockResolvedValueOnce({
|
|
3008
|
+
ok: true,
|
|
3009
|
+
json: () => Promise.resolve([{ col: 'metric1', value: 42 }]),
|
|
3010
|
+
});
|
|
3011
|
+
await DataJunctionAPI.data(
|
|
3012
|
+
['metric1'],
|
|
3013
|
+
['dim1'],
|
|
3014
|
+
['region = US', 'date > 2024-01-01'],
|
|
3015
|
+
);
|
|
3016
|
+
const url = fetch.mock.calls[0][0];
|
|
3017
|
+
expect(url).toContain('filters=region+%3D+US');
|
|
3018
|
+
});
|
|
3019
|
+
|
|
3020
|
+
// ===== data — !response.ok branch (lines 1247-1248) =====
|
|
3021
|
+
it('throws from data when response is not ok', async () => {
|
|
3022
|
+
fetch.mockResolvedValueOnce({
|
|
3023
|
+
ok: false,
|
|
3024
|
+
status: 400,
|
|
3025
|
+
json: () => Promise.resolve({ message: 'Bad request' }),
|
|
3026
|
+
});
|
|
3027
|
+
await expect(DataJunctionAPI.data(['metric1'], ['dim1'])).rejects.toThrow(
|
|
3028
|
+
'Bad request',
|
|
3029
|
+
);
|
|
3030
|
+
});
|
|
3031
|
+
|
|
3032
|
+
it('throws generic error from data when no message in error body', async () => {
|
|
3033
|
+
fetch.mockResolvedValueOnce({
|
|
3034
|
+
ok: false,
|
|
3035
|
+
status: 500,
|
|
3036
|
+
json: () => Promise.resolve({}),
|
|
3037
|
+
});
|
|
3038
|
+
await expect(DataJunctionAPI.data(['metric1'], ['dim1'])).rejects.toThrow(
|
|
3039
|
+
'Query failed: 500',
|
|
3040
|
+
);
|
|
3041
|
+
});
|
|
3042
|
+
|
|
3043
|
+
// ===== Workspace GraphQL queries (lines 1970-2284) =====
|
|
3044
|
+
it('calls getWorkspaceRecentlyEdited correctly', async () => {
|
|
3045
|
+
fetch.mockResponseOnce(
|
|
3046
|
+
JSON.stringify({
|
|
3047
|
+
data: {
|
|
3048
|
+
findNodesPaginated: { pageInfo: { hasNextPage: false }, edges: [] },
|
|
3049
|
+
},
|
|
3050
|
+
}),
|
|
3051
|
+
);
|
|
3052
|
+
const result = await DataJunctionAPI.getWorkspaceRecentlyEdited(
|
|
3053
|
+
'user@example.com',
|
|
3054
|
+
10,
|
|
3055
|
+
);
|
|
3056
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
3057
|
+
expect.stringContaining('/graphql'),
|
|
3058
|
+
expect.objectContaining({ method: 'POST' }),
|
|
3059
|
+
);
|
|
3060
|
+
const body = JSON.parse(fetch.mock.calls[0][1].body);
|
|
3061
|
+
expect(body.variables.editedBy).toBe('user@example.com');
|
|
3062
|
+
expect(body.variables.limit).toBe(10);
|
|
3063
|
+
});
|
|
3064
|
+
|
|
3065
|
+
it('calls getWorkspaceRecentlyEdited with specific nodeType', async () => {
|
|
3066
|
+
fetch.mockResponseOnce(
|
|
3067
|
+
JSON.stringify({
|
|
3068
|
+
data: {
|
|
3069
|
+
findNodesPaginated: { pageInfo: { hasNextPage: false }, edges: [] },
|
|
3070
|
+
},
|
|
3071
|
+
}),
|
|
3072
|
+
);
|
|
3073
|
+
await DataJunctionAPI.getWorkspaceRecentlyEdited(
|
|
3074
|
+
'user@example.com',
|
|
3075
|
+
5,
|
|
3076
|
+
'METRIC',
|
|
3077
|
+
);
|
|
3078
|
+
const body = JSON.parse(fetch.mock.calls[0][1].body);
|
|
3079
|
+
expect(body.variables.nodeTypes).toEqual(['METRIC']);
|
|
3080
|
+
});
|
|
3081
|
+
|
|
3082
|
+
it('calls getWorkspaceOwnedNodes correctly', async () => {
|
|
3083
|
+
fetch.mockResponseOnce(
|
|
3084
|
+
JSON.stringify({
|
|
3085
|
+
data: {
|
|
3086
|
+
findNodesPaginated: { pageInfo: { hasNextPage: false }, edges: [] },
|
|
3087
|
+
},
|
|
3088
|
+
}),
|
|
3089
|
+
);
|
|
3090
|
+
await DataJunctionAPI.getWorkspaceOwnedNodes('user@example.com', 10);
|
|
3091
|
+
const body = JSON.parse(fetch.mock.calls[0][1].body);
|
|
3092
|
+
expect(body.variables.ownedBy).toBe('user@example.com');
|
|
3093
|
+
expect(body.variables.limit).toBe(10);
|
|
3094
|
+
});
|
|
3095
|
+
|
|
3096
|
+
it('calls getWorkspaceOwnedNodes with specific nodeType', async () => {
|
|
3097
|
+
fetch.mockResponseOnce(
|
|
3098
|
+
JSON.stringify({
|
|
3099
|
+
data: {
|
|
3100
|
+
findNodesPaginated: { pageInfo: { hasNextPage: false }, edges: [] },
|
|
3101
|
+
},
|
|
3102
|
+
}),
|
|
3103
|
+
);
|
|
3104
|
+
await DataJunctionAPI.getWorkspaceOwnedNodes('user@example.com', 5, 'CUBE');
|
|
3105
|
+
const body = JSON.parse(fetch.mock.calls[0][1].body);
|
|
3106
|
+
expect(body.variables.nodeTypes).toEqual(['CUBE']);
|
|
3107
|
+
});
|
|
3108
|
+
|
|
3109
|
+
it('calls getWorkspaceCollections correctly', async () => {
|
|
3110
|
+
fetch.mockResponseOnce(JSON.stringify({ data: { listCollections: [] } }));
|
|
3111
|
+
await DataJunctionAPI.getWorkspaceCollections('user@example.com');
|
|
3112
|
+
const body = JSON.parse(fetch.mock.calls[0][1].body);
|
|
3113
|
+
expect(body.variables.createdBy).toBe('user@example.com');
|
|
3114
|
+
});
|
|
3115
|
+
|
|
3116
|
+
it('calls listAllCollections correctly', async () => {
|
|
3117
|
+
fetch.mockResponseOnce(
|
|
3118
|
+
JSON.stringify({ data: { listCollections: [{ name: 'col1' }] } }),
|
|
3119
|
+
);
|
|
3120
|
+
const result = await DataJunctionAPI.listAllCollections();
|
|
3121
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
3122
|
+
expect.stringContaining('/graphql'),
|
|
3123
|
+
expect.objectContaining({ method: 'POST' }),
|
|
3124
|
+
);
|
|
3125
|
+
expect(result.data.listCollections).toHaveLength(1);
|
|
3126
|
+
});
|
|
3127
|
+
|
|
3128
|
+
it('calls getWorkspaceNodesMissingDescription correctly', async () => {
|
|
3129
|
+
fetch.mockResponseOnce(
|
|
3130
|
+
JSON.stringify({ data: { findNodesPaginated: { edges: [] } } }),
|
|
3131
|
+
);
|
|
3132
|
+
await DataJunctionAPI.getWorkspaceNodesMissingDescription(
|
|
3133
|
+
'user@example.com',
|
|
3134
|
+
5,
|
|
3135
|
+
);
|
|
3136
|
+
const body = JSON.parse(fetch.mock.calls[0][1].body);
|
|
3137
|
+
expect(body.variables.ownedBy).toBe('user@example.com');
|
|
3138
|
+
expect(body.variables.limit).toBe(5);
|
|
3139
|
+
});
|
|
3140
|
+
|
|
3141
|
+
it('calls getWorkspaceInvalidNodes correctly', async () => {
|
|
3142
|
+
fetch.mockResponseOnce(
|
|
3143
|
+
JSON.stringify({ data: { findNodesPaginated: { edges: [] } } }),
|
|
3144
|
+
);
|
|
3145
|
+
await DataJunctionAPI.getWorkspaceInvalidNodes('user@example.com', 10);
|
|
3146
|
+
const body = JSON.parse(fetch.mock.calls[0][1].body);
|
|
3147
|
+
expect(body.variables.ownedBy).toBe('user@example.com');
|
|
3148
|
+
expect(body.variables.statuses).toEqual(['INVALID']);
|
|
3149
|
+
});
|
|
3150
|
+
|
|
3151
|
+
it('calls getWorkspaceOrphanedDimensions correctly', async () => {
|
|
3152
|
+
fetch.mockResponseOnce(
|
|
3153
|
+
JSON.stringify({ data: { findNodesPaginated: { edges: [] } } }),
|
|
3154
|
+
);
|
|
3155
|
+
await DataJunctionAPI.getWorkspaceOrphanedDimensions(
|
|
3156
|
+
'user@example.com',
|
|
3157
|
+
10,
|
|
3158
|
+
);
|
|
3159
|
+
const body = JSON.parse(fetch.mock.calls[0][1].body);
|
|
3160
|
+
expect(body.variables.ownedBy).toBe('user@example.com');
|
|
3161
|
+
expect(body.variables.orphanedDimension).toBe(true);
|
|
3162
|
+
});
|
|
3163
|
+
|
|
3164
|
+
it('calls getWorkspaceDraftNodes correctly', async () => {
|
|
3165
|
+
fetch.mockResponseOnce(
|
|
3166
|
+
JSON.stringify({ data: { findNodesPaginated: { edges: [] } } }),
|
|
3167
|
+
);
|
|
3168
|
+
await DataJunctionAPI.getWorkspaceDraftNodes('user@example.com', 50);
|
|
3169
|
+
const body = JSON.parse(fetch.mock.calls[0][1].body);
|
|
3170
|
+
expect(body.variables.ownedBy).toBe('user@example.com');
|
|
3171
|
+
expect(body.variables.mode).toBe('DRAFT');
|
|
3172
|
+
});
|
|
3173
|
+
|
|
3174
|
+
it('calls getWorkspaceMaterializations correctly', async () => {
|
|
3175
|
+
fetch.mockResponseOnce(
|
|
3176
|
+
JSON.stringify({ data: { findNodesPaginated: { edges: [] } } }),
|
|
3177
|
+
);
|
|
3178
|
+
await DataJunctionAPI.getWorkspaceMaterializations('user@example.com', 20);
|
|
3179
|
+
const body = JSON.parse(fetch.mock.calls[0][1].body);
|
|
3180
|
+
expect(body.variables.ownedBy).toBe('user@example.com');
|
|
3181
|
+
expect(body.variables.hasMaterialization).toBe(true);
|
|
3182
|
+
});
|
|
3183
|
+
|
|
3184
|
+
// ===== getCubeWorkflowUrls — null json branch (lines 2507-2508) =====
|
|
3185
|
+
it('returns empty array from getCubeWorkflowUrls when getCubeDetails returns null json', async () => {
|
|
3186
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
3187
|
+
// getCubeDetails returns null json when response is not ok
|
|
3188
|
+
fetch.mockResponseOnce('Not found', { status: 404 });
|
|
3189
|
+
const result = await DataJunctionAPI.getCubeWorkflowUrls('default.cube1');
|
|
3190
|
+
expect(result).toEqual([]);
|
|
3191
|
+
consoleSpy.mockRestore();
|
|
3192
|
+
});
|
|
3193
|
+
|
|
3194
|
+
// ===== getCubeMaterialization — null json branch (line 2533) =====
|
|
3195
|
+
it('returns null from getCubeMaterialization when getCubeDetails has no json', async () => {
|
|
3196
|
+
fetch.mockResponseOnce('Not found', { status: 404 });
|
|
3197
|
+
const result = await DataJunctionAPI.getCubeMaterialization(
|
|
3198
|
+
'default.cube1',
|
|
3199
|
+
);
|
|
3200
|
+
expect(result).toBeNull();
|
|
3201
|
+
});
|
|
3202
|
+
|
|
3203
|
+
// ===== Git Branch Management APIs (lines 2628-2834) =====
|
|
3204
|
+
it('calls getNamespaceGitConfig correctly', async () => {
|
|
3205
|
+
fetch.mockResolvedValueOnce({
|
|
3206
|
+
ok: true,
|
|
3207
|
+
json: () => Promise.resolve({ repo: 'myorg/myrepo', branch: 'main' }),
|
|
3208
|
+
});
|
|
3209
|
+
const result = await DataJunctionAPI.getNamespaceGitConfig('myproject');
|
|
3210
|
+
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/namespaces/myproject/git`, {
|
|
3211
|
+
method: 'GET',
|
|
3212
|
+
credentials: 'include',
|
|
3213
|
+
});
|
|
3214
|
+
expect(result.repo).toBe('myorg/myrepo');
|
|
3215
|
+
});
|
|
3216
|
+
|
|
3217
|
+
it('returns null from getNamespaceGitConfig on 404', async () => {
|
|
3218
|
+
fetch.mockResolvedValueOnce({ ok: false, status: 404 });
|
|
3219
|
+
const result = await DataJunctionAPI.getNamespaceGitConfig('nonexistent');
|
|
3220
|
+
expect(result).toBeNull();
|
|
3221
|
+
});
|
|
3222
|
+
|
|
3223
|
+
it('throws from getNamespaceGitConfig on non-404 error', async () => {
|
|
3224
|
+
fetch.mockResolvedValueOnce({
|
|
3225
|
+
ok: false,
|
|
3226
|
+
status: 500,
|
|
3227
|
+
json: () => Promise.resolve({ message: 'Server error' }),
|
|
3228
|
+
});
|
|
3229
|
+
await expect(
|
|
3230
|
+
DataJunctionAPI.getNamespaceGitConfig('myproject'),
|
|
3231
|
+
).rejects.toThrow('Server error');
|
|
3232
|
+
});
|
|
3233
|
+
|
|
3234
|
+
it('calls updateNamespaceGitConfig correctly', async () => {
|
|
3235
|
+
const config = { repo: 'myorg/newrepo', branch: 'main' };
|
|
3236
|
+
fetch.mockResolvedValueOnce({
|
|
3237
|
+
ok: true,
|
|
3238
|
+
json: () => Promise.resolve(config),
|
|
3239
|
+
});
|
|
3240
|
+
const result = await DataJunctionAPI.updateNamespaceGitConfig(
|
|
3241
|
+
'myproject',
|
|
3242
|
+
config,
|
|
3243
|
+
);
|
|
3244
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
3245
|
+
`${DJ_URL}/namespaces/myproject/git`,
|
|
3246
|
+
expect.objectContaining({
|
|
3247
|
+
method: 'PATCH',
|
|
3248
|
+
body: JSON.stringify(config),
|
|
3249
|
+
}),
|
|
3250
|
+
);
|
|
3251
|
+
expect(result.repo).toBe('myorg/newrepo');
|
|
3252
|
+
});
|
|
3253
|
+
|
|
3254
|
+
it('returns error object from updateNamespaceGitConfig on failure', async () => {
|
|
3255
|
+
fetch.mockResolvedValueOnce({
|
|
3256
|
+
ok: false,
|
|
3257
|
+
status: 422,
|
|
3258
|
+
json: () => Promise.resolve({ message: 'Invalid config' }),
|
|
3259
|
+
});
|
|
3260
|
+
const result = await DataJunctionAPI.updateNamespaceGitConfig(
|
|
3261
|
+
'myproject',
|
|
3262
|
+
{},
|
|
3263
|
+
);
|
|
3264
|
+
expect(result._error).toBe(true);
|
|
3265
|
+
expect(result._status).toBe(422);
|
|
3266
|
+
expect(result.message).toBe('Invalid config');
|
|
3267
|
+
});
|
|
3268
|
+
|
|
3269
|
+
it('calls deleteNamespaceGitConfig correctly', async () => {
|
|
3270
|
+
fetch.mockResolvedValueOnce({ ok: true, status: 204 });
|
|
3271
|
+
const result = await DataJunctionAPI.deleteNamespaceGitConfig('myproject');
|
|
3272
|
+
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/namespaces/myproject/git`, {
|
|
3273
|
+
method: 'DELETE',
|
|
3274
|
+
credentials: 'include',
|
|
3275
|
+
});
|
|
3276
|
+
expect(result).toEqual({});
|
|
3277
|
+
});
|
|
3278
|
+
|
|
3279
|
+
it('returns error object from deleteNamespaceGitConfig on failure', async () => {
|
|
3280
|
+
fetch.mockResolvedValueOnce({
|
|
3281
|
+
ok: false,
|
|
3282
|
+
status: 403,
|
|
3283
|
+
json: () => Promise.resolve({ message: 'Forbidden' }),
|
|
3284
|
+
});
|
|
3285
|
+
const result = await DataJunctionAPI.deleteNamespaceGitConfig('myproject');
|
|
3286
|
+
expect(result._error).toBe(true);
|
|
3287
|
+
expect(result.message).toBe('Forbidden');
|
|
3288
|
+
});
|
|
3289
|
+
|
|
3290
|
+
it('calls listBranches correctly', async () => {
|
|
3291
|
+
fetch.mockResolvedValueOnce({
|
|
3292
|
+
ok: true,
|
|
3293
|
+
json: () => Promise.resolve([{ branch: 'main' }, { branch: 'feature' }]),
|
|
3294
|
+
});
|
|
3295
|
+
const result = await DataJunctionAPI.listBranches('myproject');
|
|
3296
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
3297
|
+
`${DJ_URL}/namespaces/myproject/branches`,
|
|
3298
|
+
{ method: 'GET', credentials: 'include' },
|
|
3299
|
+
);
|
|
3300
|
+
expect(result).toHaveLength(2);
|
|
3301
|
+
});
|
|
3302
|
+
|
|
3303
|
+
it('throws from listBranches on error', async () => {
|
|
3304
|
+
fetch.mockResolvedValueOnce({
|
|
3305
|
+
ok: false,
|
|
3306
|
+
status: 404,
|
|
3307
|
+
json: () => Promise.resolve({ message: 'Not found' }),
|
|
3308
|
+
});
|
|
3309
|
+
await expect(DataJunctionAPI.listBranches('nonexistent')).rejects.toThrow(
|
|
3310
|
+
'Not found',
|
|
3311
|
+
);
|
|
3312
|
+
});
|
|
3313
|
+
|
|
3314
|
+
it('calls createBranch correctly', async () => {
|
|
3315
|
+
fetch.mockResolvedValueOnce({
|
|
3316
|
+
ok: true,
|
|
3317
|
+
json: () =>
|
|
3318
|
+
Promise.resolve({
|
|
3319
|
+
branch_name: 'feature-1',
|
|
3320
|
+
namespace: 'myproject.feature-1',
|
|
3321
|
+
}),
|
|
3322
|
+
});
|
|
3323
|
+
const result = await DataJunctionAPI.createBranch('myproject', 'feature-1');
|
|
3324
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
3325
|
+
`${DJ_URL}/namespaces/myproject/branches`,
|
|
3326
|
+
expect.objectContaining({
|
|
3327
|
+
method: 'POST',
|
|
3328
|
+
body: JSON.stringify({ branch_name: 'feature-1' }),
|
|
3329
|
+
}),
|
|
3330
|
+
);
|
|
3331
|
+
expect(result.branch_name).toBe('feature-1');
|
|
3332
|
+
});
|
|
3333
|
+
|
|
3334
|
+
it('returns error object from createBranch on failure', async () => {
|
|
3335
|
+
fetch.mockResolvedValueOnce({
|
|
3336
|
+
ok: false,
|
|
3337
|
+
status: 409,
|
|
3338
|
+
json: () => Promise.resolve({ message: 'Branch already exists' }),
|
|
3339
|
+
});
|
|
3340
|
+
const result = await DataJunctionAPI.createBranch('myproject', 'main');
|
|
3341
|
+
expect(result._error).toBe(true);
|
|
3342
|
+
expect(result.message).toBe('Branch already exists');
|
|
3343
|
+
});
|
|
3344
|
+
|
|
3345
|
+
it('calls deleteBranch correctly', async () => {
|
|
3346
|
+
fetch.mockResolvedValueOnce({
|
|
3347
|
+
ok: true,
|
|
3348
|
+
json: () => Promise.resolve({ deleted: true }),
|
|
3349
|
+
});
|
|
3350
|
+
const result = await DataJunctionAPI.deleteBranch(
|
|
3351
|
+
'myproject',
|
|
3352
|
+
'myproject.feature-1',
|
|
3353
|
+
);
|
|
3354
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
3355
|
+
`${DJ_URL}/namespaces/myproject/branches/myproject.feature-1?delete_git_branch=false`,
|
|
3356
|
+
{ method: 'DELETE', credentials: 'include' },
|
|
3357
|
+
);
|
|
3358
|
+
expect(result.deleted).toBe(true);
|
|
3359
|
+
});
|
|
3360
|
+
|
|
3361
|
+
it('calls deleteBranch with deleteGitBranch=true', async () => {
|
|
3362
|
+
fetch.mockResolvedValueOnce({
|
|
3363
|
+
ok: true,
|
|
3364
|
+
json: () => Promise.resolve({ deleted: true }),
|
|
3365
|
+
});
|
|
3366
|
+
await DataJunctionAPI.deleteBranch(
|
|
3367
|
+
'myproject',
|
|
3368
|
+
'myproject.feature-1',
|
|
3369
|
+
true,
|
|
3370
|
+
);
|
|
3371
|
+
const url = fetch.mock.calls[0][0];
|
|
3372
|
+
expect(url).toContain('delete_git_branch=true');
|
|
3373
|
+
});
|
|
3374
|
+
|
|
3375
|
+
it('returns error object from deleteBranch on failure', async () => {
|
|
3376
|
+
fetch.mockResolvedValueOnce({
|
|
3377
|
+
ok: false,
|
|
3378
|
+
status: 404,
|
|
3379
|
+
json: () => Promise.resolve({ message: 'Branch not found' }),
|
|
3380
|
+
});
|
|
3381
|
+
const result = await DataJunctionAPI.deleteBranch(
|
|
3382
|
+
'myproject',
|
|
3383
|
+
'myproject.bad',
|
|
3384
|
+
);
|
|
3385
|
+
expect(result._error).toBe(true);
|
|
3386
|
+
expect(result.message).toBe('Branch not found');
|
|
3387
|
+
});
|
|
3388
|
+
|
|
3389
|
+
it('calls syncNodeToGit correctly', async () => {
|
|
3390
|
+
fetch.mockResolvedValueOnce({
|
|
3391
|
+
ok: true,
|
|
3392
|
+
json: () => Promise.resolve({ synced: true }),
|
|
3393
|
+
});
|
|
3394
|
+
const result = await DataJunctionAPI.syncNodeToGit('default.metric1');
|
|
3395
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
3396
|
+
`${DJ_URL}/nodes/default.metric1/sync-to-git`,
|
|
3397
|
+
expect.objectContaining({ method: 'POST' }),
|
|
3398
|
+
);
|
|
3399
|
+
expect(result.synced).toBe(true);
|
|
3400
|
+
});
|
|
3401
|
+
|
|
3402
|
+
it('calls syncNodeToGit with commitMessage', async () => {
|
|
3403
|
+
fetch.mockResolvedValueOnce({
|
|
3404
|
+
ok: true,
|
|
3405
|
+
json: () => Promise.resolve({ synced: true }),
|
|
3406
|
+
});
|
|
3407
|
+
await DataJunctionAPI.syncNodeToGit('default.metric1', 'Update metric');
|
|
3408
|
+
const body = JSON.parse(fetch.mock.calls[0][1].body);
|
|
3409
|
+
expect(body.commit_message).toBe('Update metric');
|
|
3410
|
+
});
|
|
3411
|
+
|
|
3412
|
+
it('returns error object from syncNodeToGit on failure', async () => {
|
|
3413
|
+
fetch.mockResolvedValueOnce({
|
|
3414
|
+
ok: false,
|
|
3415
|
+
status: 500,
|
|
3416
|
+
json: () => Promise.resolve({ message: 'Git error' }),
|
|
3417
|
+
});
|
|
3418
|
+
const result = await DataJunctionAPI.syncNodeToGit('default.metric1');
|
|
3419
|
+
expect(result._error).toBe(true);
|
|
3420
|
+
expect(result.message).toBe('Git error');
|
|
3421
|
+
});
|
|
3422
|
+
|
|
3423
|
+
it('calls syncNamespaceToGit correctly', async () => {
|
|
3424
|
+
fetch.mockResolvedValueOnce({
|
|
3425
|
+
ok: true,
|
|
3426
|
+
json: () => Promise.resolve({ synced: 5 }),
|
|
3427
|
+
});
|
|
3428
|
+
const result = await DataJunctionAPI.syncNamespaceToGit('myproject.main');
|
|
3429
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
3430
|
+
`${DJ_URL}/namespaces/myproject.main/sync-to-git`,
|
|
3431
|
+
expect.objectContaining({ method: 'POST' }),
|
|
3432
|
+
);
|
|
3433
|
+
expect(result.synced).toBe(5);
|
|
3434
|
+
});
|
|
3435
|
+
|
|
3436
|
+
it('calls syncNamespaceToGit with commitMessage', async () => {
|
|
3437
|
+
fetch.mockResolvedValueOnce({
|
|
3438
|
+
ok: true,
|
|
3439
|
+
json: () => Promise.resolve({ synced: 3 }),
|
|
3440
|
+
});
|
|
3441
|
+
await DataJunctionAPI.syncNamespaceToGit('myproject.main', 'Bulk sync');
|
|
3442
|
+
const body = JSON.parse(fetch.mock.calls[0][1].body);
|
|
3443
|
+
expect(body.commit_message).toBe('Bulk sync');
|
|
3444
|
+
});
|
|
3445
|
+
|
|
3446
|
+
it('returns error object from syncNamespaceToGit on failure', async () => {
|
|
3447
|
+
fetch.mockResolvedValueOnce({
|
|
3448
|
+
ok: false,
|
|
3449
|
+
status: 500,
|
|
3450
|
+
json: () => Promise.resolve({}),
|
|
3451
|
+
});
|
|
3452
|
+
const result = await DataJunctionAPI.syncNamespaceToGit('myproject.main');
|
|
3453
|
+
expect(result._error).toBe(true);
|
|
3454
|
+
expect(result.message).toBe('Failed to sync namespace to git');
|
|
3455
|
+
});
|
|
3456
|
+
|
|
3457
|
+
it('calls getPullRequest correctly', async () => {
|
|
3458
|
+
fetch.mockResolvedValueOnce({
|
|
3459
|
+
ok: true,
|
|
3460
|
+
json: () =>
|
|
3461
|
+
Promise.resolve({
|
|
3462
|
+
number: 42,
|
|
3463
|
+
url: 'https://github.com/org/repo/pull/42',
|
|
3464
|
+
}),
|
|
3465
|
+
});
|
|
3466
|
+
const result = await DataJunctionAPI.getPullRequest('myproject.feature-1');
|
|
3467
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
3468
|
+
`${DJ_URL}/namespaces/myproject.feature-1/pull-request`,
|
|
3469
|
+
{ method: 'GET', credentials: 'include' },
|
|
3470
|
+
);
|
|
3471
|
+
expect(result.number).toBe(42);
|
|
3472
|
+
});
|
|
3473
|
+
|
|
3474
|
+
it('returns null from getPullRequest when response not ok', async () => {
|
|
3475
|
+
fetch.mockResolvedValueOnce({ ok: false, status: 404 });
|
|
3476
|
+
const result = await DataJunctionAPI.getPullRequest('myproject.feature-1');
|
|
3477
|
+
expect(result).toBeNull();
|
|
3478
|
+
});
|
|
3479
|
+
|
|
3480
|
+
it('calls createPullRequest correctly', async () => {
|
|
3481
|
+
fetch.mockResolvedValueOnce({
|
|
3482
|
+
ok: true,
|
|
3483
|
+
json: () =>
|
|
3484
|
+
Promise.resolve({
|
|
3485
|
+
number: 43,
|
|
3486
|
+
url: 'https://github.com/org/repo/pull/43',
|
|
3487
|
+
}),
|
|
3488
|
+
});
|
|
3489
|
+
const result = await DataJunctionAPI.createPullRequest(
|
|
3490
|
+
'myproject.feature-1',
|
|
3491
|
+
'My PR title',
|
|
3492
|
+
'PR body text',
|
|
3493
|
+
);
|
|
3494
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
3495
|
+
`${DJ_URL}/namespaces/myproject.feature-1/pull-request`,
|
|
3496
|
+
expect.objectContaining({
|
|
3497
|
+
method: 'POST',
|
|
3498
|
+
body: JSON.stringify({ title: 'My PR title', body: 'PR body text' }),
|
|
3499
|
+
}),
|
|
3500
|
+
);
|
|
3501
|
+
expect(result.number).toBe(43);
|
|
3502
|
+
});
|
|
3503
|
+
|
|
3504
|
+
it('calls createPullRequest without body', async () => {
|
|
3505
|
+
fetch.mockResolvedValueOnce({
|
|
3506
|
+
ok: true,
|
|
3507
|
+
json: () => Promise.resolve({ number: 44 }),
|
|
3508
|
+
});
|
|
3509
|
+
await DataJunctionAPI.createPullRequest(
|
|
3510
|
+
'myproject.feature-1',
|
|
3511
|
+
'Title only',
|
|
3512
|
+
);
|
|
3513
|
+
const body = JSON.parse(fetch.mock.calls[0][1].body);
|
|
3514
|
+
expect(body.body).toBeUndefined();
|
|
3515
|
+
expect(body.title).toBe('Title only');
|
|
3516
|
+
});
|
|
3517
|
+
|
|
3518
|
+
it('returns error object from createPullRequest on failure', async () => {
|
|
3519
|
+
fetch.mockResolvedValueOnce({
|
|
3520
|
+
ok: false,
|
|
3521
|
+
status: 422,
|
|
3522
|
+
json: () => Promise.resolve({ message: 'PR already exists' }),
|
|
3523
|
+
});
|
|
3524
|
+
const result = await DataJunctionAPI.createPullRequest(
|
|
3525
|
+
'myproject.feature-1',
|
|
3526
|
+
'My PR',
|
|
3527
|
+
);
|
|
3528
|
+
expect(result._error).toBe(true);
|
|
3529
|
+
expect(result.message).toBe('PR already exists');
|
|
3530
|
+
});
|
|
2940
3531
|
});
|
package/src/styles/index.css
CHANGED
|
@@ -738,6 +738,38 @@ tbody th {
|
|
|
738
738
|
padding-bottom: 1px;
|
|
739
739
|
}
|
|
740
740
|
|
|
741
|
+
.dj-tabs-bar {
|
|
742
|
+
display: flex;
|
|
743
|
+
flex-direction: row;
|
|
744
|
+
border-bottom: 1.5px solid #e8ecf0;
|
|
745
|
+
margin-top: 1rem;
|
|
746
|
+
margin-bottom: 0;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.dj-tab {
|
|
750
|
+
background: none;
|
|
751
|
+
border: none;
|
|
752
|
+
border-bottom: 3px solid transparent;
|
|
753
|
+
margin-bottom: -1.5px;
|
|
754
|
+
padding: 10px 20px;
|
|
755
|
+
font-size: 14px;
|
|
756
|
+
font-weight: 500;
|
|
757
|
+
color: #9ca3af;
|
|
758
|
+
cursor: pointer;
|
|
759
|
+
white-space: nowrap;
|
|
760
|
+
transition: color 0.15s, border-color 0.15s;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.dj-tab:hover {
|
|
764
|
+
color: #374151;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.dj-tab--active {
|
|
768
|
+
color: #2c7be5;
|
|
769
|
+
border-bottom-color: #2c7be5;
|
|
770
|
+
font-weight: 600;
|
|
771
|
+
}
|
|
772
|
+
|
|
741
773
|
pre {
|
|
742
774
|
border-radius: 1rem;
|
|
743
775
|
padding-right: 2rem;
|