Skip to content

Commit

Permalink
AJ-1278 Add rename column (#4614)
Browse files Browse the repository at this point in the history
Co-authored-by: Josh Ladieu <ladieu@broadinstitute.org>
Co-authored-by: Nick Watts <1156625+nawatts@users.noreply.github.com>
  • Loading branch information
3 people authored Feb 5, 2024
1 parent 07b7274 commit aaac3c8
Show file tree
Hide file tree
Showing 11 changed files with 319 additions and 67 deletions.
15 changes: 15 additions & 0 deletions src/libs/ajax/WorkspaceDataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export interface WDSJob {
updated: string;
}

export type AttributeSchemaUpdate = { name: string } | { datatype: string };

// The source of truth of available capabilities can be found in:
// https://github.com/DataBiosphere/terra-workspace-data-service/blob/main/service/src/main/resources/capabilities.json
// This list should only contain capabilities actively required or supported by the UI.
Expand Down Expand Up @@ -185,4 +187,17 @@ export const WorkspaceData = (signal) => ({
const res = await fetchWDS(root)(`job/v1/${jobId}`, _.merge(authOpts(), { signal }));
return res.json();
},
updateAttribute: async (
root: string,
instanceId: string,
recordType: string,
oldAttribute: string,
newAttribute: AttributeSchemaUpdate
): Promise<any> => {
const res = await fetchWDS(root)(
`${instanceId}/types/v0.2/${recordType}/${oldAttribute}`,
_.mergeAll([authOpts(), jsonBody(newAttribute), { signal, method: 'PATCH' }])
);
return res.json();
},
});
10 changes: 9 additions & 1 deletion src/libs/ajax/data-table-providers/DataTableProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ export type TsvUploadButtonTooltipOptions = {
recordTypePresent: boolean;
};

export type UpdateAttributeParameters = {
entityType: string;
oldAttributeName: string;
newAttributeName: string;
};

export interface AttributeArray {
itemsType: 'AttributeValue' | 'EntityReference';
items: unknown[]; // truly "unknown" here; the backend Java representation is Object[]
Expand Down Expand Up @@ -109,6 +115,8 @@ export type TsvUploadButtonTooltipFn = (options: TsvUploadButtonTooltipOptions)

export type UploadTsvFn = (uploadParams: UploadParameters) => Promise<any>;

export type UpdateAttributeFn = (attributeUpdateParams: UpdateAttributeParameters) => Promise<any>;

export interface DataTableFeatures {
supportsCapabilities: boolean;
supportsTsvDownload: boolean;
Expand Down Expand Up @@ -147,7 +155,7 @@ export interface DataTableProvider {
deleteColumn: DeleteColumnFn;
downloadTsv: DownloadTsvFn;
uploadTsv: UploadTsvFn;
updateAttribute: UpdateAttributeFn;
// todos that we may need soon:
// getMetadata: GetMetadataFn
// updateAttribute: function, see also boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
TSVFeatures,
TsvUploadButtonDisabledOptions,
TsvUploadButtonTooltipOptions,
UpdateAttributeParameters,
UploadParameters,
} from 'src/libs/ajax/data-table-providers/DataTableProvider';
import { asyncImportJobStore } from 'src/libs/state';
Expand Down Expand Up @@ -122,4 +123,14 @@ export class EntityServiceDataTableProvider implements DataTableProvider {
);
notifyDataImportProgress(jobId, 'Data will show up incrementally as the job progresses.');
};

updateAttribute = async (updateAttrParams: UpdateAttributeParameters): Promise<any> => {
return Ajax()
.Workspaces.workspace(this.namespace, this.name)
.renameEntityColumn(
updateAttrParams.entityType,
updateAttrParams.oldAttributeName,
updateAttrParams.newAttributeName
);
};
}
16 changes: 15 additions & 1 deletion src/libs/ajax/data-table-providers/WdsDataTableProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
TSVFeatures,
TsvUploadButtonDisabledOptions,
TsvUploadButtonTooltipOptions,
UpdateAttributeParameters,
UploadParameters,
} from 'src/libs/ajax/data-table-providers/DataTableProvider';
import { LeoAppStatus, ListAppItem } from 'src/libs/ajax/leonardo/models/app-models';
Expand Down Expand Up @@ -157,7 +158,7 @@ export class WdsDataTableProvider implements DataTableProvider {
supportsTypeRenaming: false,
supportsEntityRenaming: false,
supportsEntityUpdating: false, // TODO: enable as part of AJ-594
supportsAttributeRenaming: false, // TODO: enable as part of AJ-1278, requires `edit.renameAttribute` capability
supportsAttributeRenaming: this.isCapabilityEnabled('edit.renameAttribute'),
supportsAttributeDeleting: this.isCapabilityEnabled('edit.deleteAttribute'),
supportsAttributeClearing: false,
supportsExport: false,
Expand Down Expand Up @@ -323,4 +324,17 @@ export class WdsDataTableProvider implements DataTableProvider {
uploadParams.file
);
};

updateAttribute = (params: UpdateAttributeParameters): Promise<Blob> => {
if (!this.proxyUrl) return Promise.reject('Proxy Url not loaded');
return Ajax().WorkspaceData.updateAttribute(
this.proxyUrl,
this.workspaceId,
params.entityType,
params.oldAttributeName,
{
name: params.newAttributeName,
}
);
};
}
2 changes: 2 additions & 0 deletions src/workspace-data/Data.js
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,7 @@ export const WorkspaceData = _.flow(

const loadWdsTypes = useCallback(
(url, workspaceId) => {
setWdsTypes({ status: 'None', state: [] });
return Ajax(signal)
.WorkspaceData.getSchema(url, workspaceId)
.then((typesResult) => {
Expand Down Expand Up @@ -1490,6 +1491,7 @@ export const WorkspaceData = _.flow(
recordType: selectedData.entityType,
wdsSchema: wdsTypes.state,
editable: canEditWorkspace,
loadMetadata,
}),
]
),
Expand Down
62 changes: 0 additions & 62 deletions src/workspace-data/data-table/entity-service/RenameColumnModal.js

This file was deleted.

6 changes: 3 additions & 3 deletions src/workspace-data/data-table/shared/DataTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import * as WorkspaceUtils from 'src/workspaces/utils';
import { concatenateAttributeNames } from '../entity-service/attribute-utils';
import { entityAttributeText } from '../entity-service/entityAttributeText';
import { EntityRenamer } from '../entity-service/EntityRenamer';
import { RenameColumnModal } from '../entity-service/RenameColumnModal';
import { renderDataCell } from '../entity-service/renderDataCell';
import {
allSavedColumnSettingsEntityTypeKey,
Expand All @@ -45,6 +44,7 @@ import {
import { SingleEntityEditor } from '../entity-service/SingleEntityEditor';
import { EditDataLink } from './EditDataLink';
import { HeaderOptions } from './HeaderOptions';
import { RenameColumnModal } from './RenameColumnModal';

const entityMap = (entities) => {
return _.fromPairs(_.map((e) => [e.name, e], entities));
Expand Down Expand Up @@ -747,10 +747,10 @@ const DataTable = (props) => {
}),
!!renamingColumn &&
h(RenameColumnModal, {
namespace,
name,
entityType,
oldAttributeName: renamingColumn,
attributeNames: entityMetadata[entityType].attributeNames,
dataProvider,
onSuccess: () => {
setRenamingColumn(undefined);
Ajax().Metrics.captureEvent(Events.workspaceDataRenameColumn, extractWorkspaceDetails(workspace.workspace));
Expand Down
128 changes: 128 additions & 0 deletions src/workspace-data/data-table/shared/RenameColumnModal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { h } from 'react-hyperscript-helpers';
import { renderWithAppContexts as render } from 'src/testing/test-utils';

import { RenameColumnModal, RenameColumnModalProps } from './RenameColumnModal';

const defaultRenameColumnModalProps: RenameColumnModalProps = {
onDismiss: () => {},
onSuccess: () => {},
entityType: 'defaultEntityType',
attributeNames: ['attribute1', 'attribute2'],
oldAttributeName: 'attribute1',
dataProvider: { providerName: 'Entity Service' },
};

describe('RenameColumnModal', () => {
it('Errors on invalid characters in column name', async () => {
// Arrange
const renameProps = { ...defaultRenameColumnModalProps };
// const user = userEvent.setup();
// Act
const renameModal = render(h(RenameColumnModal, renameProps));
// User enters name with %^#@
const input = screen.getByLabelText(/New Name/);
await userEvent.type(input, 'b@d ch@r@cter$');
// Assert
expect(renameModal.getByText(/Column name may only contain alphanumeric characters/));
});

it('Errors on reserved word for column name in GCP', async () => {
// Arrange
const renameProps = { ...defaultRenameColumnModalProps };
// Act
const renameModal = render(h(RenameColumnModal, renameProps));
// User enters defaultEntityType_id
const input = screen.getByLabelText(/New Name/);
await userEvent.type(input, 'defaultEntityType_id');
// Assert
expect(renameModal.getByText(/Column name cannot be/));
});

it('Does not error on GCP reserved word in azure', async () => {
// Arrange
const renameProps = { ...defaultRenameColumnModalProps, dataProvider: { providerName: 'WDS' } };
// Act
const renameModal = render(h(RenameColumnModal, renameProps));
// User enters defaultEntityType_id
const input = screen.getByLabelText(/New Name/);
await userEvent.type(input, 'defaultEntityType_id');
// Assert
expect(renameModal.queryByText(/Column name cannot be/)).toBeNull();
});

it('Errors on existing name for column name', async () => {
// Arrange
const renameProps = { ...defaultRenameColumnModalProps };
// Act
const renameModal = render(h(RenameColumnModal, renameProps));
// User enters 'attribute2'
const input = screen.getByLabelText(/New Name/);
await userEvent.type(input, 'attribute2');
// Assert
expect(renameModal.getByText(/already exists as an attribute name/));
});

it('Errors on column name starting with sys_ in azure', async () => {
// Arrange
const renameProps = { ...defaultRenameColumnModalProps, dataProvider: { providerName: 'WDS' } };
// Act
const renameModal = render(h(RenameColumnModal, renameProps));
// User enters 'attribute2'
const input = screen.getByLabelText(/New Name/);
await userEvent.type(input, 'sys_attribute');
// Assert
expect(renameModal.getByText(/Column name cannot start with "sys_"/));
});

it('Does not errors on column name starting with sys_ in gcp', async () => {
// Arrange
const renameProps = { ...defaultRenameColumnModalProps };
// Act
const renameModal = render(h(RenameColumnModal, renameProps));
// User enters 'attribute2'
const input = screen.getByLabelText(/New Name/);
await userEvent.type(input, 'sys_attribute');
// Assert
expect(renameModal.queryByText(/Column name cannot start with "sys_"/)).toBeNull();
});

it('Does not allow colons in azure', async () => {
// Arrange
const renameProps = { ...defaultRenameColumnModalProps, dataProvider: { providerName: 'WDS' } };
// Act
const renameModal = render(h(RenameColumnModal, renameProps));
// User enters 'attribute2'
const input = screen.getByLabelText(/New Name/);
await userEvent.type(input, 'namespace:attribute');
// Assert
expect(renameModal.getByText(/Column name may only contain alphanumeric characters, underscores, and dashes./));
});

it('Allows a single colon in gcp', async () => {
// Arrange
const renameProps = { ...defaultRenameColumnModalProps };
// Act
const renameModal = render(h(RenameColumnModal, renameProps));
// User enters 'attribute2'
const input = screen.getByLabelText(/New Name/);
await userEvent.type(input, 'namespace:attribute');
// Assert
expect(
renameModal.queryByText(/Column name may only contain alphanumeric characters, underscores, and dashes./)
).toBeNull();
});

it('Does not allow multiple colons in gcp', async () => {
// Arrange
const renameProps = { ...defaultRenameColumnModalProps };
// Act
const renameModal = render(h(RenameColumnModal, renameProps));
// User enters 'attribute2'
const input = screen.getByLabelText(/New Name/);
await userEvent.type(input, 'namespace:attribute:colon');
// Assert
expect(renameModal.getByText(/Column name may only include a single colon/));
});
});
Loading

0 comments on commit aaac3c8

Please sign in to comment.