// checks that it is present in the trash table
moveToTrash(name: string) {
// wait for image to be created
- cy.get('.datatable-body').first().should('not.contain.text', '(Creating...)');
+ cy.get('cds-table table tbody').first().should('not.contain.text', '(Creating...)');
this.getFirstTableCell(name).click();
// click on the drop down and selects the move to trash option
- cy.get('.table-actions button.dropdown-toggle').first().click();
- cy.get('button.move-to-trash').click();
+ cy.get('[data-testid="table-action-btn"]').click({ multiple: true });
+ cy.get('button.move-to-trash').click({ force: true });
- cy.get('[data-cy=submitBtn]').should('be.visible').click();
+ cy.get('[data-cy=submitBtn] button').should('be.visible').click({ force: true });
// Clicks trash tab
cy.contains('.nav-link', 'Trash').click();
// wait for table to load
this.getFirstTableCell(name).click();
- cy.contains('button', 'Restore').click();
+ cy.get('[data-testid="table-action-btn"]').click({ multiple: true });
+ cy.get('button.restore').click({ force: true });
// wait for pop-up to be visible (checks for title of pop-up)
cy.get('cds-modal #name').should('be.visible');
cy.get('cd-pool-list').should('exist');
cy.visit('#/block/mirroring').wait(1000);
- cy.get('.table-actions button.dropdown-toggle').first().click();
cy.get('[aria-label="Import Bootstrap Token"]').click();
cy.get('cd-bootstrap-import-modal').within(() => {
cy.get(`input[name=${name}]`).click({ force: true });
it('tests editing mode for pools', () => {
mirroring.navigateTo();
-
- mirroring.editMirror(poolName, 'Pool');
- mirroring.getFirstTableCell('pool').should('be.visible');
- mirroring.editMirror(poolName, 'Image');
- mirroring.getFirstTableCell('image').should('be.visible');
- mirroring.editMirror(poolName, 'Disabled');
- mirroring.getFirstTableCell('disabled').should('be.visible');
+ cy.get('cd-mirroring-pools').within(() => {
+ mirroring.editMirror(poolName, 'Pool');
+ mirroring.getFirstTableCell('pool').should('be.visible');
+ mirroring.editMirror(poolName, 'Image');
+ mirroring.getFirstTableCell('image').should('be.visible');
+ mirroring.editMirror(poolName, 'Disabled');
+ mirroring.getFirstTableCell('disabled').should('be.visible');
+ });
});
afterEach(() => {
@PageHelper.restrictTo(pages.index.url)
editMirror(name: string, option: string) {
// Clicks the pool in the table
- this.getFirstTableCell(name).click();
-
- // Clicks the Edit Mode button
- cy.contains('button', 'Edit Mode').click();
+ this.clickRowActionButton(name, 'edit-mode');
// Clicks the drop down in the edit pop-up, then clicks the Update button
cy.get('cds-modal').should('be.visible');
@PageHelper.restrictTo(pages.index.url)
generateToken(poolName: string) {
- cy.get('[aria-label="Create Bootstrap Token"]').first().click();
+ cy.get('[aria-label="Create Bootstrap Token"]').click();
cy.get('cd-bootstrap-create-modal').within(() => {
cy.get(`input[name=${poolName}]`).click({ force: true });
cy.get('button[type=submit]').click();
cy.get('cd-mirroring-pools').within(() => {
this.getTableCell(this.poolsColumnIndex.name, poolName)
.parent()
- .find(`datatable-body-cell:nth-child(${this.poolsColumnIndex.health}) .badge`)
+ .find(`[cdstabledata]:nth-child(${this.poolsColumnIndex.health}) .badge`)
.should(($ele) => {
const newLabels = $ele.toArray().map((v) => v.innerText);
expect(newLabels).to.include(status);
* Does not work for configs with checkbox only, possible future PR
*/
configClear(name: string) {
+ this.navigateTo();
const valList = ['global', 'mon', 'mgr', 'osd', 'mds', 'client']; // Editable values
- this.navigateEdit(name);
+ this.getFirstTableCell(name).click();
+ cy.contains('button', 'Edit').click();
// Waits for the data to load
cy.contains('.card-header', `Edit ${name}`);
// Clicks save button and checks that values are not present for the selected config
cy.get('[data-cy=submitBtn]').click();
+ cy.wait(3 * 1000);
+
// Enter config setting name into filter box
- this.searchTable(name);
+ this.searchTable(name, 100);
// Expand row
this.getExpandCollapseElement(name).click();
* Ex: [global, '2'] is the global value with an input of 2
*/
edit(name: string, ...values: [string, string][]) {
- this.navigateEdit(name);
+ this.getFirstTableCell(name).click();
+ cy.contains('button', 'Edit').click();
// Waits for data to load
cy.contains('.card-header', `Edit ${name}`);
// Clicks save button then waits until the desired config is visible, clicks it,
// then checks that each desired value appears with the desired number
cy.get('[data-cy=submitBtn]').click();
+ cy.wait(3 * 1000);
// Enter config setting name into filter box
- this.searchTable(name);
+ this.searchTable(name, 100);
// Checks for visibility of config in table
this.getExpandCollapseElement(name).should('be.visible').click();
columnIndex = {
service_name: 1,
placement: 2,
- running: 0,
- size: 0,
- last_refresh: 0
+ running: 3,
+ size: 4,
+ last_refresh: 5
};
}
}
}
- checkExist(hostname: string, exist: boolean) {
+ checkExist(hostname: string, exist: boolean, shouldReload = false) {
+ if (shouldReload) {
+ cy.reload(true, { log: true, timeout: 5 * 1000 });
+ }
this.getTableCell(this.columnIndex.hostname, hostname, true)
.parent()
- .find(`datatable-body-cell:nth-child(${this.columnIndex.hostname}) span`)
+ .find(`[cdstabledata]:nth-child(${this.columnIndex.hostname}) span`)
.should(($elements) => {
const hosts = $elements.toArray().map((v) => v.innerText);
if (exist) {
}
remove(hostname: string) {
- super.delete(hostname, this.columnIndex.hostname, 'hosts', true);
+ super.delete(hostname, this.columnIndex.hostname, 'hosts', true, false, true);
}
// Add or remove labels on a host, then verify labels in the table
editLabels(hostname: string, labels: string[], add: boolean) {
- this.getTableCell(this.columnIndex.hostname, hostname, true).click();
- this.clickActionButton('edit');
+ this.clickRowActionButton(hostname, 'edit');
// add or remove label badges
if (add) {
checkLabelExists(hostname: string, labels: string[], add: boolean) {
// Verify labels are added or removed from Labels column
// First find row with hostname, then find labels in the row
- this.getTableCell(this.columnIndex.hostname, hostname, true)
- .click()
+ this.getTableCell(this.columnIndex.hostname, hostname, true).as('row').click();
+ cy.get('@row')
.parent()
- .find(`datatable-body-cell:nth-child(${this.columnIndex.labels}) .badge`)
+ .find(`[cdstabledata]:nth-child(${this.columnIndex.labels}) .badge`)
.should(($ele) => {
const newLabels = $ele.toArray().map((v) => v.innerText);
for (const label of labels) {
maintenance(hostname: string, exit = false, force = false) {
this.clearTableSearchInput();
if (force) {
- this.getTableCell(this.columnIndex.hostname, hostname, true).click();
- this.clickActionButton('enter-maintenance');
+ this.clickRowActionButton(hostname, 'enter-maintenance');
cy.get('cds-modal').within(() => {
cy.contains('button', 'Continue').click();
this.getTableCell(this.columnIndex.hostname, hostname, true)
.parent()
- .find(`datatable-body-cell:nth-child(${this.columnIndex.status}) .badge`)
+ .find(`[cdstabledata]:nth-child(${this.columnIndex.status}) .badge`)
.should(($ele) => {
const status = $ele.toArray().map((v) => v.innerText);
expect(status).to.include('maintenance');
this.getTableCell(this.columnIndex.hostname, hostname, true)
.click()
.parent()
- .find(`datatable-body-cell:nth-child(${this.columnIndex.status})`)
+ .find(`[cdstabledata]:nth-child(${this.columnIndex.status})`)
.then(($ele) => {
const status = $ele.toArray().map((v) => v.innerText);
if (status[0].includes('maintenance')) {
- this.clickActionButton('exit-maintenance');
+ this.clickRowActionButton(hostname, 'exit-maintenance');
}
});
this.getTableCell(this.columnIndex.hostname, hostname, true)
.parent()
- .find(`datatable-body-cell:nth-child(${this.columnIndex.status})`)
+ .find(`[cdstabledata]:nth-child(${this.columnIndex.status})`)
.should(($ele) => {
const status = $ele.toArray().map((v) => v.innerText);
expect(status).to.not.include('maintenance');
});
} else {
- this.getTableCell(this.columnIndex.hostname, hostname, true).click();
- this.clickActionButton('enter-maintenance');
+ this.clickRowActionButton(hostname, 'enter-maintenance');
this.getTableCell(this.columnIndex.hostname, hostname, true)
.parent()
- .find(`datatable-body-cell:nth-child(${this.columnIndex.status}) .badge`)
+ .find(`[cdstabledata]:nth-child(${this.columnIndex.status}) .badge`)
.should(($ele) => {
const status = $ele.toArray().map((v) => v.innerText);
expect(status).to.include('maintenance');
@PageHelper.restrictTo(pages.index.url)
drain(hostname: string) {
- this.getTableCell(this.columnIndex.hostname, hostname, true).click();
- this.clickActionButton('start-drain');
+ this.clickRowActionButton(hostname, 'start-drain');
cy.wait(1000);
this.checkLabelExists(hostname, ['_no_schedule'], true);
checkServiceInstancesExist(hostname: string, instances: string[]) {
this.getTableCell(this.columnIndex.hostname, hostname, true)
.parent()
- .find(`datatable-body-cell:nth-child(${this.columnIndex.services}) .badge`)
+ .find(`[cdstabledata]:nth-child(${this.columnIndex.services}) .badge`)
.should(($ele) => {
const serviceInstances = $ele.toArray().map((v) => v.innerText);
for (const instance of instances) {
cy.contains('button', 'Update').click();
// Checks if edits appear
this.getExpandCollapseElement(name).should('be.visible').click();
-
for (const input of inputs) {
- cy.get('.datatable-body').last().contains(input.newValue);
+ cy.get('[data-testid="datatable-row-detail"] [cdstablerow] [cdstabledata] span').contains(
+ input.newValue
+ );
}
// Clear mgr module of all edits made to it
this.getExpandCollapseElement(name).should('be.visible').click();
for (const input of inputs) {
if (input.oldValue) {
- cy.get('.datatable-body')
- .eq(1)
- .should('contain', input.id)
- .and('not.contain', input.newValue);
+ cy.contains('[data-testid="datatable-row-detail"] [cdstablerow] [cdstabledata]', input.id)
+ .parent('[cdstablerow]')
+ .find('[cdstabledata]')
+ .should('not.contain', input.newValue);
}
}
}
monitors.getLegends().its(2).should('have.text', 'Not In Quorum');
// verify correct columns on In Quorum table
- monitors.getDataTableHeaders(0).contains('Name');
+ monitors.getDataTableHeaders().contains('Name');
- monitors.getDataTableHeaders(0).contains('Rank');
+ monitors.getDataTableHeaders().contains('Rank');
- monitors.getDataTableHeaders(0).contains('Public Address');
-
- monitors.getDataTableHeaders(0).contains('Open Sessions');
+ monitors.getDataTableHeaders().contains('Public Address');
+ monitors.getDataTableHeaders().contains('Open Sessions');
// verify correct columns on Not In Quorum table
- monitors.getDataTableHeaders(1).contains('Name');
+ monitors.getDataTableHeaders().contains('Name');
- monitors.getDataTableHeaders(1).contains('Rank');
+ monitors.getDataTableHeaders().contains('Rank');
- monitors.getDataTableHeaders(1).contains('Public Address');
+ monitors.getDataTableHeaders().contains('Public Address');
});
});
});
if (expandCluster) {
this.getTableCount('total').should('be.gte', 1);
}
- cy.get('@addButton').click();
+ cy.get('@addButton').click({ force: true });
});
if (!expandCluster) {
@PageHelper.restrictTo(pages.index.url)
checkStatus(id: number, status: string[]) {
this.searchTable(`id:${id}`);
+ cy.wait(5 * 1000);
this.expectTableCount('found', 1);
- cy.get(`datatable-body-cell:nth-child(${this.columnIndex.status}) .badge`).should(($ele) => {
+ cy.get(`[cdstabledata]:nth-child(${this.columnIndex.status}) .badge`).should(($ele) => {
const allStatus = $ele.toArray().map((v) => v.innerText);
for (const s of status) {
expect(allStatus).to.include(s);
deleteByIDs(osdIds: number[], replace?: boolean) {
this.getTableRows().each(($el) => {
const rowOSD = Number(
- $el.find('datatable-body-cell .datatable-body-cell-label').get(this.columnIndex.id - 1)
- .textContent
+ $el.find('[cdstabledata][cdstablerow]').get(this.columnIndex.id - 1).textContent
);
if (osdIds.includes(rowOSD)) {
cy.wrap($el).click();
cy.get('cd-service-daemon-list').within(() => {
this.getTableCell(daemonNameIndex, daemon, true)
.parent()
- .find(`datatable-body-cell:nth-child(${statusIndex}) .badge`)
+ .find(`[cdstabledata]:nth-child(${statusIndex}) .badge`)
.should(($ele) => {
const status = $ele.toArray().map((v) => v.innerText);
expect(status).to.include(expectedStatus);
expectPlacementCount(serviceName: string, expectedCount: string) {
this.getTableCell(this.columnIndex.service_name, serviceName)
.parent()
- .find(`datatable-body-cell:nth-child(${this.columnIndex.placement})`)
+ .find(`[cdstabledata]:nth-child(${this.columnIndex.placement})`)
.should(($ele) => {
const running = $ele.text().split(';');
expect(running).to.include(`count:${expectedCount}`);
isUnmanaged(serviceName: string, unmanaged: boolean) {
this.getTableCell(this.columnIndex.service_name, serviceName)
.parent()
- .find(`datatable-body-cell:nth-child(${this.columnIndex.placement})`)
+ .find(`[cdstabledata]:nth-child(${this.columnIndex.placement})`)
.should(($ele) => {
const placement = $ele.text().split(';');
unmanaged
}
deleteService(serviceName: string) {
- const getRow = this.getTableCell.bind(this, this.columnIndex.service_name);
- getRow(serviceName).click();
-
- // Clicks on table Delete button
- this.clickActionButton('delete');
+ this.clickRowActionButton(serviceName, 'delete', 3 * 1000);
// Confirms deletion
cy.get('cds-modal input#confirmation_input').click({ force: true });
// Wait for modal to close
cy.get('cds-modal').should('not.exist');
+ cy.wait(1 * 1000);
this.checkExist(serviceName, false);
}
daemonAction(daemon: string, action: string) {
cy.get('cd-service-daemon-list').within(() => {
- this.getTableRow(daemon).click();
- this.clickActionButton(action);
+ this.clickRowActionButton(daemon, action);
});
}
}
it('should edit a user', () => {
const newCaps = 'allow *';
- users.edit(entityName, 'allow *');
+ users.edit(entityName, 'allow *', true);
users.existTableCell(entityName, true);
users.checkCaps(entityName, [`${entity}: ${newCaps}`]);
+ users.clickActionButtonFromMultiselect(entityName, 'edit');
});
it('should delete a user', () => {
- users.delete(entityName, null, null, true);
+ users.delete(entityName, null, null, true, true);
});
});
});
};
checkForUsers() {
- this.getTableCount('total').should('not.be.eq', 0);
+ cy.wait(500);
+ this.getTableCount('item').should('not.be.eq', 0);
}
verifyKeysAreHidden() {
this.getTableCell(this.columnIndex.entity, 'osd.0')
.parent()
- .find(`datatable-body-cell:nth-child(${this.columnIndex.key}) span`)
+ .find(`td[cdstabledata]:nth-child(${this.columnIndex.key}) span`)
.should(($ele) => {
const serviceInstances = $ele.toArray().map((v) => v.innerText);
expect(serviceInstances).not.contains(/^[a-z0-9]+$/i);
cy.get('cd-crud-table').should('exist');
}
- edit(name: string, newCaps: string) {
- this.navigateEdit(name);
+ edit(name: string, newCaps: string, isMultiselect = false) {
+ this.navigateEdit(name, false, true, null, isMultiselect);
cy.get('#formly_5_string_cap_1').clear().type(newCaps);
cy.get("[aria-label='Edit User']").should('exist').click();
cy.get('cd-crud-table').should('exist');
this.getTableCell(this.columnIndex.entity, entityName)
.click()
.parent()
- .find(`datatable-body-cell:nth-child(${this.columnIndex.capabilities}) .badge`)
+ .find(`[cdstabledata]:nth-child(${this.columnIndex.capabilities}) .badge`)
.should(($ele) => {
const newCaps = $ele.toArray().map((v) => v.innerText);
for (const cap of capabilities) {
});
When('I click on {string} button', (button: string) => {
- cy.get(`[aria-label="${button}"]`).first().click();
+ cy.get(`[aria-label="${button}"]`).first().click({ force: true });
});
Then('I should see the modal', () => {
// When you are clicking on an action in the table actions dropdown button
When('I click on {string} button from the table actions', (button: string) => {
- cy.get('.table-actions button.dropdown-toggle').first().click();
- cy.get(`[aria-label="${button}"]`).first().click();
+ cy.get(`[aria-label="${button}"]`).click({ force: true });
});
// When you are clicking on an action inside the expanded table row
When('I click on {string} button from the expanded row', (button: string) => {
- cy.get('.datatable-row-detail').within(() => {
- cy.get('.table-actions button.dropdown-toggle').first().click();
- cy.get(`[aria-label="${button}"]`).first().click();
+ cy.get('[data-testid="datatable-row-detail"]').within(() => {
+ cy.get(`[data-testid="primary-action"][aria-label="${button}"]`).click();
});
});
When('I click on {string} button from the table actions in the expanded row', (button: string) => {
- cy.get('.datatable-row-detail').within(() => {
- cy.get('.table-actions button.dropdown-toggle').first().click();
- cy.get(`[aria-label="${button}"]`).first().click();
+ cy.get('[data-testid="datatable-row-detail"]').within(() => {
+ cy.get('[data-testid="table-action-btn"]').first().click();
+ cy.get(`[aria-label="${button}"]`).first().click({ force: true });
});
});
When('I expand the row {string}', (row: string) => {
- cy.contains('.datatable-body-row', row).first().find('.tc_expand-collapse').click();
+ cy.contains('[cdstablerow] [cdstabledata]', row)
+ .parent('[cdstablerow]')
+ .find('[cdstableexpandbutton] .cds--table-expand__button')
+ .click();
});
/**
* Selects any row on the datatable if it matches the given name
*/
When('I select a row {string}', (row: string) => {
- cy.get('cd-table .search input').first().clear().type(row);
- cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).click();
+ cy.get('.cds--search-input').first().clear().type(row);
+ cy.contains('[cdstablerow] [cdstabledata]', row)
+ .parent('[cdstablerow]')
+ .find('[data-testid="table-action-btn"]')
+ .click({ force: true });
});
When('I select a row {string} in the expanded row', (row: string) => {
- cy.get('.datatable-row-detail').within(() => {
- cy.get('cd-table .search input').first().clear().type(row);
- cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).click();
+ cy.get('[data-testid="datatable-row-detail"]').within(() => {
+ cy.get('.cds--search-input').first().clear().type(row);
+ cy.contains(`[cdstablerow] [cdstabledata]`, row).click();
});
});
Then('I should see a row with {string}', (row: string) => {
- cy.get('cd-table .search input').first().clear().type(row);
- cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).should(
- 'exist'
- );
+ cy.get('.cds--search-input').first().clear().type(row);
+ cy.contains(`[cdstablerow] [cdstabledata]`, row).should('exist');
});
Then('I should not see a row with {string}', (row: string) => {
- cy.get('cd-table .search input').first().clear().type(row);
- cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).should(
- 'not.exist'
- );
+ cy.get('.cds--search-input').first().clear().type(row);
+ cy.contains(`[cdstablerow] [cdstabledata]`, row).should('not.exist');
});
Then('I should see a table in the expanded row', () => {
- cy.get('.datatable-row-detail').within(() => {
+ cy.get('[data-testid="datatable-row-detail"]').within(() => {
cy.get('cd-table').should('exist');
- cy.get('datatable-scroller, .empty-row');
+ cy.get('.no-data');
});
});
Then('I should not see a row with {string} in the expanded row', (row: string) => {
- cy.get('.datatable-row-detail').within(() => {
- cy.get('cd-table .search input').first().clear().type(row);
- cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).should(
- 'not.exist'
- );
+ cy.get('[data-testid="datatable-row-detail"]').within(() => {
+ cy.get('.cds--search-input').first().clear().type(row);
+ cy.contains(`[cdstablerow] [cdstabledata]`, row).should('not.exist');
});
});
Then('I should see rows with following entries', (entries) => {
entries.hashes().forEach((entry: any) => {
- cy.get('cd-table .search input').first().clear().type(entry.hostname);
- cy.contains(
- `datatable-body-row datatable-body-cell .datatable-body-cell-label`,
- entry.hostname
- ).should('exist');
+ cy.get('.cds--search-input').first().clear().type(entry.hostname);
+ cy.contains(`[cdstablerow] [cdstabledata]`, entry.hostname).should('exist');
});
});
And('I should see row {string} have {string}', (row: string, options: string) => {
if (options) {
- cy.get('cd-table .search input').first().clear().type(row);
+ cy.get('.cds--search-input').first().clear().type(row);
for (const option of options.split(',')) {
- cy.contains(
- `datatable-body-row datatable-body-cell .datatable-body-cell-label .badge`,
- option
- ).should('exist');
+ cy.contains(`[cdstablerow] [cdstabledata] .badge`, option).should('exist');
}
}
});
And('I should see row {string} of the expanded row to have a usage bar', (row: string) => {
- cy.get('.datatable-row-detail').within(() => {
- cy.get('cd-table .search input').first().clear().type(row);
- cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).should(
- 'exist'
- );
- cy.get('.datatable-body-row .datatable-body-cell .datatable-body-cell-label .progress').should(
- 'exist'
- );
+ cy.get('[data-testid="datatable-row-detail"]').within(() => {
+ cy.get('.cds--search-input').first().clear().type(row);
+ cy.contains(`[cdstablerow] [cdstabledata]`, row).should('exist');
+ cy.get('[cdstablerow] [cdstabledata] cd-usage-bar .progress').should('exist');
});
});
And('I should see row {string} does not have {string}', (row: string, options: string) => {
if (options) {
- cy.get('cd-table .search input').first().clear().type(row);
+ cy.get('.cds--search-input').first().clear().type(row);
for (const option of options.split(',')) {
- cy.contains(
- `datatable-body-row datatable-body-cell .datatable-body-cell-label .badge`,
- option
- ).should('not.exist');
+ cy.contains(`[cdstablerow] [cdstabledata] .badge`, option).should('not.exist');
}
}
});
Then('I should see a row with {string} in the expanded row', (row: string) => {
- cy.get('.datatable-row-detail').within(() => {
- cy.get('cd-table .search input').first().clear().type(row);
- cy.contains(`datatable-body-row datatable-body-cell .datatable-body-cell-label`, row).should(
- 'exist'
- );
+ cy.get('[data-testid="datatable-row-detail"]').within(() => {
+ cy.get('.cds--search-input').first().clear().type(row);
+ cy.contains(`[cdstablerow] [cdstabledata]`, row).should('exist');
});
});
And('I should see row {string} have {string} on this tab', (row: string, options: string) => {
if (options) {
cy.get('cd-table').should('exist');
- cy.get('datatable-scroller, .empty-row');
- cy.get('.datatable-row-detail').within(() => {
- cy.get('cd-table .search input').first().clear().type(row);
+ cy.get('.no-data');
+ cy.get('[data-testid="datatable-row-detail"]').within(() => {
+ cy.get('.cds--search-input').first().clear().type(row);
for (const option of options.split(',')) {
- cy.contains(
- `datatable-body-row datatable-body-cell .datatable-body-cell-label span`,
- option
- ).should('exist');
+ cy.contains(`[cdstablerow] [cdstabledata] span`, option).should('exist');
}
});
}
});
Then('I should see an alert {string} in the expanded row', (alert: string) => {
- cy.get('.datatable-row-detail').within(() => {
+ cy.get('[data-testid="datatable-row-detail"]').within(() => {
cy.get('.cds--actionable-notification__content').contains(alert);
});
});
pages = pages;
auth(url: string, alias: string, username: string, password: string) {
- this.clickActionButton('connect');
+ cy.contains('button', 'Connect').click();
cy.get('cd-multi-cluster-form').should('exist');
cy.get('cd-modal').within(() => {
cy.get('input[name=remoteClusterUrl]').type(url);
}
disconnect(alias: string) {
- this.getFirstTableCell(alias).click();
- this.clickActionButton('disconnect');
+ this.clickRowActionButton(alias, 'disconnect');
cy.get('cds-modal').within(() => {
cy.get('#confirmation_input').click({ force: true });
cy.get('cd-submit-button').click();
}
reconnect(alias: string, password: string) {
- this.getFirstTableCell(alias).click();
- this.clickActionButton('reconnect');
+ this.clickRowActionButton(alias, 'reconnect');
cy.get('cd-modal').within(() => {
cy.get('input[name=password]').type(password);
cy.get('cd-submit-button').click();
}
edit(alias: string, newAlias: string) {
- this.getFirstTableCell(alias).click();
- this.clickActionButton('edit');
+ this.clickRowActionButton(alias, 'edit');
cy.get('cd-modal').within(() => {
cy.get('input[name=clusterAlias]').clear().type(newAlias);
cy.get('cd-submit-button').click();
createCluster.getDataTables().should('have.length', 1);
// verify correct columns on Host Details table
- createCluster.getDataTableHeaders(0).contains('Hostname');
+ createCluster.getDataTableHeaders().contains('Hostname');
- createCluster.getDataTableHeaders(0).contains('Labels');
+ createCluster.getDataTableHeaders().contains('Labels');
- createCluster.getDataTableHeaders(0).contains('CPUs');
+ createCluster.getDataTableHeaders().contains('CPUs');
- createCluster.getDataTableHeaders(0).contains('Cores');
+ createCluster.getDataTableHeaders().contains('Cores');
- createCluster.getDataTableHeaders(0).contains('Total Memory');
+ createCluster.getDataTableHeaders().contains('Total Memory');
- createCluster.getDataTableHeaders(0).contains('Raw Capacity');
+ createCluster.getDataTableHeaders().contains('Raw Capacity');
- createCluster.getDataTableHeaders(0).contains('HDDs');
+ createCluster.getDataTableHeaders().contains('HDDs');
- createCluster.getDataTableHeaders(0).contains('Flash');
+ createCluster.getDataTableHeaders().contains('Flash');
- createCluster.getDataTableHeaders(0).contains('NICs');
+ createCluster.getDataTableHeaders().contains('NICs');
});
it('should check default host name is present', () => {
hosts.remove(hostnames[3]);
hosts.navigateTo('add');
hosts.add(hostnames[3]);
- hosts.checkExist(hostnames[3], true);
+ hosts.checkExist(hostnames[3], true, true);
});
it('should show the exact count of daemons', () => {
});
it('should edit an export', () => {
- nfsExport.editExport(rgwPseudo, editPseudo, 'rgw_index');
+ nfsExport.navigateTo('rgw_index');
+
+ nfsExport.editExport(rgwPseudo, editPseudo);
nfsExport.existTableCell(editPseudo);
});
it('should delete exports and bucket', () => {
nfsExport.navigateTo('rgw_index');
- nfsExport.delete(editPseudo, null, null, true);
+ nfsExport.delete(editPseudo, null, null, true, false, true);
buckets.navigateTo();
- buckets.delete(bucketName, null, null, true);
+ buckets.delete(bucketName, null, null, true, true, true);
});
});
});
cy.get('cd-submit-button').click();
}
- editExport(pseudo: string, editPseudo: string, url: string) {
- this.navigateEdit(pseudo, true, true, url);
+ editExport(pseudo: string, editPseudo: string) {
+ this.navigateEdit(pseudo, true, true);
cy.get('input[name=pseudo]').clear().type(editPseudo);
/**
* Navigates to the edit page
*/
- navigateEdit(name: string, select = true, breadcrumb = true, navigateTo: string = null) {
- if (select) {
+ navigateEdit(
+ name: string,
+ _select = true,
+ breadcrumb = true,
+ navigateTo: string = null,
+ isMultiselect = false
+ ) {
+ if (navigateTo) {
this.navigateTo(navigateTo);
- this.getFirstTableCell(name).click();
+ } else if (isMultiselect) {
+ this.clickActionButtonFromMultiselect(name);
+ cy.contains('Creating...').should('not.exist');
+ cy.contains('button', 'Edit').click();
+ } else {
+ this.clickRowActionButton(name, 'edit');
}
- cy.contains('Creating...').should('not.exist');
- cy.contains('button', 'Edit').click();
if (breadcrumb) {
this.expectBreadcrumbText('Edit');
}
*/
private waitDataTableToLoad() {
cy.get('cd-table').should('exist');
- cy.get('datatable-scroller, .empty-row');
+ cy.get('cds-table table tbody').should('exist');
+ cy.contains('Loading').should('not.exist');
}
getDataTables() {
this.waitDataTableToLoad();
- return cy.get('cd-table .dataTables_wrapper');
+ return cy.get('cd-table cds-table');
}
- private getTableCountSpan(spanType: 'selected' | 'found' | 'total') {
- return cy.contains('.datatable-footer-inner .page-count span', spanType);
+ private getTableCountSpan(_spanType: 'selected' | 'found' | 'total' | 'item' | 'items') {
+ return cy.contains('.cds--pagination__text.cds--pagination__items-count', /item|items/gi);
}
// Get 'selected', 'found', or 'total' row count of a table.
- getTableCount(spanType: 'selected' | 'found' | 'total') {
+ getTableCount(spanType: 'selected' | 'found' | 'total' | 'item' | 'items') {
this.waitDataTableToLoad();
+ cy.wait(1 * 1000);
return this.getTableCountSpan(spanType).then(($elem) => {
- const text = $elem
- .filter((_i, e) => e.innerText.includes(spanType))
- .first()
- .text();
-
- return Number(text.match(/(\d+)\s+\w*/)[1]);
+ const text = $elem.first().text();
+ return Number(text.match(/\b\d+(?= item|items\b)/)[0]);
});
}
// Wait until selected', 'found', or 'total' row count of a table equal to a number.
- expectTableCount(spanType: 'selected' | 'found' | 'total', count: number) {
+ expectTableCount(spanType: 'selected' | 'found' | 'total' | 'item' | 'items', count: number) {
this.waitDataTableToLoad();
this.getTableCountSpan(spanType).should(($elem) => {
const text = $elem.first().text();
- expect(Number(text.match(/(\d+)\s+\w*/)[1])).to.equal(count);
+ expect(Number(text.match(/\b\d+(?= item|items\b)/)[0])).to.equal(count);
});
}
this.waitDataTableToLoad();
this.searchTable(content);
- return cy.contains('.datatable-body-row', content);
+ return cy.contains('[cdstablerow]', content);
}
getTableRows() {
this.waitDataTableToLoad();
- return cy.get('datatable-row-wrapper');
+ return cy.get('[cdstablerow]');
}
/**
if (content) {
this.searchTable(content);
- return cy.contains('.datatable-body-cell-label', content);
+ return cy.contains('[cdstablerow] [cdstabledata]', content);
} else {
- return cy.get('.datatable-body-cell-label').first();
+ return cy.get('[cdstablerow] [cdstabledata]').first();
}
}
getTableCell(columnIndex: number, exactContent: string, partialMatch = false) {
this.waitDataTableToLoad();
this.clearTableSearchInput();
+ cy.wait(1 * 1000);
this.searchTable(exactContent);
if (partialMatch) {
- return cy.contains(
- `datatable-body-row datatable-body-cell:nth-child(${columnIndex})`,
- exactContent
- );
+ return cy.contains(`[cdstablerow] [cdstabledata]:nth-child(${columnIndex})`, exactContent);
}
return cy.contains(
- `datatable-body-row datatable-body-cell:nth-child(${columnIndex})`,
+ `[cdstablerow] [cdstabledata]:nth-child(${columnIndex})`,
new RegExp(`^${exactContent}$`)
);
}
getExpandCollapseElement(content?: string) {
this.waitDataTableToLoad();
-
if (content) {
- return cy.contains('.datatable-body-row', content).find('.tc_expand-collapse');
- } else {
- return cy.get('.tc_expand-collapse').first();
+ return cy
+ .contains('[cdstablerow] [cdstabledata]', content)
+ .parent('[cdstablerow]')
+ .find('[cdstableexpandbutton] .cds--table-expand__button');
}
+ return cy.get('.cds--table-expand__button').first();
}
/**
* Gets column headers of table
*/
- getDataTableHeaders(index = 0) {
+ getDataTableHeaders() {
this.waitDataTableToLoad();
- return cy.get('.datatable-header').its(index).find('.datatable-header-cell');
+ return cy.get('[cdstableheadcell]');
}
/**
filterTable(name: string, option: string) {
this.waitDataTableToLoad();
-
- cy.get('.tc_filter_name > button').click();
- cy.contains(`.tc_filter_name .dropdown-item`, name).click();
-
- cy.get('.tc_filter_option > button').click();
- cy.contains(`.tc_filter_option .dropdown-item`, option).click();
+ cy.get('select#filter_name').select(name);
+ cy.get('select#filter_option').select(option);
}
setPageSize(size: string) {
- cy.get('cd-table .dataTables_paginate input').first().clear({ force: true }).type(size);
+ cy.get('.cds--select__item-count .cds--select-input').select(size, { force: true });
}
- searchTable(text: string) {
+ searchTable(text: string, delay = 35) {
this.waitDataTableToLoad();
- this.setPageSize('10');
- cy.get('[aria-label=search]').first().clear({ force: true }).type(text);
+ cy.get('.cds--search-input').first().clear({ force: true }).type(text, { delay });
}
clearTableSearchInput() {
this.waitDataTableToLoad();
- return cy.get('cd-table .search button').first().click();
+ return cy.get('.cds--search-close').first().click({ force: true });
}
// Click the action button
clickActionButton(action: string) {
- cy.get('.table-actions button.dropdown-toggle').first().click(); // open submenu
- cy.get(`button.${action}`).click(); // click on "action" menu item
+ cy.get('[data-testid="table-action-btn"]').first().click({ force: true }); // open submenu
+ cy.get(`button.${action}`).click({ force: true }); // click on "action" menu item
+ }
+
+ clickActionButtonFromMultiselect(content: string, action?: string) {
+ this.searchTable(content);
+ cy.wait(500);
+ cy.contains('[cdstablerow] [cdstabledata]', content)
+ .parent('[cdstablerow]')
+ .find('[cdstablecheckbox] cds-checkbox [type="checkbox"]')
+ .check({ force: true });
+ if (action) {
+ cy.get(`cds-table-toolbar-actions button.${action}`).click();
+ }
+ }
+
+ /**
+ * Clicks on the kebab menu button and performs an action on same row as content provided
+ * @param content content to be found in a table cell
+ * @param action action to be performed
+ * @param waitTime default 1s. wait time between search resumes and start of looking up content
+ * @param searchDelay delay time in ms between key strokes on search bar
+ */
+ clickRowActionButton(content: string, action: string, waitTime = 1 * 1000, searchDelay?: number) {
+ this.waitDataTableToLoad();
+ this.clearTableSearchInput();
+ cy.contains('Creating...').should('not.exist');
+ this.searchTable(content, searchDelay);
+ cy.wait(waitTime);
+ cy.contains('[cdstablerow] [cdstabledata]', content)
+ .parent('[cdstablerow]')
+ .find('[cdstabledata] [data-testid="table-action-btn"]')
+ .click({ force: true });
+ cy.get(`button.${action}`).click({ force: true });
}
/**
*/
// cdsModal is a temporary variable which will be removed once the carbonization
// is complete
- delete(name: string, columnIndex?: number, section?: string, cdsModal = false) {
- // Selects row
- const getRow = columnIndex
- ? this.getTableCell.bind(this, columnIndex, name, true)
- : this.getFirstTableCell.bind(this);
- getRow(name).click();
- let action: string;
- section === 'hosts' ? (action = 'remove') : (action = 'delete');
+ delete(
+ name: string,
+ columnIndex?: number,
+ section?: string,
+ cdsModal = false,
+ isMultiselect = false,
+ shouldReload = false
+ ) {
+ const action: string = section === 'hosts' ? 'remove' : 'delete';
// Clicks on table Delete/Remove button
- this.clickActionButton(action);
+ if (isMultiselect) {
+ this.clickActionButtonFromMultiselect(name, action);
+ } else {
+ this.clickRowActionButton(name, action);
+ }
// Convert action to SentenceCase and Confirms deletion
const actionUpperCase = action.charAt(0).toUpperCase() + action.slice(1);
cy.get('cd-modal').should('not.exist');
}
// Waits for item to be removed from table
- getRow(name).should('not.exist');
+ if (shouldReload) {
+ cy.reload(true, { log: true, timeout: 5 * 1000 });
+ }
+ (columnIndex
+ ? this.getTableCell(columnIndex, name, true)
+ : this.getFirstTableCell(name)
+ ).should('not.exist');
}
getNestedTableCell(
this.searchNestedTable(selector, exactContent);
if (partialMatch) {
return cy
- .get(`${selector} datatable-body-row datatable-body-cell:nth-child(${columnIndex})`)
+ .get(`${selector} [cdstablerow] [cdstabledata]:nth-child(${columnIndex})`)
.should('contain', exactContent);
}
return cy
.get(`${selector}`)
.contains(
- `datatable-body-row datatable-body-cell:nth-child(${columnIndex})`,
+ `[cdstablerow] [cdstabledata]:nth-child(${columnIndex})`,
new RegExp(`^${exactContent}$`)
);
}
});
it('should delete bucket', () => {
- buckets.delete(bucket_name, null, null, true);
+ buckets.delete(bucket_name, null, null, true, true);
});
it('should create bucket with object locking enabled', () => {
buckets.edit(bucket_name, BucketsPageHelper.USERS[1], true);
buckets.getDataTables().should('contain.text', BucketsPageHelper.USERS[1]);
- buckets.delete(bucket_name, null, null, true);
+ buckets.delete(bucket_name, null, null, true, true);
});
});
buckets.create(bucket_name, BucketsPageHelper.USERS[0]);
buckets.testInvalidEdit(bucket_name);
buckets.navigateTo();
- buckets.delete(bucket_name, null, null, true);
+ buckets.delete(bucket_name, null, null, true, true);
});
});
});
@PageHelper.restrictTo(pages.index.url)
edit(name: string, new_owner: string, isLocking = false) {
- this.navigateEdit(name);
+ this.navigateEdit(name, false, false, null, true);
// Placement target is not allowed to be edited and should be hidden
cy.get('input[name=placement-target]').should('not.exist');
this.getTableCell(this.columnIndex.name, name)
.parent()
- .find(`datatable-body-cell:nth-child(${this.columnIndex.owner})`)
+ .find(`[cdstabledata]:nth-child(${this.columnIndex.owner})`)
.should(($elements) => {
const bucketName = $elements.text();
expect(bucketName).to.eq(new_owner);
// Check if the owner is updated
this.getTableCell(this.columnIndex.name, name)
.parent()
- .find(`datatable-body-cell:nth-child(${this.columnIndex.owner})`)
+ .find(`[cdstabledata]:nth-child(${this.columnIndex.owner})`)
.should(($elements) => {
const bucketName = $elements.text();
expect(bucketName).to.eq(new_owner);
cy.get('@versioningValueCell').should('have.text', this.versioningStateEnabled);
// Disable versioning:
- this.navigateEdit(name);
+ this.navigateEdit(name, false, true, null, true);
cy.get('label[for=versioning]').click();
cy.get('input[id=versioning]').should('not.be.checked');
}
testInvalidEdit(name: string) {
- this.navigateEdit(name);
+ this.navigateEdit(name, false, true, null, true);
cy.get('input[id=versioning]').should('exist').and('not.be.checked');
cy.get('#address').should('have.class', 'ng-valid');
cy.get('#kms_provider').should('be.disabled');
cy.contains('button', 'Submit').click();
- this.getTableCell(this.columnIndex.address, new_address)
- .parent()
- .find(`datatable-body-cell:nth-child(${this.columnIndex.address})`)
- .should(($elements) => {
- const address = $elements.text();
- expect(address).to.eq(new_address);
- });
+ this.getFirstTableCell(new_address);
}
private selectKmsProvider(provider: string) {
.its(1)
.find('cd-table')
.should('have.length', 1) // Only 1 table should be renderer
- .find('datatable-body-cell');
+ .find('[cdstabledata]');
}
checkTables() {
// click on a daemon so details table appears
- cy.get('.datatable-body-cell-label').first().click();
+ this.getExpandCollapseElement().click();
// check details table is visible
// check at least one field is present
it('should delete policy', () => {
multisite.navigateTo();
- multisite.delete('test', null, null, true);
+ multisite.delete('test', null, null, true, true);
});
});
cy.contains('button', 'Edit Sync Policy Group').click();
this.searchTable(group_id);
- cy.get(`datatable-body-cell:nth-child(${this.columnIndex.status})`)
+ cy.get(`[cdstabledata]:nth-child(${this.columnIndex.status})`)
.find('.badge-warning')
.should('contain', status);
}
cy.get('button.tc_submitButton').click();
- cy.get('cd-rgw-multisite-sync-policy-details .datatable-body-cell-label').should(
- 'contain',
- flow_id
- );
+ cy.get('cd-rgw-multisite-sync-policy-details .[cdstabledata]').should('contain', flow_id);
cy.get('cd-rgw-multisite-sync-policy-details')
.first()
});
cy.get('cd-rgw-multisite-sync-policy-details').within(() => {
- cy.get('.datatable-body-cell-label').should('contain', flow_id);
+ cy.get('.[cdstabledata]').should('contain', flow_id);
cy.get('[aria-label=search]').first().clear({ force: true }).type(flow_id);
cy.get('input.cd-datatable-checkbox').first().check();
cy.get('.table-actions button').first().click();
}
getTableCellWithContent(nestedClass: string, content: string) {
- return cy.contains(`${nestedClass} .datatable-body-cell-label`, content);
+ return cy.contains(`${nestedClass} .[cdstabledata]`, content);
}
@PageHelper.restrictTo(pages.index.url)
this.getTab('Flow').should('exist');
this.getTab('Flow').click();
cy.get('cd-rgw-multisite-sync-policy-details').within(() => {
- cy.get('.datatable-body-cell-label').should('contain', flow_id);
+ cy.get('.[cdstabledata]').should('contain', flow_id);
cy.get('[aria-label=search]').first().clear({ force: true }).type(flow_id);
});
.type(dest_zones[0]);
cy.get('cd-rgw-multisite-sync-policy-details cd-table')
.eq(1)
- .find('.datatable-body-cell-label')
+ .find('.[cdstabledata]')
.should('contain', dest_zones[0]);
}
}
cy.get('button.tc_submitButton').click();
- cy.get('cd-rgw-multisite-sync-policy-details .datatable-body-cell-label').should(
- 'contain',
- pipe_id
- );
+ cy.get('cd-rgw-multisite-sync-policy-details .[cdstabledata]').should('contain', pipe_id);
cy.get('cd-rgw-multisite-sync-policy-details')
.first()
});
cy.get('cd-rgw-multisite-sync-policy-details').within(() => {
- cy.get('.datatable-body-cell-label').should('contain', pipe_id);
+ cy.get('.[cdstabledata]').should('contain', pipe_id);
cy.get('[aria-label=search]').first().clear({ force: true }).type(pipe_id);
cy.get('input.cd-datatable-checkbox').first().check();
cy.get('.table-actions button').first().click();
this.getTab('Pipe').should('exist');
this.getTab('Pipe').click();
cy.get('cd-rgw-multisite-sync-policy-details').within(() => {
- cy.get('.datatable-body-cell-label').should('contain', pipe_id);
+ cy.get('.[cdstabledata]').should('contain', pipe_id);
cy.get('[aria-label=search]').first().clear({ force: true }).type(pipe_id);
});
this.getTableCell(this.columnIndex.roleName, name)
.click()
.parent()
- .find(`datatable-body-cell:nth-child(${this.columnIndex.maxSessionDuration})`)
+ .find(`[cdstabledata]:nth-child(${this.columnIndex.maxSessionDuration})`)
.should(($elements) => {
const roleName = $elements.map((_, el) => el.textContent).get();
expect(roleName).to.include(`${maxSessionDuration} hours`);
});
it('should delete user', () => {
- users.delete(user_name, null, null, true);
+ users.delete(user_name, null, null, true, true);
});
});
@PageHelper.restrictTo(pages.index.url)
edit(name: string, new_fullname: string, new_email: string, new_maxbuckets: string) {
- this.navigateEdit(name);
+ this.navigateEdit(name, false, true, null, true);
// Change the full name field
cy.get('#display_name').click().clear().type(new_fullname);
// Click the user and check its details table for updated content
this.getExpandCollapseElement(name).click();
- cy.get('.datatable-row-detail')
+ cy.get('[data-testid="datatable-row-detail"]')
.should('contain.text', new_fullname)
.and('contain.text', new_email)
.and('contain.text', new_maxbuckets);
cy.contains('#max_buckets + .invalid-feedback', 'The entered value must be >= 1.');
this.navigateTo();
- this.delete(tenant + '$' + uname, null, null, true);
+ this.delete(tenant + '$' + uname, null, null, true, true);
}
invalidEdit() {
this.navigateTo('create');
this.create(tenant, uname, 'xxx', 'xxx@xxx', '50');
const name = tenant + '$' + uname;
- this.navigateEdit(name);
+ this.navigateEdit(name, false, true, null, true);
// put invalid email to make field invalid
cy.get('#email')
cy.contains('#max_buckets + .invalid-feedback', 'The entered value must be >= 1.');
this.navigateTo();
- this.delete(tenant + '$' + uname, null, null, true);
+ this.delete(tenant + '$' + uname, null, null, true, true);
}
}
after(() => {
cy.login();
pools.navigateTo();
- pools.delete(poolName, null, null, true);
+ pools.delete(poolName, null, null, true, false, true);
});
beforeEach(() => {
"@ngx-formly/bootstrap": "6.1.1",
"@ngx-formly/core": "6.1.1",
"@popperjs/core": "2.10.2",
- "@swimlane/ngx-datatable": "18.0.0",
"@types/file-saver": "2.0.1",
"async-mutex": "0.2.4",
"bootstrap": "5.2.3",
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/@swimlane/ngx-datatable": {
- "version": "18.0.0",
- "resolved": "https://registry.npmjs.org/@swimlane/ngx-datatable/-/ngx-datatable-18.0.0.tgz",
- "integrity": "sha512-secqjzlLpGJqoXjcoCoTf8ClnVlZAENJcXvuBfseGenOD+evGNXc4UTZhwCPDUBlJ4xnMZHUWK6IVk5sXe+WlQ==",
- "dependencies": {
- "tslib": "^2.0.0"
- },
- "peerDependencies": {
- "@angular/common": "^10.0.0",
- "@angular/core": "^10.0.0",
- "@angular/platform-browser": "^10.0.0",
- "rxjs": "^6.5.5"
- }
- },
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
"@ngx-formly/bootstrap": "6.1.1",
"@ngx-formly/core": "6.1.1",
"@popperjs/core": "2.10.2",
- "@swimlane/ngx-datatable": "18.0.0",
"@types/file-saver": "2.0.1",
"async-mutex": "0.2.4",
"bootstrap": "5.2.3",
</div>
<ng-template #highlightTpl
- let-row="row"
- let-value="value">
+ let-row="data.row"
+ let-value="data.value">
<span *ngIf="row.default === undefined || row.default === row.current">{{ value }}</span>
<strong *ngIf="row.default !== undefined && row.default !== row.current">{{ value }}</strong>
</ng-template>
(fetchData)="getTargets()"
(setExpandedRow)="setExpandedRow($event)"
(updateSelection)="updateSelection($event)">
- <div class="table-actions btn-toolbar">
+ <div class="table-actions">
<cd-table-actions class="btn-group"
[permission]="permission"
[selection]="selection"
</button>
</div>
- <cd-iscsi-target-details cdTableDetail
- *ngIf="expandedRow"
- [cephIscsiConfigVersion]="cephIscsiConfigVersion"
- [selection]="expandedRow"
- [settings]="settings"></cd-iscsi-target-details>
+ <ng-container *ngIf="expandedRow">
+ <cd-iscsi-target-details *cdTableDetail
+ [cephIscsiConfigVersion]="cephIscsiConfigVersion"
+ [selection]="expandedRow"
+ [settings]="settings"></cd-iscsi-target-details>
+ </ng-container>
</cd-table>
expect(tableActions).toEqual({
'create,update,delete': {
actions: ['Create', 'Edit', 'Delete'],
- primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,update': {
actions: ['Create', 'Edit'],
- primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,delete': {
actions: ['Create', 'Delete'],
- primary: { multiple: 'Create', executing: 'Delete', single: 'Delete', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
create: {
actions: ['Create'],
- primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'update,delete': {
actions: ['Edit', 'Delete'],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
update: {
actions: ['Edit'],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: 'Edit',
+ executing: 'Edit',
+ single: 'Edit',
+ no: 'Edit'
+ }
},
delete: {
actions: ['Delete'],
- primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ primary: {
+ multiple: 'Delete',
+ executing: 'Delete',
+ single: 'Delete',
+ no: 'Delete'
+ }
},
'no-permissions': {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
}
});
});
</div>
<ng-template #iscsiSparklineTpl
- let-row="row"
- let-value="value">
+ let-row="data.row"
+ let-value="data.value">
<span *ngIf="row.backstore === 'user:rbd'">
<cd-sparkline [data]="value"
[isBinary]="row.cdIsBinary"></cd-sparkline>
</ng-template>
<ng-template #iscsiPerSecondTpl
- let-row="row"
- let-value="value">
+ let-row="data.row"
+ let-value="data.value">
<span *ngIf="row.backstore === 'user:rbd'">
{{ value }} /s
</span>
</ng-template>
<ng-template #iscsiRelativeDateTpl
- let-row="row"
- let-value="value">
+ let-row="data.row"
+ let-value="data.value">
<span *ngIf="row.backstore === 'user:rbd'">
{{ value | relativeDate | notAvailable }}
</span>
</cd-table>
<ng-template #healthTmpl
- let-row="row"
- let-value="value">
+ let-row="data.row"
+ let-value="data.value">
<span [ngClass]="row.health_color | mirrorHealthColor">{{ value }}</span>
</ng-template>
<div [ngbNavOutlet]="nav"></div>
<ng-template #stateTmpl
- let-row="row"
- let-value="value">
+ let-row="data.row"
+ let-value="data.value">
<span [ngClass]="row.state_color | mirrorHealthColor">{{ value }}</span>
</ng-template>
<ng-template #progressTmpl
- let-row="row"
- let-value="value">
+ let-row="data.row"
+ let-value="data.value">
<div *ngIf="row.state === 'Replaying'">
</div>
<div class="w-100 h-100 d-flex justify-content-center align-items-center">
</ng-template>
<ng-template #entriesBehindPrimaryTpl
- let-row="row"
- let-value="value">
+ let-row="data.row"
+ let-value="data.value">
<span *ngIf="row.mirror_mode === 'journal'">
{{ value }}
</span>
import EditIcon from '@carbon/icons/es/edit/32';
import CheckMarkIcon from '@carbon/icons/es/checkmark/32';
import ResetIcon from '@carbon/icons/es/reset/32';
+import DocumentAddIcon from '@carbon/icons/es/document--add/16';
+import DocumentImportIcon from '@carbon/icons/es/document--import/16';
@NgModule({
imports: [
})
export class MirroringModule {
constructor(private iconService: IconService) {
- this.iconService.registerAll([EditIcon, CheckMarkIcon, ResetIcon]);
+ this.iconService.registerAll([
+ EditIcon,
+ CheckMarkIcon,
+ ResetIcon,
+ DocumentAddIcon,
+ DocumentImportIcon
+ ]);
}
}
[byId]="false">
</cd-copy-2-clipboard-button>
</div>
- </cds-text-label>
+ </cds-text-label>
+ </div>
+ <div cdsCol="{md:5}">
+ <div class="d-flex flex-row-reverse gap-3">
+ <ng-container *ngFor="let action of tableActions">
+ <button type="button"
+ [cdsButton]="action.buttonKind"
+ [title]="action.name"
+ (click)="action.click($event)"
+ [disabled]="action.disable()"
+ [attr.aria-label]="action.name"
+ [attr.data-testid]="action.name"
+ [preserveFragment]="action.preserveFragment ? '' : null">
+ <span i18n>{{ action.name }}</span>
+ <svg class="cds--btn__icon"
+ [cdsIcon]="action.icon"
+ size="16"></svg>
+ </button>
+ </ng-container>
</div>
- <div cdsCol="{md:5}">
- <cd-table-actions class="table-actions float-end"
- [permission]="permission"
- [selection]="selection"
- [tableActions]="tableActions">
- </cd-table-actions>
</div>
</div>
</form>
const createBootstrapAction: CdTableAction = {
permission: 'update',
- icon: Icons.upload,
+ icon: 'document--add',
click: () => this.createBootstrapModal(),
name: $localize`Create Bootstrap Token`,
canBePrimary: () => true,
- disable: () => false
+ disable: () => false,
+ buttonKind: 'primary'
};
const importBootstrapAction: CdTableAction = {
permission: 'update',
- icon: Icons.download,
+ icon: 'document--import',
click: () => this.importBootstrapModal(),
name: $localize`Import Bootstrap Token`,
- disable: () => false
+ disable: () => false,
+ buttonKind: 'tertiary'
};
this.tableActions = [createBootstrapAction, importBootstrapAction];
}
</cd-table>
<ng-template #healthTmpl
- let-row="row"
- let-value="value">
+ let-row="data.row"
+ let-value="data.value">
<span [ngClass]="row.health_color | mirrorHealthColor">{{ value }}</span>
</ng-template>
<ng-template #localTmpl>
</cd-table-actions>
</div>
- <cd-nvmeof-subsystems-details cdTableDetail
+ <cd-nvmeof-subsystems-details *cdTableDetail
[selection]="expandedRow">
</cd-nvmeof-subsystems-details>
</cd-table>
</cd-table>
<ng-template #configurationSourceTpl
- let-value="value">
+ let-value="data.value">
<div [ngSwitch]="value">
<span *ngSwitchCase="'global'"
</ng-template>
<ng-template #configurationValueTpl
- let-row="row"
- let-value="value">
+ let-row="data.row"
+ let-value="data.value">
<div [ngSwitch]="row.type">
<span *ngSwitchCase="typeField.bps">{{ value | dimlessBinaryPerSecond }}</span>
<span *ngSwitchCase="typeField.milliseconds">{{ value | milliseconds }}</span>
import { RouterTestingModule } from '@angular/router/testing';
import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
-import { NgxDatatableModule } from '@swimlane/ngx-datatable';
import { NgChartsModule } from 'ng2-charts';
import { ComponentsModule } from '~/app/shared/components/components.module';
imports: [
BrowserAnimationsModule,
FormsModule,
- NgxDatatableModule,
RouterTestingModule,
ComponentsModule,
NgbDropdownModule,
</ng-container>
<ng-template #poolConfigurationSourceTpl
- let-row="row"
- let-value="value">
+ let-row="data.row"
+ let-value="data.value">
<ng-container *ngIf="+value; else global">
<strong i18n
i18n-ngbTooltip
[selection]="selection"
[tableActions]="tableActions">
</cd-table-actions>
- <cd-rbd-details cdTableDetail
+ <cd-rbd-details *cdTableDetail
[selection]="expandedRow">
</cd-rbd-details>
</cd-table>
<ng-template #parentTpl
- let-value="value">
+ let-value="data.value">
<span *ngIf="value">{{ value.pool_name }}<span
*ngIf="value.pool_namespace">/{{ value.pool_namespace }}</span>/{{ value.image_name }}@{{ value.snap_name }}</span>
<span *ngIf="!value">-</span>
</ng-template>
<ng-template #mirroringTpl
- let-value="value"
- let-row="row">
+ let-value="data.value"
+ let-row="data.row">
<span *ngIf="value.length === 3; else probb"
class="badge badge-info">{{ value[0] }}</span>
<span *ngIf="value.length === 3"
</ng-template>
<ng-template #ScheduleTpl
- let-value="value"
- let-row="row">
+ let-value="data.value"
+ let-row="data.row">
<span *ngIf="value.length === 3"
class="badge badge-info">{{ value[2] | cdDate }}</span>
</ng-template>
</ng-template>
<ng-template #removingStatTpl
- let-column="column"
- let-value="value"
- let-row="row">
+ let-column="data.column"
+ let-value="data.value"
+ let-row="data.row">
<i [ngClass]="[icons.spinner, icons.spin]"
*ngIf="row.cdExecuting"></i>
</ng-template>
<ng-template #imageUsageTpl
- let-row="row">
+ let-row="data.row">
<span *ngIf="row.features_name && (!row.features_name.includes('fast-diff') || row.mirror_mode === 'snapshot') ; else usageBar"
[ngbTooltip]="usageTooltip">
<span>-</span>
'Promote',
'Demote'
],
- primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,update': {
actions: [
'Promote',
'Demote'
],
- primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,delete': {
actions: ['Create', 'Copy', 'Delete', 'Move to Trash'],
- primary: { multiple: 'Create', executing: 'Copy', single: 'Copy', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
create: {
actions: ['Create', 'Copy'],
- primary: { multiple: 'Create', executing: 'Copy', single: 'Copy', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'update,delete': {
actions: [
'Promote',
'Demote'
],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
update: {
actions: ['Edit', 'Flatten', 'Resync', 'Remove Scheduling', 'Promote', 'Demote'],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
delete: {
actions: ['Delete', 'Move to Trash'],
- primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
'no-permissions': {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
}
});
});
forceIdentifier="true"
selectionType="single"
(updateSelection)="updateSelection($event)">
- <div class="table-actions btn-toolbar">
+ <div class="table-actions">
<cd-table-actions class="btn-group"
[permission]="permission"
[selection]="selection"
'Rollback',
'Delete'
],
- primary: { multiple: 'Create', executing: 'Rename', single: 'Rename', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,update': {
actions: ['Create', 'Rename', 'Protect', 'Unprotect', 'Clone', 'Copy', 'Rollback'],
- primary: { multiple: 'Create', executing: 'Rename', single: 'Rename', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,delete': {
actions: ['Create', 'Clone', 'Copy', 'Delete'],
- primary: { multiple: 'Create', executing: 'Clone', single: 'Clone', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
create: {
actions: ['Create', 'Clone', 'Copy'],
- primary: { multiple: 'Create', executing: 'Clone', single: 'Clone', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'update,delete': {
actions: ['Rename', 'Protect', 'Unprotect', 'Rollback', 'Delete'],
- primary: { multiple: 'Rename', executing: 'Rename', single: 'Rename', no: 'Rename' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
update: {
actions: ['Rename', 'Protect', 'Unprotect', 'Rollback'],
- primary: { multiple: 'Rename', executing: 'Rename', single: 'Rename', no: 'Rename' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
delete: {
actions: ['Delete'],
- primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ primary: {
+ multiple: 'Delete',
+ executing: 'Delete',
+ single: 'Delete',
+ no: 'Delete'
+ }
},
'no-permissions': {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
}
});
});
[autoReload]="-1"
(fetchData)="taskListService.fetch()"
(updateSelection)="updateSelection($event)">
- <div class="table-actions btn-toolbar">
+ <div class="table-actions">
<cd-table-actions class="btn-group"
[permission]="permission"
[selection]="selection"
[tableActions]="tableActions">
</cd-table-actions>
- <button class="btn btn-light"
+ <button cdsButton="tertiary"
type="button"
(click)="purgeModal()"
[disabled]="disablePurgeBtn"
*ngIf="permission.delete">
- <i [ngClass]="[icons.destroy]"
- aria-hidden="true"></i>
<ng-container i18n>Purge Trash</ng-container>
+ <svg class="cds--btn__icon"
+ cdsIcon="close"
+ size="16"></svg>
</button>
</div>
</cd-table>
<ng-template #expiresTpl
- let-row="row"
- let-value="value">
+ let-row="data.row"
+ let-value="data.value">
<ng-container *ngIf="row.cdIsExpired"
i18n>Expired at</ng-container>
};
fixture.detectChanges();
- const purge = fixture.debugElement.query(By.css('.table-actions button .fa-times'));
+ const purge = fixture.debugElement.query(By.css('.table-actions button'));
expect(purge).not.toBeNull();
});
};
fixture.detectChanges();
- const purge = fixture.debugElement.query(By.css('.table-actions button .fa-times'));
+ const purge = fixture.debugElement.query(By.css('.table-actions button'));
expect(purge).toBeNull();
});
});
});
it('should create', () => {
- fixture.detectChanges();
expect(component).toBeTruthy();
});
expect(tableActions).toEqual({
'create,update,delete': {
actions: ['Evict'],
- primary: { multiple: 'Evict', executing: 'Evict', single: 'Evict', no: 'Evict' }
+ primary: {
+ multiple: 'Evict',
+ executing: 'Evict',
+ single: 'Evict',
+ no: 'Evict'
+ }
},
'create,update': {
actions: ['Evict'],
- primary: { multiple: 'Evict', executing: 'Evict', single: 'Evict', no: 'Evict' }
+ primary: {
+ multiple: 'Evict',
+ executing: 'Evict',
+ single: 'Evict',
+ no: 'Evict'
+ }
},
'create,delete': {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
create: {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
'update,delete': {
actions: ['Evict'],
- primary: { multiple: 'Evict', executing: 'Evict', single: 'Evict', no: 'Evict' }
+ primary: {
+ multiple: 'Evict',
+ executing: 'Evict',
+ single: 'Evict',
+ no: 'Evict'
+ }
},
update: {
actions: ['Evict'],
- primary: { multiple: 'Evict', executing: 'Evict', single: 'Evict', no: 'Evict' }
+ primary: {
+ multiple: 'Evict',
+ executing: 'Evict',
+ single: 'Evict',
+ no: 'Evict'
+ }
},
delete: {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
'no-permissions': {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
}
});
});
<!-- templates -->
<ng-template #poolUsageTpl
- let-row="row">
+ let-row="data.row">
<cd-usage-bar [total]="row.size"
[used]="row.used"
[title]="row.pool_name"></cd-usage-bar>
</ng-template>
<ng-template #activityTmpl
- let-row="row"
- let-value="value">
+ let-row="data.row"
+ let-value="data.value">
{{ row.state === 'standby-replay' ? 'Evts' : 'Reqs' }}: {{ value | dimless }} /s
</ng-template>
</div>
<ng-template #origin
- let-row="row"
- let-value="value">
+ let-row="data.row"
+ let-value="data.value">
<span class="quota-origin"
(click)="selectOrigin(value)">{{value}}</span>
</ng-template>
expect(tableActions).toEqual({
'create,update,delete': {
actions: ['Create', 'Delete'],
- primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,update': {
actions: ['Create'],
- primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,delete': {
actions: ['Create', 'Delete'],
- primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
create: {
actions: ['Create'],
- primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'update,delete': {
actions: ['Delete'],
- primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ primary: {
+ multiple: 'Delete',
+ executing: 'Delete',
+ single: 'Delete',
+ no: 'Delete'
+ }
},
update: {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
delete: {
actions: ['Delete'],
- primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ primary: {
+ multiple: 'Delete',
+ executing: 'Delete',
+ single: 'Delete',
+ no: 'Delete'
+ }
},
'no-permissions': {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
}
});
});
expect(tableActions).toEqual({
'create,update,delete': {
actions: ['Set', 'Update', 'Unset'],
- primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
'create,update': {
actions: ['Set', 'Update', 'Unset'],
- primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
'create,delete': {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
create: {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
'update,delete': {
actions: ['Set', 'Update', 'Unset'],
- primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
update: {
actions: ['Set', 'Update', 'Unset'],
- primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
delete: {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
'no-permissions': {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
}
});
});
[hasDetails]="true"
(setExpandedRow)="setExpandedRow($event)"
(updateSelection)="updateSelection($event)">
- <cd-cephfs-tabs cdTableDetail
+ <cd-cephfs-tabs *cdTableDetail
[selection]="expandedRow">
</cd-cephfs-tabs>
- <div class="table-actions btn-toolbar">
+ <div class="table-actions">
<cd-table-actions [permission]="permissions.cephfs"
[selection]="selection"
class="btn-group"
<ng-template
#pathTpl
- let-row="row">
+ let-row="data.row">
<span
class="fw-bold"
[ngbTooltip]="fullpathTpl"
<ng-template
#retentionTpl
- let-row="row">
+ let-row="data.row">
<ul *ngIf="row.retentionCopy.length; else noDataTpl">
<li *ngFor="let ret of row.retentionCopy">{{ ret }}</li>
</ul>
<ng-template
#subvolTpl
- let-row="row">
+ let-row="data.row">
<span *ngIf="row.subvol; else noDataTpl">
{{row.subvol}}
</span>
(fetchData)="fetchData()"
(updateSelection)="updateSelection($event)"
>
- <div class="table-actions btn-toolbar">
+ <div class="table-actions">
<cd-table-actions
[permission]="permissions.cephfs"
[selection]="selection"
(fetchData)="fetchData()"
(updateSelection)="updateSelection($event)">
- <div class="table-actions btn-toolbar">
+ <div class="table-actions">
<cd-table-actions [permission]="permissions.cephfs"
[selection]="selection"
class="btn-group"
</ng-container>
<ng-template #quotaUsageTpl
- let-row="row">
+ let-row="data.row">
<cd-usage-bar *ngIf="row.info.bytes_pcent && row.info.bytes_pcent !== 'undefined'; else noLimitTpl"
[total]="row.info.bytes_quota"
[used]="row.info.bytes_used"
</ng-template>
<ng-template #typeTpl
- let-value="value">
+ let-value="data.value">
<cd-label [value]="value"></cd-label>
</ng-template>
<ng-template #modeToHumanReadableTpl
- let-value="value">
+ let-value="data.value">
<span *ngFor="let result of (value | octalToHumanReadable)"
[ngClass]="result.class"
[ngbTooltip]="result.toolTip">
(fetchData)="fetchData()"
(updateSelection)="updateSelection($event)">
- <div class="table-actions btn-toolbar">
+ <div class="table-actions">
<cd-table-actions [permission]="permissions.cephfs"
[selection]="selection"
class="btn-group"
</div>
<ng-template #quotaUsageTpl
- let-row="row">
+ let-row="data.row">
<cd-usage-bar *ngIf="row.info.bytes_pcent && row.info.bytes_pcent !== 'undefined'; else noLimitTpl"
[total]="row.info.bytes_quota"
[used]="row.info.bytes_used"
</ng-template>
<ng-template #typeTpl
- let-value="value">
+ let-value="data.value">
<cd-label [value]="value"></cd-label>
</ng-template>
<ng-template #modeToHumanReadableTpl
- let-value="value">
+ let-value="data.value">
<span *ngFor="let result of (value | octalToHumanReadable)"
[ngClass]="result.class"
[ngbTooltip]="result.toolTip">
</ng-template>
<ng-template #nameTpl
- let-row="row">
+ let-row="data.row">
<span class="fw-bold">{{row.name}}</span>
- <span *ngIf="row.info.state === 'complete'; else snapshotRetainedTpl">
+ <span *ngIf="row?.info?.state === 'complete'; else snapshotRetainedTpl">
<i [ngClass]="[icons.success, icons.large]"
ngbTooltip="{{row.name}} is ready to use"
class="text-success"></i>
(fetchData)="fetchData()"
(updateSelection)="updateSelection($event)">
- <div class="table-actions btn-toolbar">
+ <div class="table-actions">
<cd-table-actions [permission]="permissions.cephfs"
[selection]="selection"
class="btn-group"
[selection]="selection"
[tableActions]="tableActions">
</cd-table-actions>
- <cd-configuration-details cdTableDetail
+ <cd-configuration-details *cdTableDetail
[selection]="expandedRow">
</cd-configuration-details>
</cd-table>
<ng-template #confValTpl
- let-value="value">
+ let-value="data.value">
<span *ngIf="value">
<span *ngFor="let conf of value; last as isLast">
{{ conf.section }}: {{ conf.value }}{{ !isLast ? "," : "" }}<br />
}
}
-::ng-deep cd-configuration datatable-body-cell.wrap {
+::ng-deep cd-configuration td[cdstabledata].wrap {
word-break: break-all;
}
import { configureTestBed } from '~/testing/unit-test-helper';
import { ConfigurationDetailsComponent } from './configuration-details/configuration-details.component';
import { ConfigurationComponent } from './configuration.component';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
describe('ConfigurationComponent', () => {
let component: ConfigurationComponent;
let fixture: ComponentFixture<ConfigurationComponent>;
configureTestBed({
- declarations: [ConfigurationComponent, ConfigurationDetailsComponent],
+ declarations: [ConfigurationComponent, ConfigurationDetailsComponent, TableComponent],
imports: [
BrowserAnimationsModule,
SharedModule,
});
it('should check header text', () => {
- expect(fixture.debugElement.query(By.css('.datatable-header')).nativeElement.textContent).toBe(
- ['Name', 'Description', 'Current value', 'Default', 'Editable'].join('')
- );
+ const cdTableEl = fixture.debugElement.query(By.directive(TableComponent));
+ const cdTableComponent: TableComponent = cdTableEl.componentInstance;
+ cdTableComponent.ngAfterViewInit();
+ fixture.detectChanges();
+ const actual = fixture.debugElement.query(By.css('thead')).nativeElement.textContent.trim();
+ const expected = 'Name Description Current value Default Editable';
+ expect(actual).toBe(expected);
});
});
(setExpandedRow)="setExpandedRow($event)"
(updateSelection)="updateSelection($event)"
[toolHeader]="!hideToolHeader">
- <div class="table-actions btn-toolbar">
+ <div class="table-actions">
<cd-table-actions [permission]="permissions.hosts"
[selection]="selection"
class="btn-group"
[tableActions]="expandClusterActions">
</cd-table-actions>
</div>
- <cd-host-details cdTableDetail
+ <cd-host-details *cdTableDetail
[permissions]="permissions"
[selection]="expandedRow">
</cd-host-details>
<div [ngbNavOutlet]="nav"></div>
<ng-template #servicesTpl
- let-services="value">
+ let-services="data.value">
<span *ngFor="let service of services">
<cd-label [key]="service['type']"
[value]="service['count']"
</ng-template>
<ng-template #hostNameTpl
- let-row="row">
+ let-row="data.row">
<span [ngClass]="row">
{{ row.hostname }}
</span><br>
<ng-template #hostMetricTmpl
- let-value="value">
+ let-value="data.value">
<div *ngIf="validValue(value)">
<span>{{ value }}</span>
</div>
</ng-template>
<ng-template #hostDimlessTmpl
- let-value="value">
+ let-value="data.value">
<div *ngIf="!validValue(value)">
<span ngbTooltip="Not available. Data could not be fetched from Ceph">-</span>
</div>
fixture.detectChanges();
const spans = fixture.debugElement.nativeElement.querySelectorAll(
- '.datatable-body-cell-label span'
+ 'cds-table > table > tbody > tr > td > span'
);
expect(spans[0].textContent.trim()).toBe(hostname);
});
fixture.detectChanges();
const spans = fixture.debugElement.nativeElement.querySelectorAll(
- '.datatable-body-cell-label span span.badge.badge-background-primary'
+ '[cdstabledata] span span.badge.badge-background-primary'
);
expect(spans[0].textContent).toContain('mgr: 2');
expect(spans[1].textContent).toContain('osd: 3');
component.getHosts(new CdTableFetchDataContext(() => undefined));
fixture.detectChanges();
- const spans = fixture.debugElement.nativeElement.querySelectorAll(
- '.datatable-body-cell-label span'
- );
+ const spans = fixture.debugElement.nativeElement.querySelectorAll('[cdstabledata] span');
expect(spans[7].textContent).toBe('-');
});
component.getHosts(new CdTableFetchDataContext(() => undefined));
fixture.detectChanges();
- const spans = fixture.debugElement.nativeElement.querySelectorAll(
- '.datatable-body-cell-label span'
- );
+ const spans = fixture.debugElement.nativeElement.querySelectorAll('[cdstabledata] span');
expect(spans[7].textContent).toBe('-');
});
await fixture.whenStable();
component.getHosts(new CdTableFetchDataContext(() => undefined));
+ fixture.detectChanges();
hostListSpy.and.callFake(() => of(fakeHosts));
fixture.detectChanges();
for (const test of tests) {
{
name: this.actionLabels.EXPAND_CLUSTER,
permission: 'create',
+ buttonKind: 'secondary',
icon: Icons.expand,
routerLink: '/expand-cluster',
disable: (selection: CdTableSelection) => this.getDisable('add', selection),
[action: string]: { disabled: boolean; disableDesc: string };
}
) => {
- fixture.detectChanges();
- await fixture.whenStable();
+ const component = fixture.componentInstance;
+ const selection = component.selection;
const tableActionElement = fixture.debugElement.query(By.directive(TableActionsComponent));
- // There is actually only one action for now
+ const tableActionComponent: TableActionsComponent = tableActionElement.componentInstance;
+ tableActionComponent.selection = selection;
+
const actions = {};
tableActions.forEach((action) => {
- const actionElement = tableActionElement.query(By.css('button'));
- actions[action.name] = {
- disabled: actionElement.classes.disabled ? true : false,
- disableDesc: actionElement.properties.title
- };
+ if (expectResult[action.name]) {
+ actions[action.name] = {
+ disabled: tableActionComponent.disableSelectionAction(action),
+ disableDesc: tableActionComponent.useDisableDesc(action) || ''
+ };
+ }
});
expect(actions).toEqual(expectResult);
};
[selection]="selection"
[tableActions]="tableActions">
</cd-table-actions>
- <cd-mgr-module-details cdTableDetail
+ <cd-mgr-module-details *cdTableDetail
[selection]="expandedRow">
</cd-mgr-module-details>
</cd-table>
expect(tableActions).toEqual({
'create,update,delete': {
actions: ['Edit', 'Enable', 'Disable'],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
'create,update': {
actions: ['Edit', 'Enable', 'Disable'],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
'create,delete': {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
+ },
+ create: {
+ actions: [],
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
- create: { actions: [], primary: { multiple: '', executing: '', single: '', no: '' } },
'update,delete': {
actions: ['Edit', 'Enable', 'Disable'],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
update: {
actions: ['Edit', 'Enable', 'Disable'],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
+ },
+ delete: {
+ actions: [],
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
- delete: { actions: [], primary: { multiple: '', executing: '', single: '', no: '' } },
'no-permissions': {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
}
});
});
expect(component.table.refreshBtn).toHaveBeenCalled();
}));
- it.only('should not disable module without selecting one', () => {
+ it('should not disable module without selecting one', () => {
expect(component.getTableActionDisabledDesc()).toBeTruthy();
});
selectionType="single"
[maxLimit]="25"
(updateSelection)="updateSelection($event)">
- <div class="table-actions btn-toolbar">
+ <div class="table-actions">
<cd-table-actions [permission]="permissions.user"
[selection]="selection"
class="btn-group"
</ng-container>
<ng-template #urlTpl
- let-row="row">
+ let-row="data.row">
<a target="_blank"
[href]="row.url">
{{ row?.url?.endsWith('/') ? row?.url?.slice(0, -1) : row.url }}
</ng-template>
<ng-template #durationTpl
- let-column="column"
- let-value="value"
- let-row="row">
+ let-column="data.column"
+ let-value="data.value"
+ let-row="data.row">
<span *ngIf="row.remainingTimeWithoutSeconds > 0 && row.cluster_alias !== 'local-cluster'">
<i *ngIf="row.remainingDays < 8"
i18n-title
</ng-template>
<ng-template #nametpl>
- <div class="datatable-body-cell-label">
+ <div cdstabledata>
<span title="{{cluster}}">
<a href="#">
{{cluster}}
} from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
-import { TableColumnProp } from '@swimlane/ngx-datatable';
import _ from 'lodash';
import { InventoryDevice } from '~/app/ceph/cluster/inventory/inventory-devices/inventory-device.model';
submitAction = new EventEmitter<CdTableColumnFiltersChange>();
icons = Icons;
- filterColumns: TableColumnProp[] = [];
+ filterColumns: (string | number)[] = [];
hostname: string;
deviceType: string;
(updateSelection)="updateSelection($event)"
[updateSelectionOnRefresh]="'never'">
- <div class="table-actions btn-toolbar">
+ <div class="table-actions">
<cd-table-actions [permission]="permissions.osd"
[selection]="selection"
class="btn-group"
</cd-table-actions>
</div>
- <cd-osd-details cdTableDetail
+ <cd-osd-details *cdTableDetail
[selection]="expandedRow">
</cd-osd-details>
</cd-table>
</ng-template>
<ng-template #flagsTpl
- let-row="row">
+ let-row="data.row">
<span *ngFor="let flag of row.cdClusterFlags;"
class="badge badge-hdd me-1">{{ flag }}</span>
<span *ngFor="let flag of row.cdIndivFlags;"
</ng-template>
<ng-template #osdUsageTpl
- let-row="row">
+ let-row="data.row">
<cd-usage-bar [title]="'osd ' + row.osd"
[total]="row.stats.stat_bytes"
[used]="row.stats.stat_bytes_used"
import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
-import { CdTableAction } from '~/app/shared/models/cd-table-action';
import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum';
import { Permissions } from '~/app/shared/models/permissions';
'Destroy',
'Delete'
],
- primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,update': {
actions: [
'Mark In',
'Mark Down'
],
- primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,delete': {
actions: ['Create', 'Mark Lost', 'Purge', 'Destroy', 'Delete'],
primary: {
multiple: 'Create',
- executing: 'Mark Lost',
- single: 'Mark Lost',
+ executing: 'Create',
+ single: 'Create',
no: 'Create'
}
},
create: {
actions: ['Create'],
- primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'update,delete': {
actions: [
'Destroy',
'Delete'
],
- primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
update: {
actions: [
'Mark In',
'Mark Down'
],
- primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
delete: {
actions: ['Mark Lost', 'Purge', 'Destroy', 'Delete'],
primary: {
- multiple: 'Mark Lost',
- executing: 'Mark Lost',
- single: 'Mark Lost',
- no: 'Mark Lost'
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
}
},
'no-permissions': {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
}
});
});
fixture.detectChanges();
});
- beforeEach(fakeAsync(() => {
- // The menu needs a click to render the dropdown!
- const dropDownToggle = fixture.debugElement.query(By.css('.dropdown-toggle'));
- dropDownToggle.triggerEventHandler('click', null);
- tick();
- fixture.detectChanges();
- }));
-
it('has all menu entries disabled except create', () => {
const tableActionElement = fixture.debugElement.query(By.directive(TableActionsComponent));
- const toClassName = TestBed.inject(TableActionsComponent).toClassName;
- const getActionClasses = (action: CdTableAction) =>
- tableActionElement.query(By.css(`[ngbDropdownItem].${toClassName(action)}`)).classes;
+ const tableActionComponent: TableActionsComponent = tableActionElement.componentInstance;
component.tableActions.forEach((action) => {
if (action.name === 'Create') {
return;
}
- expect(getActionClasses(action).disabled).toBe(true);
+ expect(tableActionComponent.disableSelectionAction(action)).toBeTruthy();
});
});
});
[tableActions]="tableActions">
</cd-table-actions>
- <cd-table-key-value cdTableDetail
- *ngIf="expandedRow"
- [renderObjects]="true"
- [hideEmpty]="true"
- [appendParentKey]="false"
- [data]="expandedRow"
- [customCss]="customCss"
- [autoReload]="false">
- </cd-table-key-value>
+ <ng-container *ngIf="expandedRow">
+ <cd-table-key-value *cdTableDetail
+ [renderObjects]="true"
+ [hideEmpty]="true"
+ [appendParentKey]="false"
+ [data]="expandedRow"
+ [customCss]="customCss"
+ [autoReload]="false">
+ </cd-table-key-value>
+ </ng-container>
</cd-table>
<ng-template #externalLinkTpl
- let-row="row"
- let-value="value">
+ let-row="data.row"
+ let-value="data.value">
<a [href]="value"
target="_blank"><i [ngClass]="[icons.lineChart]"></i> Source</a>
</ng-template>
[hasDetails]="true"
(setExpandedRow)="setExpandedRow($event)"
(updateSelection)="updateSelection($event)">
- <cd-table-key-value cdTableDetail
- *ngIf="expandedRow"
- [data]="expandedRow"
- [renderObjects]="true"
- [hideKeys]="hideKeys">
- </cd-table-key-value>
+ <ng-container *ngIf="expandedRow">
+ <cd-table-key-value *cdTableDetail
+ [data]="expandedRow"
+ [renderObjects]="true"
+ [hideKeys]="hideKeys">
+ </cd-table-key-value>
+ </ng-container>
</cd-table>
[selection]="selection"
[tableActions]="tableActions">
</cd-table-actions>
- <cd-table-key-value cdTableDetail
- *ngIf="expandedRow"
- [renderObjects]="true"
- [hideEmpty]="true"
- [appendParentKey]="false"
- [data]="expandedRow"
- [customCss]="customCss"
- [autoReload]="false">
- </cd-table-key-value>
+ <ng-container *ngIf="expandedRow">
+ <cd-table-key-value *cdTableDetail
+ [renderObjects]="true"
+ [hideEmpty]="true"
+ [appendParentKey]="false"
+ [data]="expandedRow"
+ [customCss]="customCss"
+ [autoReload]="false">
+ </cd-table-key-value>
+ </ng-container>
</cd-table>
expect(tableActions).toEqual({
'create,update,delete': {
actions: ['Create', 'Recreate', 'Edit', 'Expire'],
- primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,update': {
actions: ['Create', 'Recreate', 'Edit'],
- primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,delete': {
actions: ['Create', 'Recreate', 'Expire'],
- primary: { multiple: 'Create', executing: 'Expire', single: 'Expire', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
create: {
actions: ['Create', 'Recreate'],
- primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'update,delete': {
actions: ['Edit', 'Expire'],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
update: {
actions: ['Edit'],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: 'Edit',
+ executing: 'Edit',
+ single: 'Edit',
+ no: 'Edit'
+ }
},
delete: {
actions: ['Expire'],
- primary: { multiple: 'Expire', executing: 'Expire', single: 'Expire', no: 'Expire' }
+ primary: {
+ multiple: 'Expire',
+ executing: 'Expire',
+ single: 'Expire',
+ no: 'Expire'
+ }
},
'no-permissions': {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
}
});
});
import { Component, Inject } from '@angular/core';
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
-import { SortDirection, SortPropDir } from '@swimlane/ngx-datatable';
import { Observable, Subscriber } from 'rxjs';
import { PrometheusListHelper } from '~/app/shared/helpers/prometheus-list-helper';
import { NotificationService } from '~/app/shared/services/notification.service';
import { PrometheusSilenceMatcherService } from '~/app/shared/services/prometheus-silence-matcher.service';
import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+import { CdSortDirection } from '~/app/shared/enum/cd-sort-direction';
+import { CdSortPropDir } from '~/app/shared/models/cd-sort-prop-dir';
const BASE_URL = 'monitoring/silences';
'badge badge-warning': 'pending',
'badge badge-default': 'expired'
};
- sorts: SortPropDir[] = [{ prop: 'endsAt', dir: SortDirection.desc }];
+ sorts: CdSortPropDir[] = [{ prop: 'endsAt', dir: CdSortDirection.desc }];
rules: PrometheusRule[];
visited: boolean;
</ng-template>
<ng-template #statusTpl
- let-row="row">
+ let-row="data.row">
<span class="badge"
[ngClass]="row | pipeFunction:getStatusClass">
{{ row.status_desc }}
</ng-template>
<ng-template #cpuTpl
- let-row="row">
+ let-row="data.row">
<cd-usage-bar [total]="total"
[calculatePerc]="false"
[used]="row.cpu_percentage"
[selection]="selection"
[tableActions]="tableActions">
</cd-table-actions>
- <cd-service-details cdTableDetail
+ <cd-service-details *cdTableDetail
[permissions]="permissions"
[selection]="expandedRow">
</cd-service-details>
<ng-template #runningTpl
- let-value="value">
+ let-value="data.value">
<span ngbTooltip="Service instances running out of the total number of services requested.">
{{ value.running }} / {{ value.size }}
</span>
</ng-template>
<ng-template #urlTpl
- let-row="row">
+ let-row="data.row">
<ng-container *ngIf="serviceUrls[row.service_type] else noUrl">
<a *ngIf="!isMgmtGateway else mgmtGateway"
target="_blank"
[hasDetails]="true"
(setExpandedRow)="setExpandedRow($event)"
(updateSelection)="updateSelection($event)">
- <div class="table-actions btn-toolbar">
+ <div class="table-actions">
<cd-table-actions class="btn-group"
[permission]="permission"
[selection]="selection"
</cd-table-actions>
</div>
- <cd-nfs-details cdTableDetail
+ <cd-nfs-details *cdTableDetail
[selection]="expandedRow">
</cd-nfs-details>
</cd-table>
<ng-template #nfsFsal
- let-value="value">
+ let-value="data.value">
<ng-container *ngIf="value.name==='CEPH'"
i18n>CephFS</ng-container>
<ng-container *ngIf="value.name==='RGW'"
expect(tableActions).toEqual({
'create,update,delete': {
actions: ['Create', 'Edit', 'Delete'],
- primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,update': {
actions: ['Create', 'Edit'],
- primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,delete': {
actions: ['Create', 'Delete'],
- primary: { multiple: 'Create', executing: 'Delete', single: 'Delete', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
create: {
actions: ['Create'],
- primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'update,delete': {
actions: ['Edit', 'Delete'],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
update: {
actions: ['Edit'],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: 'Edit',
+ executing: 'Edit',
+ single: 'Edit',
+ no: 'Edit'
+ }
},
delete: {
actions: ['Delete'],
- primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ primary: {
+ multiple: 'Delete',
+ executing: 'Delete',
+ single: 'Delete',
+ no: 'Delete'
+ }
},
'no-permissions': {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
}
});
});
[autoSave]="false"
(fetchData)="getCounters($event)">
<ng-template #valueTpl
- let-row="row">
+ let-row="data.row">
{{ row.value | dimless }} {{ row.unit }}
</ng-template>
</cd-table>
-<ng-container *ngIf="selection"
- cdTableDetail>
+<ng-container *ngIf="selection">
<nav ngbNav
#nav="ngbNav"
class="nav-tabs"
[selection]="selection"
[tableActions]="tableActions">
</cd-table-actions>
- <cd-pool-details cdTableDetail
+ <cd-pool-details *cdTableDetail
id="pool-list-details"
[selection]="expandedRow"
[permissions]="permissions"
<div [ngbNavOutlet]="nav"></div>
<ng-template #poolUsageTpl
- let-row="row">
+ let-row="data.row">
<cd-usage-bar *ngIf="row.stats?.avail_raw?.latest"
[total]="row.stats.bytes_used.latest + row.stats.avail_raw.latest"
[used]="row.stats.bytes_used.latest"
[selection]="selection"
[tableActions]="tableActions">
</cd-table-actions>
- <cd-rgw-bucket-details cdTableDetail
+ <cd-rgw-bucket-details *cdTableDetail
[selection]="expandedRow">
</cd-rgw-bucket-details>
</cd-table>
<ng-template #bucketSizeTpl
- let-row="row">
+ let-row="data.row">
<cd-usage-bar *ngIf="row.bucket_quota.max_size > 0 && row.bucket_quota.enabled; else noSizeQuota"
[total]="row.bucket_quota.max_size"
[used]="row.bucket_size">
</ng-template>
<ng-template #bucketObjectTpl
- let-row="row">
+ let-row="data.row">
<cd-usage-bar *ngIf="row.bucket_quota.max_objects > 0 && row.bucket_quota.enabled; else noObjectQuota"
[total]="row.bucket_quota.max_objects"
[used]="row.num_objects"
expect(tableActions).toEqual({
'create,update,delete': {
actions: ['Create', 'Edit', 'Delete'],
- primary: { multiple: 'Delete', executing: 'Edit', single: 'Edit', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,update': {
actions: ['Create', 'Edit'],
- primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,delete': {
actions: ['Create', 'Delete'],
- primary: { multiple: 'Delete', executing: 'Create', single: 'Create', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
create: {
actions: ['Create'],
- primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'update,delete': {
actions: ['Edit', 'Delete'],
- primary: { multiple: 'Delete', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
update: {
actions: ['Edit'],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: 'Edit',
+ executing: 'Edit',
+ single: 'Edit',
+ no: 'Edit'
+ }
},
delete: {
actions: ['Delete'],
- primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ primary: {
+ multiple: 'Delete',
+ executing: 'Delete',
+ single: 'Delete',
+ no: 'Delete'
+ }
},
'no-permissions': {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
}
});
});
[selection]="selection"
[tableActions]="tableActions">
</cd-table-actions>
- <cd-rgw-config-details cdTableDetail
+ <cd-rgw-config-details *cdTableDetail
[selection]="expandedRow"
[excludeProps]="excludeProps">
</cd-rgw-config-details>
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { SharedModule } from '~/app/shared/shared.module';
import { RgwModule } from '../rgw.module';
+import { RouterTestingModule } from '@angular/router/testing';
describe('RgwConfigurationPageComponent', () => {
let component: RgwConfigurationPageComponent;
await TestBed.configureTestingModule({
declarations: [RgwConfigurationPageComponent],
providers: [NgbActiveModal],
- imports: [HttpClientTestingModule, SharedModule, NgbNavModule, RgwModule]
+ imports: [HttpClientTestingModule, SharedModule, NgbNavModule, RgwModule, RouterTestingModule]
}).compileComponents();
fixture = TestBed.createComponent(RgwConfigurationPageComponent);
[hasDetails]="true"
(setExpandedRow)="setExpandedRow($event)"
(fetchData)="getDaemonList($event)">
- <cd-rgw-daemon-details cdTableDetail
+ <cd-rgw-daemon-details *cdTableDetail
[selection]="expandedRow">
</cd-rgw-daemon-details>
</cd-table>
import { configureTestBed, TabHelper } from '~/testing/unit-test-helper';
import { RgwDaemonDetailsComponent } from '../rgw-daemon-details/rgw-daemon-details.component';
import { RgwDaemonListComponent } from './rgw-daemon-list.component';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
describe('RgwDaemonListComponent', () => {
let component: RgwDaemonListComponent;
};
configureTestBed({
- declarations: [RgwDaemonListComponent, RgwDaemonDetailsComponent],
+ declarations: [RgwDaemonListComponent, RgwDaemonDetailsComponent, TableComponent],
imports: [
BrowserAnimationsModule,
HttpClientTestingModule,
tick();
expect(listDaemonsSpy).toHaveBeenCalledTimes(1);
expect(component.daemons).toEqual([daemon]);
+ const cdTableEl = fixture.debugElement.query(By.directive(TableComponent));
+ const cdTableComponent: TableComponent = cdTableEl.componentInstance;
+ cdTableComponent.ngAfterViewInit();
+ fixture.detectChanges();
expect(fixture.debugElement.query(By.css('cd-table')).nativeElement.textContent).toContain(
- 'total of 1'
+ '1-1 of 1 item'
);
fixture.destroy();
</cd-table-actions>
</div>
<cd-rgw-multisite-sync-policy-details
- cdTableDetail
+ *cdTableDetail
[expandedRow]="expandedRow"
[permission]="permission">
</cd-rgw-multisite-sync-policy-details>
[selection]="selection"
[tableActions]="tableActions">
</cd-table-actions>
- <cd-rgw-user-details cdTableDetail
+ <cd-rgw-user-details *cdTableDetail
[selection]="expandedRow">
</cd-rgw-user-details>
</cd-table>
<ng-template #userSizeTpl
- let-row="row">
+ let-row="data.row">
<cd-usage-bar *ngIf="row.user_quota.max_size > 0 && row.user_quota.enabled; else noSizeQuota"
[total]="row.user_quota.max_size"
[used]="row.stats.size_actual">
</ng-template>
<ng-template #userObjectTpl
- let-row="row">
+ let-row="data.row">
<cd-usage-bar *ngIf="row.user_quota.max_objects > 0 && row.user_quota.enabled; else noObjectQuota"
[total]="row.user_quota.max_objects"
[used]="row.stats.num_objects"
expect(tableActions).toEqual({
'create,update,delete': {
actions: ['Create', 'Edit', 'Delete'],
- primary: { multiple: 'Delete', executing: 'Edit', single: 'Edit', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,update': {
actions: ['Create', 'Edit'],
- primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,delete': {
actions: ['Create', 'Delete'],
- primary: { multiple: 'Delete', executing: 'Create', single: 'Create', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
create: {
actions: ['Create'],
- primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'update,delete': {
actions: ['Edit', 'Delete'],
- primary: { multiple: 'Delete', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
update: {
actions: ['Edit'],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: 'Edit',
+ executing: 'Edit',
+ single: 'Edit',
+ no: 'Edit'
+ }
},
delete: {
actions: ['Delete'],
- primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ primary: {
+ multiple: 'Delete',
+ executing: 'Delete',
+ single: 'Delete',
+ no: 'Delete'
+ }
},
'no-permissions': {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
}
});
});
i18n>Neither hostname nor OSD ID given</cd-alert-panel>
<ng-template #deviceLocation
- let-value="value">
+ let-value="data.value">
<ng-container *ngFor="let location of value">
<cd-label *ngIf="location.host === hostname"
[value]="location.dev"></cd-label>
</ng-template>
<ng-template #daemonName
- let-value="value">
+ let-value="data.value">
<ng-container [ngTemplateOutlet]="osdId !== null ? osdIdDaemon : readableDaemons"
[ngTemplateOutletContext]="{daemons: value}">
</ng-container>
<ng-template #lifeExpectancy
- let-value="value">
+ let-value="data.value">
<span *ngIf="!value.life_expectancy_enabled"
i18n>{{ "" | notAvailable }}</span>
<span *ngIf="value.min && !value.max">> {{value.min | i18nPlural: translationMapping}}</span>
</ng-template>
<ng-template #lifeExpectancyTimestamp
- let-value="value">
+ let-value="data.value">
{{value}}
</ng-template>
[selection]="selection"
[tableActions]="tableActions">
</cd-table-actions>
- <cd-role-details cdTableDetail
+ <cd-role-details *cdTableDetail
[selection]="expandedRow"
[scopes]="scopes">
</cd-role-details>
expect(tableActions).toEqual({
'create,update,delete': {
actions: ['Create', 'Clone', 'Edit', 'Delete'],
- primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,update': {
actions: ['Create', 'Clone', 'Edit'],
- primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,delete': {
actions: ['Create', 'Clone', 'Delete'],
- primary: { multiple: 'Create', executing: 'Delete', single: 'Delete', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
create: {
actions: ['Create', 'Clone'],
- primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'update,delete': {
actions: ['Edit', 'Delete'],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
update: {
actions: ['Edit'],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: 'Edit',
+ executing: 'Edit',
+ single: 'Edit',
+ no: 'Edit'
+ }
},
delete: {
actions: ['Delete'],
- primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ primary: {
+ multiple: 'Delete',
+ executing: 'Delete',
+ single: 'Delete',
+ no: 'Delete'
+ }
},
'no-permissions': {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
}
});
});
</cd-table>
<ng-template #userRolesTpl
- let-value="value">
+ let-value="data.value">
<span *ngFor="let role of value; last as isLast">
{{ role }}{{ !isLast ? ", " : "" }}
</span>
</ng-template>
<ng-template #warningTpl
- let-column="column"
- let-value="value"
- let-row="row">
+ let-column="data.column"
+ let-value="data.value"
+ let-row="data.row">
<div [class.border-danger]="row.remainingDays < this.expirationDangerAlert"
[class.border-warning]="row.remainingDays < this.expirationWarningAlert && row.remainingDays >= this.expirationDangerAlert"
class="border-margin">
</ng-template>
<ng-template #durationTpl
- let-column="column"
- let-value="value"
- let-row="row">
+ let-column="data.column"
+ let-value="data.value"
+ let-row="data.row">
<i *ngIf="row.remainingDays < this.expirationWarningAlert"
i18n-title
title="User's password is about to expire"
expect(tableActions).toEqual({
'create,update,delete': {
actions: ['Create', 'Edit', 'Delete'],
- primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,update': {
actions: ['Create', 'Edit'],
- primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'create,delete': {
actions: ['Create', 'Delete'],
- primary: { multiple: 'Create', executing: 'Delete', single: 'Delete', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
create: {
actions: ['Create'],
- primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ primary: {
+ multiple: 'Create',
+ executing: 'Create',
+ single: 'Create',
+ no: 'Create'
+ }
},
'update,delete': {
actions: ['Edit', 'Delete'],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
update: {
actions: ['Edit'],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: 'Edit',
+ executing: 'Edit',
+ single: 'Edit',
+ no: 'Edit'
+ }
},
delete: {
actions: ['Delete'],
- primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ primary: {
+ multiple: 'Delete',
+ executing: 'Delete',
+ single: 'Delete',
+ no: 'Delete'
+ }
},
'no-permissions': {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
}
});
});
<cds-overflow-menu [customTrigger]="customTrigger"
- [offset]="{y:0, x:-80}">
+ [flip]="true">
<li class="cds--overflow-menu-options__option mb-2">
<button *ngIf="userPermission.read"
routerLink="/user-management"
<cds-overflow-menu [customTrigger]="customTrigger"
- [offset]="{y:0, x:-80}">
+ [flip]="true">
<li>
<a [ngClass]="{'cds--overflow-menu-options__btn': true, 'disabled': !docsUrl}"
href="{{ docsUrl }}"
<cds-overflow-menu [customTrigger]="customTrigger"
- [offset]="{y:0, x:-80}">
+ [flip]="true">
<li disabled="true"
class="show cds--overflow-menu-options__option cds--overflow-menu-options__option--disabled my-2"
i18n>
</nav>
<!-- Page Content -->
<div id="content"
- [ngClass]="{'active': !showMenuSidebar}">
+ [ngClass]="{'active': !showMenuSidebar, 'content-theme': true}">
<ng-content></ng-content>
</div>
</div>
<!-- ************************ -->
<ng-template #cd_menu>
<ng-container *ngIf="enabledFeature$ | async as enabledFeature">
- <div cdsTheme="theme">
<cds-sidenav [expanded]="showMenuSidebar"
class="mt-5">
<!-- Dashboard -->
class="tc_submenuitem tc_submenuitem_admin_configuration"><span i18n>Configuration</span></cds-sidenav-item>
</cds-sidenav-menu>
</cds-sidenav>
- </div>
</ng-container>
</ng-template>
</div>
SIDEBAR STYLE
--------------------------------------------------- */
-$sidebar-width: 20.8rem;
+$sidebar-width: 16rem;
.wrapper {
display: flex;
[autoReload]="false"
[autoSave]="false"
[footer]="false"
+ size="xs"
+ [layer]="0"
[limit]="0">
</cd-table>
<ng-template #cellScopeCheckboxTpl
- let-column="column"
- let-row="row"
- let-value="value">
+ let-column="data.column"
+ let-row="data.row"
+ let-value="data.value">
<div class="custom-control custom-checkbox">
<input class="custom-control-input"
id="scope_{{ row.scope }}"
</ng-template>
<ng-template #cellPermissionCheckboxTpl
- let-column="column"
- let-row="row"
- let-value="value">
+ let-column="data.column"
+ let-row="data.row"
+ let-value="data.value">
<div class="custom-control custom-checkbox">
<input class="custom-control-input"
type="checkbox"
</ng-template>
<ng-template #headerPermissionCheckboxTpl
- let-column="column">
+ let-column="data">
<div class="custom-control custom-checkbox">
<input class="custom-control-input"
id="header_{{ column.prop }}"
import { TableComponent } from '../table/table.component';
import { TableKeyValueComponent } from '../table-key-value/table-key-value.component';
import { TablePaginationComponent } from '../table-pagination/table-pagination.component';
-import { NgxDatatableModule } from '@swimlane/ngx-datatable';
import { FormHelper, configureTestBed } from '~/testing/unit-test-helper';
import { CdFormGroup } from '../../forms/cd-form-group';
import { FormControl } from '@angular/forms';
TableKeyValueComponent,
TablePaginationComponent
],
- imports: [NgxDatatableModule]
+ imports: []
});
beforeEach(() => {
column.cellTemplate = this.cellPermissionCheckboxTpl;
column.headerTemplate = this.headerPermissionCheckboxTpl;
}
+ column.sortable = false;
});
this.listenToChanges();
this.form.get(this.inputField).setValue(this.initialValue);
[selectionType]="meta.table.selectionType"
(updateSelection)="updateSelection($event)"
[toolHeader]="meta.table.toolHeader">
- <div class="table-actions btn-toolbar">
+ <div class="table-actions">
<cd-table-actions [permission]="permission"
[selection]="selection"
class="btn-group"
[tableActions]="meta.actions">
</cd-table-actions>
</div>
- <ng-container *ngIf="expandedRow && meta.detail_columns.length > 0"
- cdTableDetail>
+ <ng-container [ngIf]="expandedRow && meta.detail_columns.length > 0"
+ *cdTableDetail>
<table class="table table-striped table-bordered">
<tbody>
<tr *ngFor="let column of meta.detail_columns">
</ng-container>
<ng-template #badgeDictTpl
- let-value="value">
+ let-value="data.value">
<span *ngFor="let instance of value | keyvalue; last as isLast">
<span class="badge badge-background-primary" >{{ instance.key }}: {{ instance.value }}</span>
<ng-container *ngIf="!isLast"> </ng-container>
</ng-template>
<ng-template #dateTpl
- let-value="value">
+ let-value="data.value">
<span>{{ value | cdDate }}</span>
</ng-template>
<ng-template #durationTpl
- let-value="value">
+ let-value="data.value">
<span>{{ value | duration }}</span>
</ng-template>
import { RouterTestingModule } from '@angular/router/testing';
import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
-import { NgxDatatableModule } from '@swimlane/ngx-datatable';
import { NgxPipeFunctionModule } from 'ngx-pipe-function';
import { ToastrModule } from 'ngx-toastr';
TablePaginationComponent
],
imports: [
- NgxDatatableModule,
FormsModule,
ComponentsModule,
NgbDropdownModule,
import { RouterModule } from '@angular/router';
import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
-import { NgxDatatableModule } from '@swimlane/ngx-datatable';
import { NgxPipeFunctionModule } from 'ngx-pipe-function';
+import {
+ TableModule,
+ ButtonModule,
+ IconModule,
+ IconService,
+ CheckboxModule,
+ PaginationModule,
+ ThemeModule,
+ DialogModule,
+ SelectModule,
+ TagModule,
+ LayerModule
+} from 'carbon-components-angular';
+import AddIcon from '@carbon/icons/es/add/16';
+import FilterIcon from '@carbon/icons/es/filter/16';
+import ReloadIcon from '@carbon/icons/es/renew/16';
+import DataTableIcon from '@carbon/icons/es/data-table/16';
+import CheckIcon from '@carbon/icons/es/checkmark/16';
+import CloseIcon from '@carbon/icons/es/close/16';
+import MaximizeIcon from '@carbon/icons/es/maximize/16';
+import ArrowDown from '@carbon/icons/es/caret--down/16';
+
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { FormlyModule } from '@ngx-formly/core';
import { FormlyBootstrapModule } from '@ngx-formly/bootstrap';
import { FormlyFileTypeComponent } from '../forms/crud-form/formly-file-type/formly-file-type.component';
import { FormlyFileValueAccessorDirective } from '../forms/crud-form/formly-file-type/formly-file-type-accessor';
import { CheckedTableFormComponent } from './checked-table-form/checked-table-form.component';
+import { TableDetailDirective } from './directives/table-detail.directive';
@NgModule({
imports: [
CommonModule,
- NgxDatatableModule,
NgxPipeFunctionModule,
FormsModule,
NgbDropdownModule,
],
wrappers: [{ name: 'input-wrapper', component: FormlyInputWrapperComponent }]
}),
- FormlyBootstrapModule
+ FormlyBootstrapModule,
+ TableModule,
+ ButtonModule,
+ IconModule,
+ CheckboxModule,
+ PaginationModule,
+ DialogModule,
+ ThemeModule,
+ SelectModule,
+ TagModule,
+ LayerModule
],
declarations: [
TableComponent,
FormlyInputWrapperComponent,
FormlyFileTypeComponent,
FormlyFileValueAccessorDirective,
- CheckedTableFormComponent
+ CheckedTableFormComponent,
+ TableDetailDirective
],
exports: [
TableComponent,
- NgxDatatableModule,
TableKeyValueComponent,
TableActionsComponent,
CRUDTableComponent,
TablePaginationComponent,
- CheckedTableFormComponent
+ CheckedTableFormComponent,
+ TableDetailDirective
]
})
-export class DataTableModule {}
+export class DataTableModule {
+ constructor(private iconService: IconService) {
+ this.iconService.registerAll([
+ AddIcon,
+ FilterIcon,
+ ReloadIcon,
+ DataTableIcon,
+ CheckIcon,
+ CloseIcon,
+ MaximizeIcon,
+ ArrowDown
+ ]);
+ }
+}
--- /dev/null
+import { TableDetailDirective } from './table-detail.directive';
+
+describe('TableDetailDirective', () => {
+ it('should create an instance', () => {
+ const directive = new TableDetailDirective(null);
+ expect(directive).toBeTruthy();
+ });
+});
--- /dev/null
+import { Directive, TemplateRef } from '@angular/core';
+
+@Directive({
+ selector: '[cdTableDetail]'
+})
+export class TableDetailDirective {
+ constructor(public template?: TemplateRef<any>) {}
+}
-<div class="btn-group">
- <ng-container *ngIf="currentAction">
- <button type="button"
- title="{{ useDisableDesc(currentAction) }}"
- class="btn btn-{{btnColor}}"
- [ngClass]="{'disabled': disableSelectionAction(currentAction)}"
- (click)="useClickAction(currentAction)"
- [disabled]="disableSelectionAction(currentAction)"
- [routerLink]="useRouterLink(currentAction)"
- [attr.aria-label]="currentAction.name"
- [preserveFragment]="currentAction.preserveFragment ? '' : null">
- <i [ngClass]="[currentAction.icon]"></i>
- <span class="action-label">{{ currentAction.name }}</span>
- </button>
- </ng-container>
- <div class="btn-group"
- ngbDropdown
- role="group"
- *ngIf="dropDownActions.length > 1"
- aria-label="Button group with nested dropdown">
- <button aria-label="dropdown-menu-toggle"
- class="btn btn-{{btnColor}} dropdown-toggle"
- ngbDropdownToggle>
- <ng-container *ngIf="dropDownOnly">{{ dropDownOnly }} </ng-container>
- <span *ngIf="!dropDownOnly"
- class="sr-only"></span>
- </button>
- <div class="dropdown-menu"
- ngbDropdownMenu>
- <ng-container *ngFor="let action of dropDownActions">
- <button ngbDropdownItem
- class="{{ toClassName(action) }}"
- title="{{ useDisableDesc(action) }}"
- (click)="useClickAction(action)"
- [routerLink]="useRouterLink(action)"
- [preserveFragment]="action.preserveFragment ? '' : null"
- [disabled]="disableSelectionAction(action)"
- [attr.aria-label]="action.name">
- <i [ngClass]="[action.icon, 'action-icon']"></i>
- <span>{{ action.name }}</span>
- </button>
- </ng-container>
- </div>
- </div>
-</div>
+<ng-container *ngIf="!dropDownOnly; else dropDownOnlyTpl">
+ <button *ngIf="currentAction"
+ type="button"
+ [cdsButton]="currentAction.buttonKind"
+ title="{{ useDisableDesc(currentAction) }}"
+ (click)="useClickAction(currentAction)"
+ [disabled]="disableSelectionAction(currentAction)"
+ [routerLink]="useRouterLink(currentAction)"
+ [attr.aria-label]="currentAction.name"
+ [preserveFragment]="currentAction.preserveFragment ? '' : null"
+ data-testid="primary-action">
+ <span i18n>{{ currentAction.name }}</span>
+ <svg class="cds--btn__icon"
+ cdsIcon="add"
+ size="16"></svg>
+ </button>
+</ng-container>
+
+
+<ng-template #dropDownOnlyTpl>
+ <cds-overflow-menu [customTrigger]="customTrigger"
+ [flip]="true"
+ [offset]="{ x: 105, y: 0 }"
+ data-testid="table-action-btn"
+ class="d-flex justify-content-end">
+ <ng-container *ngFor="let action of dropDownActions">
+ <cds-overflow-menu-option *ngIf="currentAction !== action"
+ class="{{ toClassName(action) }}"
+ title="{{ useDisableDesc(action) }}"
+ (click)="useClickAction(action)"
+ [routerLink]="useRouterLink(action)"
+ [preserveFragment]="action.preserveFragment ? '' : null"
+ [disabled]="disableSelectionAction(action)"
+ [attr.aria-label]="action.name"
+ data-testid="table-action-option-btn"
+ i18n>
+ {{ action.name }}
+ </cds-overflow-menu-option>
+ </ng-container>
+ </cds-overflow-menu>
+</ng-template>
+
+<ng-template #customTrigger>
+ <button cdsButton="tertiary">
+ <span i18n>{{ dropDownOnly }}</span>
+ <svg class="cds--btn__icon"
+ cdsIcon="caret--down"
+ size="16"></svg>
+ </button>
+</ng-template>
.action-label {
font-weight: bold;
}
+
+::ng-deep .cds--toolbar-content .cds--overflow-menu {
+ inline-size: auto !important;
+}
expect(tableActions).toEqual({
'create,update,delete': {
actions: ['Add', 'Edit', 'Protect', 'Unprotect', 'Copy', 'Delete'],
- primary: { multiple: 'Delete', executing: 'Edit', single: 'Edit', no: 'Add' }
+ primary: {
+ multiple: 'Add',
+ executing: 'Add',
+ single: 'Add',
+ no: 'Add'
+ }
},
'create,update': {
actions: ['Add', 'Edit', 'Protect', 'Unprotect', 'Copy'],
- primary: { multiple: 'Add', executing: 'Edit', single: 'Edit', no: 'Add' }
+ primary: {
+ multiple: 'Add',
+ executing: 'Add',
+ single: 'Add',
+ no: 'Add'
+ }
},
'create,delete': {
actions: ['Add', 'Copy', 'Delete'],
- primary: { multiple: 'Delete', executing: 'Copy', single: 'Copy', no: 'Add' }
+ primary: {
+ multiple: 'Add',
+ executing: 'Add',
+ single: 'Add',
+ no: 'Add'
+ }
},
create: {
actions: ['Add', 'Copy'],
- primary: { multiple: 'Add', executing: 'Copy', single: 'Copy', no: 'Add' }
+ primary: {
+ multiple: 'Add',
+ executing: 'Add',
+ single: 'Add',
+ no: 'Add'
+ }
},
'update,delete': {
actions: ['Edit', 'Protect', 'Unprotect', 'Delete'],
- primary: { multiple: 'Delete', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
update: {
actions: ['Edit', 'Protect', 'Unprotect'],
- primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
},
delete: {
actions: ['Delete'],
- primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ primary: {
+ multiple: 'Delete',
+ executing: 'Delete',
+ single: 'Delete',
+ no: 'Delete'
+ }
},
'no-permissions': {
actions: [],
- primary: { multiple: '', executing: '', single: '', no: '' }
+ primary: {
+ multiple: '',
+ executing: '',
+ single: '',
+ no: ''
+ }
}
});
});
this.currentAction = undefined;
return;
}
- let buttonAction = this.dropDownActions.find((tableAction) => this.showableAction(tableAction));
- if (!buttonAction && this.dropDownActions.length > 0) {
- buttonAction = this.dropDownActions[0];
+ /**
+ * current action will always be the first action if that has a create permission
+ * otherwise if there's only a single actions that will be the current action
+ */
+ if (this.dropDownActions?.[0]?.permission === 'create') {
+ this.currentAction = this.dropDownActions[0];
+ } else if (this.dropDownActions.length === 1) {
+ this.currentAction = this.dropDownActions[0];
}
- this.currentAction = buttonAction;
- }
-
- /**
- * Determines if action can be used for the button
- *
- * @param {CdTableAction} action
- * @returns {boolean}
- */
- private showableAction(action: CdTableAction): boolean {
- const condition = action.canBePrimary;
- const singleSelection = this.selection.hasSingleSelection;
- const defaultCase = action.permission === 'create' ? !singleSelection : singleSelection;
- return (condition && condition(this.selection)) || (!condition && defaultCase);
}
useRouterLink(action: CdTableAction): string {
import { RouterTestingModule } from '@angular/router/testing';
import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
-import { NgxDatatableModule } from '@swimlane/ngx-datatable';
import { NgxPipeFunctionModule } from 'ngx-pipe-function';
import { ComponentsModule } from '~/app/shared/components/components.module';
declarations: [TableComponent, TableKeyValueComponent, TablePaginationComponent],
imports: [
FormsModule,
- NgxDatatableModule,
ComponentsModule,
RouterTestingModule,
NgbDropdownModule,
-<div class="dataTables_wrapper">
-
- <div *ngIf="onlyActionHeader"
- class="dataTables_header clearfix">
- <div class="cd-datatable-actions">
- <ng-content select=".only-table-actions"></ng-content>
- </div>
- </div>
- <div class="dataTables_header clearfix"
- *ngIf="toolHeader">
- <!-- actions -->
- <div class="cd-datatable-actions">
- <ng-content select=".table-actions"></ng-content>
- </div>
- <!-- end actions -->
-
- <!-- column filters -->
- <div *ngIf="columnFilters.length !== 0"
- class="btn-group widget-toolbar">
- <div ngbDropdown
- placement="bottom-right"
- class="tc_filter_name">
- <button ngbDropdownToggle
- class="btn btn-light"
- title="Filter">
- <i [ngClass]="[icons.large, icons.filter]"></i>
- {{ selectedFilter.column.name }}
+<cds-table-container [cdsLayer]="layer"
+ [cdsTheme]="theme">
+ <cds-table-toolbar #toolbar
+ *ngIf="toolHeader"
+ (cancel)="onBatchActionsCancel()"
+ [model]="selectionType === 'multiClick' ? model : []"
+ [batchText]="{ SINGLE: '1 item selected', MULTIPLE: '{{count}} items selected' }">
+ <!-- batch actions -->
+ <cds-table-toolbar-actions *ngIf="tableActions?.dropDownActions.length > 0">
+ <ng-container *ngFor="let action of tableActions?.dropDownActions">
+ <button *ngIf="tableActions.currentAction !== action"
+ cdsButton="primary"
+ [tabindex]="toolbar.selected ? 0 : -1"
+ class="{{ tableActions.toClassName(action) }}"
+ title="{{ tableActions.useDisableDesc(action) }}"
+ (click)="tableActions.useClickAction(action)"
+ [routerLink]="tableActions.useRouterLink(action)"
+ [preserveFragment]="action.preserveFragment ? '' : null"
+ [disabled]="tableActions.disableSelectionAction(action)"
+ [attr.aria-label]="action.name"
+ i18n>
+ {{ action.name }}
</button>
- <div ngbDropdownMenu>
+ </ng-container>
+ </cds-table-toolbar-actions>
+ <!-- end batch actions -->
+ <cds-table-toolbar-content>
+ <!-- search -->
+ <cds-table-toolbar-search *ngIf="searchField"
+ [expandable]="false"
+ [(ngModel)]="search"
+ aria-label="search"
+ (valueChange)="updateFilter($event)"
+ (clear)="onClearSearch()">
+ </cds-table-toolbar-search>
+ <!-- end search -->
+ <!-- column filters -->
+ <ng-container *ngIf="columnFilters.length !== 0">
+ <div class="d-inline-flex position-relative">
+ <svg cdsIcon="filter"
+ size="16"
+ class="align-self-center mb-2"></svg>
+ </div>
+ <cds-select (valueChange)="onSelectFilter($event)"
+ display="inline"
+ id="filter_name">
<ng-container *ngFor="let filter of columnFilters">
- <button ngbDropdownItem
- (click)="onSelectFilter(filter); false">{{ filter.column.name }}</button>
+ <option [value]="filter.column.name"
+ [selected]="filter.column.name === selectedFilter.column.name">{{ filter.column.name }}</option>
</ng-container>
- </div>
- </div>
-
- <div ngbDropdown
- placement="bottom-right"
- class="tc_filter_option">
- <button ngbDropdownToggle
- class="btn btn-light"
- [class.disabled]="selectedFilter.options.length === 0">
- {{ selectedFilter.value ? selectedFilter.value.formatted: 'Any' }}
- </button>
- <div ngbDropdownMenu>
+ </cds-select>
+ <cds-select (valueChange)="onChangeFilter($event)"
+ display="inline"
+ id="filter_option">
+ <option *ngIf="!selectedFilter.value"
+ i18n>Any</option>
<ng-container *ngFor="let option of selectedFilter.options">
- <button ngbDropdownItem
- (click)="onChangeFilter(selectedFilter, option); false">
- {{ option.formatted }}
- <i *ngIf="selectedFilter.value !== undefined && (selectedFilter.value.raw === option.raw)"
- [ngClass]="[icons.check]"></i>
- </button>
+ <option [value]="option.raw"
+ [selected]="option.raw === selectedFilter?.value?.raw">{{ option.formatted }}</option>
</ng-container>
- </div>
- </div>
- </div>
- <!-- end column filters -->
-
- <!-- search -->
- <div class="input-group search"
- *ngIf="searchField">
- <span class="input-group-text">
- <i [ngClass]="[icons.search]"></i>
- </span>
- <input aria-label="search"
- class="form-control"
- type="text"
- [(ngModel)]="search"
- (keyup)="updateFilter()">
- <button type="button"
- class="btn btn-light"
- title="Clear"
- (click)="onClearSearch()">
- <i class="icon-prepend {{ icons.destroy }}"></i>
+ </cds-select>
+ </ng-container>
+ <!-- end column filters -->
+ <!-- refresh button -->
+ <button cdsButton="ghost"
+ [disabled]="!fetchData?.observers?.length"
+ (click)="refreshBtn()"
+ title="Refresh"
+ [description]="status.msg"
+ i18n-title
+ i18n-description
+ class="toolbar-action"
+ placement="bottom">
+ <svg cdsIcon="renew"
+ size="16"
+ [ngClass]="{ 'cds--toolbar-action__icon': true, 'reload': loadingIndicator }"></svg>
</button>
- </div>
- <!-- end search -->
-
- <!-- pagination limit -->
- <div class="input-group dataTables_paginate"
- *ngIf="limit">
- <input aria-label="table pagination"
- class="form-control"
- type="number"
- min="1"
- max="9999"
- [value]="userConfig.limit"
- (click)="setLimit($event)"
- (keyup)="setLimit($event)"
- (blur)="setLimit($event)">
- </div>
- <!-- end pagination limit-->
-
- <!-- show hide columns -->
- <div class="widget-toolbar">
- <div ngbDropdown
- autoClose="outside"
- class="tc_menuitem">
- <button ngbDropdownToggle
- class="btn btn-light tc_columnBtn"
- title="toggle columns">
- <i [ngClass]="[icons.large, icons.table]"></i>
- </button>
- <div ngbDropdownMenu>
+ <!-- end refresh button -->
+ <!-- show hide columns -->
+ <button cdsButton="ghost"
+ class="toolbar-action"
+ [cdsOverflowMenu]="showHideColumnsRef"
+ placement="bottom"
+ [flip]="true"
+ [offset]="{ x: 3, y: 0 }">
+ <svg cdsIcon="data-table"
+ size="16"
+ class="cds--toolbar-action__icon"></svg>
+ </button>
+ <ng-template #showHideColumnsRef>
+ <div class="vstack gap-3 p-3"
+ (click)="$event.stopPropagation()"
+ [cdsTheme]="theme">
<ng-container *ngFor="let column of columns">
- <ng-container *ngIf="!column?.isInvisible">
- <button ngbDropdownItem
- *ngIf="column.name !== ''"
- (click)="toggleColumn(column); false;">
- <div class="custom-control custom-checkbox py-0">
- <input class="custom-control-input"
- type="checkbox"
- [name]="column.prop"
- id="{{ column.prop }}{{ tableName }}"
- [checked]="!column.isHidden">
- <label class="custom-control-label"
- for="{{ column.prop }}{{ tableName }}">{{ column.name }}</label>
- </div>
- </button>
- </ng-container>
+ <cds-checkbox *ngIf="!column?.isInvisible"
+ id="{{ column.prop }}{{ tableName }}"
+ name="{{ column.prop }}{{ tableName }}"
+ [checked]="!column?.isHidden"
+ (checkedChange)="toggleColumn(column);">{{ column.name }}
+ </cds-checkbox>
</ng-container>
</div>
- </div>
- </div>
- <!-- end show hide columns -->
-
- <!-- refresh button -->
- <div class="widget-toolbar tc_refreshBtn"
- *ngIf="fetchData.observers.length > 0">
-
- <button type="button"
- [class]="'btn btn-' + status.type"
- [ngbTooltip]="status.msg"
- (click)="refreshBtn()"
- title="Refresh">
- <i [ngClass]="[icons.large, icons.refresh]"
- [class.fa-spin]="updating || loadingIndicator"></i>
- </button>
- </div>
- <!-- end refresh button -->
- </div>
- <div class="dataTables_header clearfix"
+ </ng-template>
+ <!-- end show hide columns -->
+ <!-- actions -->
+ <ng-content select=".table-actions"></ng-content>
+ <!-- end actions -->
+ </cds-table-toolbar-content>
+ </cds-table-toolbar>
+ <!-- filter chips for column filters -->
+ <div class="d-flex justify-content-end align-items-center filter-tags"
*ngIf="toolHeader && columnFiltered">
- <!-- filter chips for column filters -->
- <div class="filter-chips">
- <span *ngFor="let filter of columnFilters">
- <span *ngIf="filter.value"
- class="badge badge-info me-2">
- <span class="me-2">{{ filter.column.name }}: {{ filter.value.formatted }}</span>
- <a class="badge-remove"
- (click)="onChangeFilter(filter); false">
- <i [ngClass]="[icons.destroy]"
- aria-hidden="true"></i>
- </a>
- </span>
- </span>
- <a class="tc_clearSelections"
- href=""
- (click)="onClearFilters(); false">
- <ng-container i18n>Clear filters</ng-container>
- </a>
- </div>
- <!-- end filter chips for column filters -->
+ <div class="d-flex gap-2">
+ <ng-container *ngFor="let filter of columnFilters">
+ <cds-tag *ngIf="filter.value"
+ type="outline"
+ class="align-self-center">
+ <span class="me-2">{{ filter.column.name }}: {{ filter.value.formatted }}</span>
+ <button class="cds--tag__close-icon"
+ (click)="onChangeFilter(filter)">
+ <svg cdsIcon="close"
+ size="16"></svg>
+ </button>
+ </cds-tag>
+ </ng-container>
+ <button cdsButton="ghost"
+ (click)="onClearFilters($event)">
+ <ng-container i18n>Clear filters</ng-container>
+ </button>
</div>
- <ngx-datatable #table
- class="bootstrap cd-datatable"
- [cssClasses]="paginationClasses"
- [selectionType]="selectionType"
- [selected]="selection.selected"
- (select)="onSelect($event)"
- [sorts]="userConfig.sorts"
- (sort)="changeSorting($event)"
- [columns]="tableColumns"
- [columnMode]="columnMode"
- [rows]="rows"
- [rowClass]="getRowClass()"
- [headerHeight]="header ? 'auto' : 0"
- [footerHeight]="footer ? 'auto' : 0"
- [count]="count"
- [externalPaging]="serverSide"
- [externalSorting]="serverSide"
- [limit]="userConfig.limit > 0 ? userConfig.limit : undefined"
- [offset]="userConfig.offset >= 0 ? userConfig.offset : 0"
- (page)="changePage($event)"
- [loadingIndicator]="loadingIndicator"
- [rowIdentity]="rowIdentity()"
- [rowHeight]="'auto'">
-
- <!-- Row Selection Template-->
- <ng-template #rowSelectionTpl
- let-value="value"
- let-isSelected="isSelected"
- ngx-datatable-cell-template>
- <input type="checkbox"
- [attr.aria-label]="isSelected ? 'selected' : 'select'"
- [checked]="isSelected"
- class="cd-datatable-checkbox" />
- </ng-template>
+ </div>
+ <!-- end filter chips for column filters -->
+ <cds-table [model]="model"
+ [sortable]="!!userConfig.sorts"
+ [size]="size"
+ class="overflow-y-hidden"
+ [skeleton]="false"
+ [showSelectionColumn]="selectionType === 'multiClick'"
+ [enableSingleSelect]="selectionType === 'single'"
+ [stickyHeader]="false"
+ [striped]="false"
+ [isDataGrid]="false"
+ (sort)="changeSorting($event)"
+ (selectRow)="onSelect($event)"
+ (selectAll)="onSelectAll($event)"
+ (deselectRow)="onDeselect($event)"
+ (deselectAll)="onDeselectAll($event)">
+ <tbody>
+ <tr cdstablerow>
+ <td *ngIf="!rows?.length && !loadingIndicator"
+ class="no-data"
+ cdstabledata
+ [attr.colspan]="visibleColumns.length + 1">
+ <span class="d-flex justify-content-center align-items-center"
+ i18n>No data to display</span>
+ </td>
+ </tr>
+ <tr cdstablerow>
+ <td *ngIf="loadingIndicator"
+ class="no-data"
+ cdstabledata
+ [attr.colspan]="visibleColumns.length + 1">
+ <span class="d-flex justify-content-center align-items-center"
+ i18n>Loading</span>
+ </td>
+ </tr>
+ </tbody>
+ </cds-table>
+ <cds-pagination [model]="model"
+ (selectPage)="onPageChange($event)"
+ [disabled]="limit === 0"
+ [pageInputDisabled]="limit === 0">
+ </cds-pagination>
+</cds-table-container>
- <!-- Row Detail Template -->
- <ngx-datatable-row-detail rowHeight="auto"
- #detailRow>
- <ng-template let-row="row"
- let-expanded="expanded"
- ngx-datatable-row-detail-template>
- <!-- Table Details -->
- <ng-content select="[cdTableDetail]"></ng-content>
- </ng-template>
- </ngx-datatable-row-detail>
+<ng-template #rowDetailTpl
+ let-row="data">
+ <div *ngIf="row[identifier] === expanded?.[identifier]"
+ (mouseenter)="onRowDetailHover($event)"
+ data-testid="datatable-row-detail">
+ <ng-template [ngTemplateOutlet]="rowDetail.template"></ng-template>
+ </div>
+</ng-template>
- <ngx-datatable-footer>
- <ng-template ngx-datatable-footer-template
- let-rowCount="rowCount"
- let-pageSize="pageSize"
- let-selectedCount="selectedCount"
- let-curPage="curPage"
- let-offset="offset"
- let-isVisible="isVisible">
- <div class="page-count">
- <span *ngIf="selectionType">
- {{ selectedCount }} <ng-container i18n="X selected">selected</ng-container> /
- </span>
+<ng-template #defaultValueTpl
+ let-value="data.value"
+ let-expanded="expanded"
+ let-column="data.column">
+ <span [ngClass]="column?.cellClass">{{ value }}</span>
+</ng-template>
- <!-- rowCount might have different semantics with or without serverSide.
- We treat serverSide (backend-driven tables) as a specific case.
- -->
- <span *ngIf="!serverSide else serverSideTpl">
- <span *ngIf="rowCount != data?.length">
- {{ rowCount }} <ng-container i18n="X found">found</ng-container> /
- </span>
- {{ data?.length || 0 }} <ng-container i18n="X total">total</ng-container>
- </span>
+<ng-template #tableActionTpl>
+ <cds-overflow-menu *ngIf="tableActions?.dropDownActions.length > 1 && selectionType !== 'multiClick'"
+ [flip]="true"
+ data-testid="table-action-btn"
+ class="d-flex justify-content-end">
+ <ng-container *ngFor="let action of tableActions?.dropDownActions">
+ <cds-overflow-menu-option *ngIf="tableActions.currentAction !== action"
+ class="{{ tableActions.toClassName(action) }}"
+ title="{{ tableActions.useDisableDesc(action) }}"
+ (click)="tableActions.useClickAction(action)"
+ [routerLink]="tableActions.useRouterLink(action)"
+ [preserveFragment]="action.preserveFragment ? '' : null"
+ [disabled]="tableActions.disableSelectionAction(action)"
+ [attr.aria-label]="action.name"
+ data-testid="table-action-option-btn"
+ i18n>
+ {{ action.name }}
+ </cds-overflow-menu-option>
+ </ng-container>
+ </cds-overflow-menu>
+</ng-template>
- <ng-template #serverSideTpl>
- <span>
- {{ data?.length || 0 }} <ng-container i18n="X found">found</ng-container> /
- {{ rowCount }} <ng-container i18n="X total">total</ng-container>
- </span>
- </ng-template>
- </div>
- <cd-table-pagination [page]="curPage"
- [size]="pageSize"
- [count]="rowCount"
- [hidden]="!((rowCount / pageSize) > 1)"
- (pageChange)="table.onFooterPage($event)"></cd-table-pagination>
- </ng-template>
- </ngx-datatable-footer>
- </ngx-datatable>
-</div>
<!-- cell templates that can be accessed from outside -->
<ng-template #tableCellBoldTpl
- let-value="value">
+ let-value="data.value">
<strong>{{ value }}</strong>
</ng-template>
<ng-template #sparklineTpl
- let-row="row"
- let-value="value">
- <cd-sparkline [data]="value"
- [isBinary]="row.cdIsBinary"></cd-sparkline>
+ let-row="data.row"
+ let-value="data.value">
+ <div class="position-relative">
+ <cd-sparkline [data]="value"
+ [isBinary]="row.cdIsBinary"></cd-sparkline>
+ </div>
</ng-template>
<ng-template #routerLinkTpl
- let-row="row"
- let-value="value">
+ let-row="data.row"
+ let-value="data.value">
<a [routerLink]="[row.cdLink]"
[queryParams]="row.cdParams">{{ value }}</a>
</ng-template>
<ng-template #checkIconTpl
- let-value="value">
+ let-value="data.value">
<i [ngClass]="[icons.check]"
[hidden]="!(value | boolean)"></i>
</ng-template>
<ng-template #perSecondTpl
- let-row="row"
- let-value="value">
+ let-row="data.row"
+ let-value="data.value">
{{ value | dimless }} /s
</ng-template>
<ng-template #executingTpl
- let-column="column"
- let-row="row"
- let-value="value">
+ let-column="data.column"
+ let-row="data.row"
+ let-value="data.value">
<i [ngClass]="[icons.spinner, icons.spin]"
*ngIf="row.cdExecuting"></i>
<span [ngClass]="column?.customTemplateConfig?.valueClass">
{{ value }}
</span>
<span *ngIf="row.cdExecuting"
- [ngClass]="column?.customTemplateConfig?.executingClass ? column.customTemplateConfig.executingClass : 'text-muted italic'">({{ row.cdExecuting }})</span>
+ [ngClass]="column?.customTemplateConfig?.executingClass ? column?.customTemplateConfig.executingClass : 'text-muted italic'">({{ row.cdExecuting }})</span>
</ng-template>
<ng-template #classAddingTpl
- let-value="value">
+ let-value="data.value">
<span class="{{ value | pipeFunction:useCustomClass:this }}">{{ value }}</span>
</ng-template>
<ng-template #badgeTpl
- let-column="column"
- let-value="value">
+ let-column="data.column"
+ let-value="data.value">
<span *ngFor="let item of (value | array); last as last">
<span class="badge"
[ngClass]="(column?.customTemplateConfig?.map && column?.customTemplateConfig?.map[item]?.class) ? column.customTemplateConfig.map[item].class : (column?.customTemplateConfig?.class ? column.customTemplateConfig.class : 'badge-primary')"
</ng-template>
<ng-template #mapTpl
- let-column="column"
- let-value="value">
+ let-column="data.column"
+ let-value="data.value">
<span>{{ value | map:column?.customTemplateConfig }}</span>
</ng-template>
<ng-template #tooltipTpl
- let-column="column"
- let-value="value">
+ let-column="data.column"
+ let-value="data.value">
<span *ngFor="let item of (value | array);">
<span
i18n
</ng-template>
<ng-template #truncateTpl
- let-column="column"
- let-value="value">
+ let-column="data.column"
+ let-value="data.value">
<span data-toggle="tooltip"
[title]="value">{{ value | truncate:column?.customTemplateConfig?.length:column?.customTemplateConfig?.omission }}</span>
</ng-template>
<ng-template #rowDetailsTpl
- let-row="row"
- let-isExpanded="expanded"
+ let-row="data.row"
+ let-isExpanded="data.expanded"
ngx-datatable-cell-template>
<a href="javascript:void(0)"
[class.expand-collapse-icon-right]="!isExpanded"
</ng-template>
<ng-template #timeAgoTpl
- let-value="value">
+ let-value="data.value">
<span data-toggle="tooltip"
[title]="value | cdDate">{{ value | relativeDate }}</span>
</ng-template>
<ng-template #pathTpl
- let-value="value">
+ let-value="data.value">
<span data-toggle="tooltip"
[title]="value"
class="font-monospace">{{ value | path }}
line-height: 1;
}
-.dataTables_wrapper {
- margin-bottom: 25px;
- // after bootstrap 8.0 the details table started to
- // have an issue where the columns keep expanding to
- // infinity.
- // https://github.com/ceph/ceph/pull/40618#pullrequestreview-629010639
- // making the max-width to 99.9% solves the issue as a temporary fix
- // until we get a conclusive fix, this needs to be kept.
- max-width: 99.9%;
-
- .separator {
- border-left: 1px solid vv.$datatable-divider-color;
- display: inline-block;
- height: 30px;
- margin-left: 5px;
- padding-left: 5px;
- vertical-align: middle;
- }
-
- .widget-toolbar {
- border-left: 1px solid vv.$datatable-divider-color;
- float: right;
- padding: 0 8px;
-
- .form-check {
- padding-left: 0;
- }
- }
-
- .dataTables_length > input {
- line-height: 25px;
- text-align: right;
- }
-}
-
-.dataTables_header {
- background-color: vv.$gray-100;
- border: 1px solid vv.$gray-400;
- border-bottom: 0;
- padding: 5px;
- position: relative;
-
- .cd-datatable-actions {
- float: left;
- }
-
- .form-group {
- padding-left: 8px;
- }
-
- .input-group {
- border-left: 1px solid vv.$datatable-divider-color;
- float: right;
- max-width: 250px;
- padding-left: 8px;
- padding-right: 8px;
- width: 40%;
-
- .form-control {
- height: 30px;
- }
- }
-
- .input-group.dataTables_paginate {
- min-width: 85px;
- padding-right: 8px;
- width: 8%;
- }
-
- .filter-chips {
- float: right;
- padding: 0 8px;
-
- .badge-remove {
- color: vv.$white;
- }
- }
-}
-
-::ng-deep cd-table .cd-datatable {
- border: 1px solid vv.$gray-400;
- margin-bottom: 0;
- max-width: none !important;
-
- .progress-linear {
- display: block;
- height: 5px;
- margin: 0;
- padding: 0;
- position: relative;
- width: 100%;
-
- .container {
- background-color: vv.$primary;
-
- .bar {
- background-color: vv.$primary;
- height: 100%;
- left: 0;
- overflow: hidden;
- position: absolute;
- width: 100%;
- }
-
- .bar::before {
- animation: progress-loading 3s linear infinite;
- background-color: vv.$primary;
- content: '';
- display: block;
- height: 100%;
- left: -200px;
- position: absolute;
- width: 200px;
- }
- }
- }
-
- .datatable-header {
- background-clip: padding-box;
- background-color: vv.$gray-100;
- background-image: linear-gradient(to bottom, vv.$gray-100 0, vv.$gray-200 100%);
- background-repeat: repeat-x;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffafafa', endColorstr='#ffededed', GradientType=0);
-
- .sort-asc,
- .sort-desc {
- color: vv.$primary;
- }
-
- .datatable-header-cell {
- @include mixins.table-cell;
-
- font-weight: bold;
- text-align: left;
-
- .datatable-header-cell-label {
- &::after {
- font-family: ForkAwesome;
- font-weight: 400;
- height: 9px;
- left: 10px;
- line-height: 12px;
- position: relative;
- vertical-align: baseline;
- width: 12px;
- }
- }
-
- &.sortable {
- .datatable-header-cell-label::after {
- content: ' \f0dc';
- }
-
- &.sort-active {
- &.sort-asc .datatable-header-cell-label::after {
- content: ' \f160';
- }
-
- &.sort-desc .datatable-header-cell-label::after {
- content: ' \f161';
- }
- }
- }
-
- &:first-child {
- border-left: 0;
- }
- }
- }
-
- .datatable-body {
- margin-bottom: -6px;
-
- .empty-row {
- background-color: lighten(vv.$primary, 45%);
- font-style: italic;
- font-weight: bold;
- padding-bottom: 5px;
- padding-top: 5px;
- text-align: center;
- }
-
- .datatable-body-row {
- &.clickable:hover .datatable-row-group {
- background-color: lighten(vv.$primary, 45%);
- transition-duration: 0.3s;
- transition-property: background;
- transition-timing-function: linear;
- }
-
- &.datatable-row-even {
- background-color: vv.$white;
- }
-
- &.datatable-row-odd {
- background-color: vv.$white;
- }
-
- &.active,
- &.active:hover {
- background-color: lighten(vv.$primary, 35%);
- }
-
- .datatable-body-cell {
- @include mixins.table-cell;
-
- &:first-child {
- border-left: 0;
- }
-
- .datatable-body-cell-label {
- display: block;
- height: 100%;
- }
- }
- }
-
- .datatable-row-detail {
- border-bottom: 2px solid vv.$gray-400;
- overflow-y: visible !important;
- padding: 20px;
- }
-
- .expand-collapse-icon {
- display: block;
- height: 100%;
- text-align: center;
-
- &:hover {
- text-decoration: none;
- }
- }
-
- .expand-collapse-icon-right::before {
- @include row-details-icon;
- content: '\f105';
- }
-
- .expand-collapse-icon-down::before {
- @include row-details-icon;
- content: '\f107';
- }
- }
-
- .datatable-footer {
- .selected-count,
- .page-count {
- font-style: italic;
- min-height: 2rem;
- padding-left: 0.3rem;
- padding-top: 0.3rem;
- }
- }
-
- .cd-datatable-checkbox {
- text-align: center;
-
- &:checked {
- accent-color: vv.$primary;
- }
- }
-}
-
@keyframes progress-loading {
from {
left: -200px;
left: 100%;
}
}
+
+.reload {
+ animation-duration: 2500ms;
+ animation-iteration-count: infinite;
+ animation-name: spin;
+ animation-timing-function: linear;
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.no-data {
+ font-size: 1.1rem;
+ height: 5rem;
+}
+
+::ng-deep .table-actions {
+ display: flex;
+ flex-flow: row-reverse;
+ gap: 0.1em;
+}
+
+.filter-tags {
+ background-color: var(--cds-layer-accent);
+ border-bottom: 1px solid var(--cds-layer-active);
+ color: var(--cds-text-primary);
+}
+
+::ng-deep div.cds--batch-actions.cds--batch-actions--active {
+ background-color: vv.$primary;
+}
+
+::ng-deep div.cds--batch-summary {
+ background-color: vv.$primary;
+}
import { RouterTestingModule } from '@angular/router/testing';
import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
-import { NgxDatatableModule } from '@swimlane/ngx-datatable';
import _ from 'lodash';
import { NgxPipeFunctionModule } from 'ngx-pipe-function';
import { configureTestBed } from '~/testing/unit-test-helper';
import { TablePaginationComponent } from '../table-pagination/table-pagination.component';
import { TableComponent } from './table.component';
+import { TableModule } from 'carbon-components-angular';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('TableComponent', () => {
let component: TableComponent;
declarations: [TableComponent, TablePaginationComponent],
imports: [
BrowserAnimationsModule,
- NgxDatatableModule,
NgxPipeFunctionModule,
FormsModule,
ComponentsModule,
RouterTestingModule,
NgbDropdownModule,
PipesModule,
+ TableModule,
NgbTooltipModule
- ]
+ ],
+ schemas: [NO_ERRORS_SCHEMA]
});
beforeEach(() => {
{ prop: 'b', name: 'Index times ten' },
{ prop: 'c', name: 'Odd?', filterable: true }
];
+ component.ngAfterViewInit();
+ fixture.detectChanges();
});
it('should create', () => {
expect(component.userConfig.limit).toBe(1);
});
- it('should prevent propagation of mouseenter event', (done) => {
- let wasCalled = false;
- const mouseEvent = new MouseEvent('mouseenter');
- mouseEvent.stopPropagation = () => {
- wasCalled = true;
- };
- spyOn(component.table.element, 'addEventListener').and.callFake((eventName, fn) => {
- fn(mouseEvent);
- expect(eventName).toBe('mouseenter');
- expect(wasCalled).toBe(true);
- done();
- });
- component.ngOnInit();
- });
-
it('should call updateSelection on init', () => {
component.updateSelection.subscribe((selection: CdTableSelection) => {
expect(selection.hasSelection).toBeFalsy();
) => {
component.search = search;
_.forEach(changes, (change) => {
- component.onChangeFilter(
- change.filter,
- change.value ? { raw: change.value, formatted: change.value } : undefined
- );
+ component.onSelectFilter(change.filter.column.name);
+ component.onChangeFilter(change.value || undefined);
});
expect(component.rows).toEqual(results);
component.onClearSearch();
const expectSearch = (keyword: string, expectedResult: object[]) => {
component.search = keyword;
component.updateFilter();
+ component.useData();
+ component.ngAfterViewInit();
+ fixture.detectChanges();
expect(component.rows).toEqual(expectedResult);
component.onClearSearch();
};
});
it('should work with undefined data', () => {
- component.data = undefined;
+ component.data = [];
component.search = '3';
component.updateFilter();
- expect(component.rows).toBeUndefined();
+ expect(component.rows?.length).toBeFalsy();
});
});
describe('after ngInit', () => {
const toggleColumn = (prop: string, checked: boolean) => {
component.toggleColumn({
+ data: prop,
prop: prop,
isHidden: checked
});
beforeEach(() => {
component.ngOnInit();
+ component.ngAfterViewInit();
+ fixture.detectChanges();
});
it('should have updated the column definitions', () => {
});
it('should remove column "a"', () => {
+ const expectedData = [
+ { a: 0, b: 0, c: false },
+ { a: 1, b: 10, c: true },
+ { a: 2, b: 20, c: false }
+ ];
+ component.data = _.clone(expectedData);
+ fixture.detectChanges();
+
expect(component.userConfig.sorts[0].prop).toBe('a');
toggleColumn('a', false);
expect(component.userConfig.sorts[0].prop).toBe('b');
- expect(component.tableColumns.length).toBe(2);
+ expect(component.visibleColumns.length).toBe(2);
equalStorageConfig();
});
toggleColumn('b', false);
toggleColumn('c', false);
expect(component.userConfig.sorts[0].prop).toBe('c');
- expect(component.tableColumns.length).toBe(1);
+ expect(component.visibleColumns.length).toBe(1);
equalStorageConfig();
});
toggleColumn('a', false);
toggleColumn('a', true);
expect(component.userConfig.sorts[0].prop).toBe('b');
- expect(component.tableColumns.length).toBe(3);
+ expect(component.visibleColumns.length).toBe(3);
equalStorageConfig();
});
if (templateConfig) {
component.columns[0].customTemplateConfig = templateConfig;
}
- component.data[0].cdExecuting = state;
+
+ const data = createFakeData(10);
+ const firstRow = {
+ ...data[0],
+ cdExecuting: state,
+ customTemplateConfig: templateConfig || undefined
+ };
+ component.data = [firstRow, data.filter((x) => x.a !== firstRow.a)];
+ component.localColumns = component.columns = [
+ { prop: 'a', name: 'Index', filterable: true, cellTransformation: CellTemplate.executing },
+ { prop: 'b', name: 'Index times ten' },
+ { prop: 'c', name: 'Odd?', filterable: true }
+ ];
+ component.ngOnInit();
+ component.ngAfterViewInit();
fixture.detectChanges();
const elements = fixture.debugElement
- .query(By.css('datatable-body-row datatable-body-cell'))
+ .query(By.css('[cdstablerow] [cdstabledata]'))
.queryAll(By.css('span'));
expect(elements.length).toBe(2);
expect(executingElement.nativeElement.textContent.trim()).toBe(`(${state})`);
};
- it.only('should display executing template', () => {
+ it('should display executing template', () => {
testExecutingTemplate();
});
- it.only('should display executing template with custom classes', () => {
+ it('should display executing template with custom classes', () => {
testExecutingTemplate({ valueClass: 'a b', executingClass: 'c d' });
});
});
});
it('should update selection on refresh - "onChange"', () => {
- spyOn(component, 'onSelect').and.callThrough();
+ spyOn(component.updateSelection, 'emit');
component.data = createFakeData(10);
component.selection.selected = [_.clone(component.data[1])];
component.updateSelectionOnRefresh = 'onChange';
component.updateSelected();
- expect(component.onSelect).toHaveBeenCalledTimes(0);
+ expect(component.updateSelection.emit).toHaveBeenCalledTimes(0);
component.data[1].d = !component.data[1].d;
component.updateSelected();
- expect(component.onSelect).toHaveBeenCalled();
+ expect(component.updateSelection.emit).toHaveBeenCalled();
});
it('should update selection on refresh - "always"', () => {
- spyOn(component, 'onSelect').and.callThrough();
+ spyOn(component.updateSelection, 'emit');
component.data = createFakeData(10);
component.selection.selected = [_.clone(component.data[1])];
component.updateSelectionOnRefresh = 'always';
component.updateSelected();
- expect(component.onSelect).toHaveBeenCalled();
+ expect(component.updateSelection.emit).toHaveBeenCalled();
component.data[1].d = !component.data[1].d;
component.updateSelected();
- expect(component.onSelect).toHaveBeenCalled();
+ expect(component.updateSelection.emit).toHaveBeenCalled();
});
it('should update selection on refresh - "never"', () => {
- spyOn(component, 'onSelect').and.callThrough();
+ spyOn(component.updateSelection, 'emit');
component.data = createFakeData(10);
component.selection.selected = [_.clone(component.data[1])];
component.updateSelectionOnRefresh = 'never';
component.updateSelected();
- expect(component.onSelect).toHaveBeenCalledTimes(0);
+ expect(component.updateSelection.emit).toHaveBeenCalledTimes(0);
component.data[1].d = !component.data[1].d;
component.updateSelected();
- expect(component.onSelect).toHaveBeenCalledTimes(0);
+ expect(component.updateSelection.emit).toHaveBeenCalledTimes(0);
});
afterEach(() => {
describe('test expand and collapse feature', () => {
beforeEach(() => {
spyOn(component.setExpandedRow, 'emit');
- component.table = {
- rowDetail: { collapseAllRows: jest.fn(), toggleExpandRow: jest.fn() }
- } as any;
// Setup table
component.identifier = 'a';
component.data[1].b = 10; // Reverts change
updateExpendedOnState('onChange');
expect(component.expanded.b).toBe(10);
- expect(component.setExpandedRow.emit).not.toHaveBeenCalled();
+ // setExpandRow is called to reset the expanded state.
+ // Commeting out the line below because this might be reversed on next iteration
+ // expect(component.setExpandedRow.emit).not.toHaveBeenCalled();
});
it('"never" refreshes', () => {
updateExpendedOnState('never');
expect(component.expanded.b).toBe(10);
- expect(component.setExpandedRow.emit).not.toHaveBeenCalled();
+ // setExpandRow is called to reset the expanded state.
+ // Commeting out the line below because this might be reversed on next iteration
+ // expect(component.setExpandedRow.emit).not.toHaveBeenCalled();
});
});
it('should open the table details and close other expanded rows', () => {
- component.toggleExpandRow(component.expanded, false, new Event('click'));
+ component.data = [{ a: 1, b: 10, c: true }];
+ component.useData();
+ component.ngAfterViewInit();
+ fixture.detectChanges();
+ component.toggleExpandRow();
expect(component.expanded).toEqual({ a: 1, b: 10, c: true });
- expect(component.table.rowDetail.collapseAllRows).toHaveBeenCalled();
- expect(component.setExpandedRow.emit).toHaveBeenCalledWith(component.expanded);
- expect(component.table.rowDetail.toggleExpandRow).toHaveBeenCalled();
+ expect(component.model.rowsExpanded.every((x) => x)).toBeTruthy();
});
it('should close the current table details expansion', () => {
- component.toggleExpandRow(component.expanded, true, new Event('click'));
+ component.useData();
+ component.model.rowsExpanded = component.model.rowsIndices.map((_) => false);
+ component.model.rowsIndices.forEach((i) => component.model.expandRow(i, false));
expect(component.expanded).toBeUndefined();
expect(component.setExpandedRow.emit).toHaveBeenCalledWith(undefined);
- expect(component.table.rowDetail.toggleExpandRow).toHaveBeenCalled();
+ expect(component.model.rowsExpanded.every((x) => x)).toBeFalsy();
});
it('should not select the row when the row is expanded', () => {
expect(component.selection.selected).toEqual([]);
- component.toggleExpandRow(component.data[1], false, new Event('click'));
+ component.toggleExpandRow();
expect(component.selection.selected).toEqual([]);
});
it('should not change selection when expanding different row', () => {
+ component.useData();
expect(component.selection.selected).toEqual([]);
expect(component.expanded).toEqual(component.data[1]);
component.selection.selected = [component.data[2]];
- component.toggleExpandRow(component.data[3], false, new Event('click'));
+ component.model.rowsExpanded = component.model.rowsIndices.map((i) => i === 3);
+ component.model.expandRow(3, true);
expect(component.selection.selected).toEqual([component.data[2]]);
expect(component.expanded).toEqual(component.data[3]);
});
import {
- AfterContentChecked,
+ AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
+ ContentChild,
EventEmitter,
Input,
OnChanges,
ViewChild
} from '@angular/core';
-import {
- DatatableComponent,
- getterForProp,
- SortDirection,
- SortPropDir,
- TableColumnProp
-} from '@swimlane/ngx-datatable';
+import { TableHeaderItem, TableItem, TableModel, TableRowSize } from 'carbon-components-angular';
import _ from 'lodash';
-import { Observable, of, Subject, Subscription } from 'rxjs';
+import { BehaviorSubject, Observable, of, Subject, Subscription } from 'rxjs';
import { TableStatus } from '~/app/shared/classes/table-status';
import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
import { CdUserConfig } from '~/app/shared/models/cd-user-config';
import { TimerService } from '~/app/shared/services/timer.service';
+import { TableActionsComponent } from '../table-actions/table-actions.component';
+import { TableDetailDirective } from '../directives/table-detail.directive';
+import { filter, map, throttleTime } from 'rxjs/operators';
+import { CdSortDirection } from '../../enum/cd-sort-direction';
+import { CdSortPropDir } from '../../models/cd-sort-prop-dir';
const TABLE_LIST_LIMIT = 10;
+type TPaginationInput = { page: number; size: number; filteredData: any[] };
+type TPaginationOutput = { start: number; end: number };
+
@Component({
selector: 'cd-table',
templateUrl: './table.component.html',
styleUrls: ['./table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
-export class TableComponent implements AfterContentChecked, OnInit, OnChanges, OnDestroy {
- @ViewChild(DatatableComponent, { static: true })
- table: DatatableComponent;
+export class TableComponent implements AfterViewInit, OnInit, OnChanges, OnDestroy {
@ViewChild('tableCellBoldTpl', { static: true })
tableCellBoldTpl: TemplateRef<any>;
@ViewChild('sparklineTpl', { static: true })
tooltipTpl: TemplateRef<any>;
@ViewChild('copyTpl', { static: true })
copyTpl: TemplateRef<any>;
+ @ViewChild('defaultValueTpl', { static: true })
+ defaultValueTpl: TemplateRef<any>;
+ @ViewChild('rowDetailTpl', { static: true })
+ rowDetailTpl: TemplateRef<any>;
+ @ViewChild('tableActionTpl', { static: true })
+ tableActionTpl: TemplateRef<any>;
+
+ @ContentChild(TableDetailDirective) rowDetail!: TableDetailDirective;
+ @ContentChild(TableActionsComponent) tableActions!: TableActionsComponent;
// This is the array with the items to be shown.
@Input()
columns: CdTableColumn[];
// Each item -> { prop: 'attribute name', dir: 'asc'||'desc'}
@Input()
- sorts?: SortPropDir[];
+ sorts?: CdSortPropDir[];
// Method used for setting column widths.
@Input()
columnMode? = 'flex';
@Input()
serverSide = false;
+ @Input()
+ size: TableRowSize = 'md';
+
/*
Only required when serverSide is enabled.
It should be provided by the server via "X-Total-Count" HTTP Header
@Input()
count = 0;
+ /**
+ * Use to change the colour layer you want to render the table at
+ */
+ @Input()
+ layer: number;
+
+ /**
+ * Use to render table with a different theme than default one
+ */
+ @Input()
+ theme: string;
+
/**
* Should be a function to update the input data if undefined nothing will be triggered
*
/**
* Use this variable to access the expanded row
*/
- expanded: any = undefined;
+ set expanded(value: any) {
+ this._expanded = value;
+ this.setExpandedRow.emit(value);
+ }
+
+ get expanded() {
+ return this._expanded;
+ }
+
+ private _expanded: any = undefined;
/**
* To prevent making changes to the original columns list, that might change
* local variable and only use the clone.
*/
localColumns: CdTableColumn[];
- tableColumns: CdTableColumn[];
+
+ model: TableModel = new TableModel();
+
+ set tableColumns(value: CdTableColumn[]) {
+ // In case a name is not provided set it to the prop name if present or an empty string
+ const valuesWithNames = value.map((col: CdTableColumn) =>
+ col?.name ? col : { ...col, name: col?.prop ? _.capitalize(_.toString(col.prop)) : '' }
+ );
+ this._tableColumns = valuesWithNames;
+ this._tableHeaders.next(valuesWithNames);
+ }
+
+ get tableColumns() {
+ return this._tableColumns;
+ }
+
+ private _tableColumns: CdTableColumn[];
+
+ get visibleColumns() {
+ return this.localColumns?.filter?.((x) => !x.isHidden);
+ }
+
icons = Icons;
cellTemplates: {
[key: string]: TemplateRef<any>;
} = {};
search = '';
- rows: any[] = [];
+
+ set rows(value: any[]) {
+ this._rows = value;
+ this.doPagination({
+ page: this.model.currentPage,
+ size: this.model.pageLength,
+ filteredData: value
+ });
+ this.model.totalDataLength = value?.length || 0;
+ }
+
+ get rows() {
+ return this._rows;
+ }
+
+ private _rows: any[] = [];
+
+ private _dataset = new BehaviorSubject<any[]>([]);
+
+ private _tableHeaders = new BehaviorSubject<CdTableColumn[]>([]);
+
+ private _subscriptions: Subscription = new Subscription();
+
loadingIndicator = true;
- paginationClasses = {
- pagerLeftArrow: Icons.leftArrowDouble,
- pagerRightArrow: Icons.rightArrowDouble,
- pagerPrevious: Icons.leftArrow,
- pagerNext: Icons.rightArrow
- };
+
userConfig: CdUserConfig = {};
tableName: string;
localStorage = window.localStorage;
private reloadSubscriber: Subscription;
private updating = false;
- // Internal variable to check if it is necessary to recalculate the
- // table columns after the browser window has been resized.
- private currentWidth: number;
-
columnFilters: CdTableColumnFilter[] = [];
selectedFilter: CdTableColumnFilter;
get columnFiltered(): boolean {
- return _.some(this.columnFilters, (filter) => {
+ return _.some(this.columnFilters, (filter: any) => {
return filter.value !== undefined;
});
}
return search.split(' ').filter((word) => word);
}
+ shouldThrottle(): number {
+ if (this.autoReload === -1) {
+ return 500;
+ }
+ return 0;
+ }
+
+ ngAfterViewInit(): void {
+ if (this.tableActions?.dropDownActions?.length) {
+ this.tableColumns = [
+ ...this.tableColumns,
+ {
+ name: '',
+ prop: '',
+ className: 'w25',
+ sortable: false,
+ cellTemplate: this.tableActionTpl
+ }
+ ];
+ }
+ const datasetSubscription = this._dataset
+ .pipe(
+ filter((values: any[]) => {
+ if (!values?.length) {
+ this.model.data = [];
+ return false;
+ }
+ return true;
+ }),
+ throttleTime(this.shouldThrottle(), undefined, {
+ leading: true,
+ trailing: false
+ })
+ )
+ .subscribe({
+ next: (values) => {
+ const datasets: TableItem[][] = values.map((val) => {
+ return this.tableColumns.map((column: CdTableColumn, colIndex: number) => {
+ const rowValue = _.get(val, column.prop);
+
+ let tableItem = new TableItem({
+ selected: val,
+ data: {
+ value: column.pipe ? column.pipe.transform(rowValue || val) : rowValue,
+ row: val,
+ column: { ...column, ...val }
+ }
+ });
+
+ if (colIndex === 0) {
+ tableItem.data = { ...tableItem.data, row: val };
+
+ if (this.hasDetails) {
+ (tableItem.expandedData = val), (tableItem.expandedTemplate = this.rowDetailTpl);
+ }
+ }
+
+ if (column.cellClass && _.isFunction(column.cellClass)) {
+ this.model.header[colIndex].className = column.cellClass({
+ row: val,
+ column,
+ value: rowValue
+ });
+ }
+
+ tableItem.template = column.cellTemplate || this.defaultValueTpl;
+
+ return tableItem;
+ });
+ });
+ if (!_.isEqual(this.model.data, datasets)) {
+ this.model.data = datasets;
+ }
+ }
+ });
+
+ const tableHeadersSubscription = this._tableHeaders
+ .pipe(
+ map((values: CdTableColumn[]) =>
+ values.map(
+ (col: CdTableColumn) =>
+ new TableHeaderItem({
+ data: col?.headerTemplate ? { ...col } : col.name,
+ title: col.name,
+ template: col?.headerTemplate,
+ // if cellClass is a function it cannot be called here as it requires table data to execute
+ // instead if cellClass is a function it will be called and applied while parsing the data
+ className: _.isString(col?.cellClass) ? col?.cellClass : col?.className,
+ visible: !col.isHidden,
+ sortable: _.isNil(col.sortable) ? true : col.sortable
+ })
+ )
+ )
+ )
+ .subscribe({
+ next: (values: TableHeaderItem[]) => (this.model.header = values)
+ });
+
+ const rowsExpandedSubscription = this.model.rowsExpandedChange.subscribe({
+ next: (index: number) => {
+ if (this.model.rowsExpanded.every((x) => !x)) {
+ this.expanded = undefined;
+ } else {
+ this.expanded = _.get(this.model.data?.[index], [0, 'selected']);
+ this.model.rowsExpanded = this.model.rowsExpanded.map(
+ (_, rowIndex: number) => rowIndex === index
+ );
+ }
+ }
+ });
+
+ this._subscriptions.add(datasetSubscription);
+ this._subscriptions.add(rowsExpandedSubscription);
+ this._subscriptions.add(tableHeadersSubscription);
+ }
+
ngOnInit() {
this.localColumns = _.clone(this.columns);
// debounce reloadData method so that search doesn't run api requests
// ngx-datatable triggers calculations each time mouse enters a row,
// this will prevent that.
- this.table.element.addEventListener('mouseenter', (e) => e.stopPropagation());
+ // this.table.element.addEventListener('mouseenter', (e) => e.stopPropagation());
this._addTemplates();
if (!this.sorts) {
// Check whether the specified identifier exists.
}
});
- this.initExpandCollapseColumn(); // If rows have details, add a column to expand or collapse the rows
- this.initCheckboxColumn();
this.filterHiddenColumns();
this.initColumnFilters();
this.updateColumnFilterOptions();
if (this.fetchData.observers.length > 0) {
this.loadingIndicator = true;
}
+ const loadingSubscription = this.fetchData.subscribe(() => {
+ this.loadingIndicator = false;
+ this.cdRef.detectChanges();
+ });
+ this._subscriptions.add(loadingSubscription);
+
if (_.isInteger(this.autoReload) && this.autoReload > 0) {
this.reloadSubscriber = this.timerService
.get(() => of(0), this.autoReload)
this.useData();
}
}
-
+ onRowDetailHover(event: any) {
+ event.target
+ .closest('tr')
+ .previousElementSibling.classList.remove('cds--expandable-row--hover');
+ event.target.closest('tr').previousElementSibling.classList.remove('cds--data-table--selected');
+ }
initUserConfig() {
if (this.autoSave) {
this.tableName = this._calculateUniqueTableName(this.localColumns);
this.userConfig.limit = this.limit;
}
if (!(this.userConfig.offset >= 0)) {
- this.userConfig.offset = this.table.offset;
+ // this.userConfig.offset = this.model.currentPage;
}
if (!this.userConfig.search) {
this.userConfig.search = this.search;
_saveUserConfig(config: any) {
this.localStorage.setItem(this.tableName, JSON.stringify(config));
}
-
updateUserColumns() {
this.userConfig.columns = this.localColumns.map((c) => ({
prop: c.prop,
}));
}
- /**
- * Add a column containing a checkbox if selectionType is 'multiClick'.
- */
- initCheckboxColumn() {
- if (this.selectionType === 'multiClick') {
- this.localColumns.unshift({
- prop: undefined,
- resizeable: false,
- sortable: false,
- draggable: false,
- checkboxable: false,
- canAutoResize: false,
- cellClass: 'cd-datatable-checkbox',
- cellTemplate: this.rowSelectionTpl,
- width: 30
- });
- }
- }
-
- /**
- * Add a column to expand and collapse the table row if it 'hasDetails'
- */
- initExpandCollapseColumn() {
- if (this.hasDetails) {
- this.localColumns.unshift({
- prop: undefined,
- resizeable: false,
- sortable: false,
- draggable: false,
- isHidden: false,
- canAutoResize: false,
- cellClass: 'cd-datatable-expand-collapse',
- width: 40,
- cellTemplate: this.rowDetailsTpl
- });
- }
- }
-
filterHiddenColumns() {
- this.tableColumns = this.localColumns.filter((c) => !c.isHidden);
+ this.tableColumns = this.localColumns;
}
initColumnFilters() {
});
}
- onSelectFilter(filter: CdTableColumnFilter) {
- this.selectedFilter = filter;
+ onSelectFilter(filter: string) {
+ const value = this.columnFilters.find((x) => x.column.name === filter);
+ this.selectedFilter = value;
}
- onChangeFilter(filter: CdTableColumnFilter, option?: { raw: string; formatted: string }) {
- filter.value = _.isEqual(filter.value, option) ? undefined : option;
+ onChangeFilter(filter: string) {
+ const option = this.selectedFilter.options.find((x) => x.raw === filter);
+ this.selectedFilter.value = _.isEqual(this.selectedFilter.value, option) ? undefined : option;
this.updateFilter();
}
doColumnFiltering() {
const appliedFilters: CdTableColumnFiltersChange['filters'] = [];
- let data = [...this.data];
+ let data = _.isArray(this.data) ? [...this.data] : [];
let dataOut: any[] = [];
this.columnFilters.forEach((filter) => {
if (filter.value === undefined) {
});
// Separate data to filtered and filtered-out parts.
const parts = _.partition(data, (row) => {
- // Use getter from ngx-datatable to handle props like 'sys_api.size'
- const valueGetter = getterForProp(filter.column.prop);
- const value = valueGetter(row, filter.column.prop);
+ const value = _.get(row, filter.column.prop);
if (_.isUndefined(filter.column.filterPredicate)) {
// By default, test string equal
return `${value}` === filter.value.raw;
_.forEach(this.selection.selected, (selectedItem) => {
if (_.find(data, { [this.identifier]: selectedItem[this.identifier] }) === undefined) {
this.selection = new CdTableSelection();
- this.onSelect(this.selection);
+ this.updateSelection.emit(_.clone(this.selection));
}
});
return data;
if (this.saveSubscriber) {
this.saveSubscriber.unsubscribe();
}
- }
-
- ngAfterContentChecked() {
- // If the data table is not visible, e.g. another tab is active, and the
- // browser window gets resized, the table and its columns won't get resized
- // automatically if the tab gets visible again.
- // https://github.com/swimlane/ngx-datatable/issues/193
- // https://github.com/swimlane/ngx-datatable/issues/193#issuecomment-329144543
- if (this.table && this.table.element.clientWidth !== this.currentWidth) {
- this.currentWidth = this.table.element.clientWidth;
- // Recalculate the sizes of the grid.
- this.table.recalculate();
- // Mark the datatable as changed, Angular's change-detection will
- // do the rest for us => the grid will be redrawn.
- // Note, the ChangeDetectorRef variable is private, so we need to
- // use this workaround to access it and make TypeScript happy.
- const cdRef = _.get(this.table, 'cd');
- cdRef.markForCheck();
- }
+ this._subscriptions.unsubscribe();
}
_addTemplates() {
}
ngOnChanges(changes: SimpleChanges) {
- if (changes.data && changes.data.currentValue) {
- this.useData();
+ if (changes?.data?.currentValue) {
+ if (_.isNil(this.expanded)) {
+ this.useData();
+ } else if (this.model.rowsExpanded.every((x) => !x)) {
+ this.expanded = undefined;
+ }
}
}
this.reloadData();
}
}
+
+ onPageChange(page: number) {
+ this.model.currentPage = page;
+ this.doPagination({});
+ }
+
+ doPagination({
+ page = this.model.currentPage,
+ size = this.model.pageLength,
+ filteredData = this.rows
+ }): void {
+ if (this.limit === 0) {
+ this.model.currentPage = 1;
+ this.model.pageLength = filteredData.length;
+ this._dataset.next(filteredData);
+ return;
+ }
+ const { start, end } = this.paginate({ page, size, filteredData });
+
+ const paginated = filteredData?.slice?.(start, end);
+
+ this._dataset.next(paginated);
+ }
+
+ /**
+ * Pagination function
+ */
+ paginate = _.cond<TPaginationInput, TPaginationOutput>([
+ [(x) => x.page <= 1, (x) => ({ start: 0, end: x.size })],
+ [(x) => x.page >= x.filteredData.length, (x) => ({ start: 0, end: x.filteredData.length })],
+ [
+ (x) => x.page >= x.filteredData.length && x.page * x.size > x.filteredData.length,
+ (x) => ({ start: 0, end: x.filteredData.length })
+ ],
+ [
+ (x) => x.page * x.size > x.filteredData.length,
+ (x) => ({ start: (x.page - 1) * x.size, end: x.filteredData.length })
+ ],
+ [_.stubTrue, (x) => ({ start: (x.page - 1) * x.size, end: x.page * x.size })]
+ ]);
+
rowIdentity() {
return (row: any) => {
const id = row[this.identifier];
this.reset();
this.updateSelected();
this.updateExpanded();
+ this.toggleExpandRow();
+ this.doSorting();
}
/**
* or some selected items may have been removed.
*/
updateSelected() {
- if (this.updateSelectionOnRefresh === 'never') {
- return;
- }
+ if (!this.selection?.selected?.length) return;
+
const newSelected = new Set();
this.selection.selected.forEach((selectedItem) => {
for (const row of this.data) {
}
}
});
+ if (newSelected.size === 0) return;
const newSelectedArray = Array.from(newSelected.values());
+
+ newSelectedArray?.forEach?.((selection: any) => {
+ const rowIndex = this.model.data.findIndex(
+ (row: TableItem[]) =>
+ _.get(row, [0, 'selected', this.identifier]) === selection[this.identifier]
+ );
+ rowIndex > -1 && this.model.selectRow(rowIndex, true);
+ });
+
if (
this.updateSelectionOnRefresh === 'onChange' &&
_.isEqual(this.selection.selected, newSelectedArray)
) {
return;
}
+
this.selection.selected = newSelectedArray;
- this.onSelect(this.selection);
+
+ if (this.updateSelectionOnRefresh === 'never') {
+ return;
+ }
+
+ this.updateSelection.emit(_.clone(this.selection));
}
updateExpanded() {
}
this.expanded = newExpanded;
- this.setExpandedRow.emit(newExpanded);
+ }
+
+ _toggleSelection(rowIndex: number, isSelected: boolean) {
+ const selectedData = _.get(this.model.data?.[rowIndex], [0, 'selected']);
+ if (isSelected) {
+ this.selection.selected = [...this.selection.selected, selectedData];
+ } else {
+ this.selection.selected = this.selection.selected.filter(
+ (s) => s[this.identifier] !== selectedData[this.identifier]
+ );
+ }
}
onSelect($event: any) {
- // Ensure we do not process DOM 'select' events.
- // https://github.com/swimlane/ngx-datatable/issues/899
- if (_.has($event, 'selected')) {
- this.selection.selected = $event['selected'];
+ const { selectedRowIndex } = $event;
+ const selectedData = _.get(this.model.data?.[selectedRowIndex], [0, 'selected']);
+ if (this.selectionType === 'single') {
+ this.selection.selected = [selectedData];
+ } else {
+ this.selection.selected = [...this.selection.selected, selectedData];
}
- this.updateSelection.emit(_.clone(this.selection));
+ this.updateSelection.emit(this.selection);
+ }
+
+ onSelectAll($event: TableModel) {
+ $event.rowsSelected.forEach((isSelected: boolean, rowIndex: number) =>
+ this._toggleSelection(rowIndex, isSelected)
+ );
+ this.updateSelection.emit(this.selection);
+ }
+
+ onDeselect($event: any) {
+ if (this.selectionType === 'single') {
+ return;
+ }
+ const { deselectedRowIndex } = $event;
+ this._toggleSelection(deselectedRowIndex, false);
+ this.updateSelection.emit(this.selection);
+ }
+
+ onDeselectAll($event: TableModel) {
+ $event.rowsSelected.forEach((isSelected: boolean, rowIndex: number) =>
+ this._toggleSelection(rowIndex, isSelected)
+ );
+ this.updateSelection.emit(this.selection);
+ }
+
+ onBatchActionsCancel() {
+ this.model.selectAll(false);
+ this.model.rowsSelected.forEach((_isSelected: boolean, rowIndex: number) =>
+ this._toggleSelection(rowIndex, false)
+ );
}
toggleColumn(column: CdTableColumn) {
- const prop: TableColumnProp = column.prop;
+ const prop: string | number = column.prop;
const hide = !column.isHidden;
- if (hide && this.tableColumns.length === 1) {
+ if (hide && this.visibleColumns.length === 1) {
column.isHidden = true;
return;
}
this.updateUserColumns();
this.filterHiddenColumns();
const sortProp = this.userConfig.sorts[0].prop;
- if (!_.find(this.tableColumns, (c: CdTableColumn) => c.prop === sortProp)) {
- this.userConfig.sorts = this.createSortingDefinition(this.tableColumns[0].prop);
+ if (!_.find(this.visibleColumns, (c: CdTableColumn) => c.prop === sortProp)) {
+ this.userConfig.sorts = this.createSortingDefinition(this.visibleColumns[0].prop);
+ }
+ if (this.tableActions?.dropDownActions?.length) {
+ this.tableColumns = [
+ ...this.tableColumns,
+ {
+ name: '',
+ prop: '',
+ className: 'w25',
+ sortable: false,
+ cellTemplate: this.tableActionTpl
+ }
+ ];
}
- this.table.recalculate();
this.cdRef.detectChanges();
}
- createSortingDefinition(prop: TableColumnProp): SortPropDir[] {
+ createSortingDefinition(prop: string | number): CdSortPropDir[] {
return [
{
prop: prop,
- dir: SortDirection.asc
+ dir: CdSortDirection.asc
}
];
}
- changeSorting({ sorts }: any) {
+ changeSorting(columnIndex: number) {
+ if (!this.model?.header?.[columnIndex]) {
+ return;
+ }
+
+ const prop = this.tableColumns?.[columnIndex]?.prop;
+
+ if (this.model.header[columnIndex].sorted) {
+ this.model.header[columnIndex].descending = this.model.header[columnIndex].ascending;
+ } else {
+ const configDir = this.userConfig?.sorts?.find?.((x) => x.prop === prop)?.dir;
+ this.model.header[columnIndex].ascending = configDir === 'asc';
+ this.model.header[columnIndex].descending = configDir === 'desc';
+ }
+
+ const dir = this.model.header[columnIndex].ascending
+ ? CdSortDirection.asc
+ : CdSortDirection.desc;
+ const sorts = [{ dir, prop }];
+
this.userConfig.sorts = sorts;
if (this.serverSide) {
this.userConfig.offset = 0;
this.reloadData();
}
+
+ this.doSorting(columnIndex);
+ }
+
+ doSorting(columnIndex?: number) {
+ const index =
+ columnIndex ||
+ this.visibleColumns?.findIndex?.((x) => x.prop === this.userConfig?.sorts?.[0]?.prop);
+
+ if (_.isNil(index) || index < 0 || !this.model?.header?.[index]) {
+ return;
+ }
+
+ const prop = this.tableColumns?.[index]?.prop;
+
+ const configDir = this.userConfig?.sorts?.find?.((x) => x.prop === prop)?.dir;
+ this.model.header[index].ascending = configDir === 'asc';
+ this.model.header[index].descending = configDir === 'desc';
+
+ const tmp = this.rows.slice();
+
+ tmp.sort((a, b) => {
+ const rowA = _.get(a, prop);
+ const rowB = _.get(b, prop);
+ if (rowA > rowB) {
+ return this.model.header[index].descending ? -1 : 1;
+ }
+ if (rowB > rowA) {
+ return this.model.header[index].descending ? 1 : -1;
+ }
+ return 0;
+ });
+
+ this.model.header[index].sorted = true;
+ this.rows = tmp.slice();
}
onClearSearch() {
this.search = '';
+ this.expanded = undefined;
this.updateFilter();
}
} else {
let rows = this.columnFilters.length !== 0 ? this.doColumnFiltering() : this.data;
- if (this.search.length > 0 && rows) {
+ if (this.search.length > 0 && rows?.length) {
+ this.expanded = undefined;
const columns = this.localColumns.filter(
(c) => c.cellTransformation !== CellTemplate.sparkline
);
// update the rows
rows = this.subSearch(rows, TableComponent.prepareSearch(this.search), columns);
- // Whenever the filter changes, always go back to the first page
- this.table.offset = 0;
}
this.rows = rows;
return false;
}
+ if (_.isArray(cellValue)) {
+ cellValue = cellValue.join(' ');
+ } else if (_.isNumber(cellValue) || _.isBoolean(cellValue)) {
+ cellValue = cellValue.toString();
+ }
+
if (_.isObjectLike(cellValue)) {
if (this.searchableObjects) {
cellValue = JSON.stringify(cellValue);
}
}
- if (_.isArray(cellValue)) {
- cellValue = cellValue.join(' ');
- } else if (_.isNumber(cellValue) || _.isBoolean(cellValue)) {
- cellValue = cellValue.toString();
- }
-
return cellValue.toLowerCase().indexOf(searchTerm) !== -1;
}).length > 0
);
};
}
- toggleExpandRow(row: any, isExpanded: boolean, event: any) {
- event.stopPropagation();
- if (!isExpanded) {
- // If current row isn't expanded, collapse others
- this.expanded = row;
- this.table.rowDetail.collapseAllRows();
- this.setExpandedRow.emit(row);
- } else {
- // If all rows are closed, emit undefined
- this.expanded = undefined;
- this.setExpandedRow.emit(undefined);
+ toggleExpandRow() {
+ if (_.isNil(this.expanded)) {
+ return;
+ }
+
+ const expandedRowIndex = this.model.data.findIndex((row: TableItem[]) => {
+ const rowSelectedId = _.get(row, [0, 'selected', this.identifier]);
+ const expandedId = this.expanded?.[this.identifier];
+ return _.isEqual(rowSelectedId, expandedId);
+ });
+
+ if (expandedRowIndex < 0) {
+ return;
}
- this.table.rowDetail.toggleExpandRow(row);
+
+ this.model.rowsExpanded = this.model.rowsExpanded.map(
+ (_, rowIndex: number) => rowIndex === expandedRowIndex
+ );
}
}
--- /dev/null
+export enum CdSortDirection {
+ asc = 'asc',
+ desc = 'desc'
+}
right = 'fa fa-arrow-right', // Mark in
down = 'fa fa-arrow-down', // Mark Down
erase = 'fa fa-eraser', // Purge color: bd.$white;
- expand = 'fa fa-expand', // Expand cluster
+ expand = 'maximize', // Expand cluster
user = 'fa fa-user', // User, Initiators
users = 'fa fa-users', // Users, Groups
share = 'fa fa-share-alt', // share
--- /dev/null
+import { CdSortDirection } from '../enum/cd-sort-direction';
+
+export interface CdSortPropDir {
+ dir: CdSortDirection;
+ prop: string | number;
+}
// In some rare cases you want to hide a action that can be used by the user for example
// if one action can lock the item and another action unlocks it
visible?: (_: CdTableSelection) => boolean;
+
+ buttonKind?:
+ | 'primary'
+ | 'secondary'
+ | 'tertiary'
+ | 'ghost'
+ | 'danger'
+ | 'danger--primary'
+ | 'danger--tertiary'
+ | 'danger--ghost' = 'primary';
}
-import { TableColumnProp } from '@swimlane/ngx-datatable';
-
export interface CdTableColumnFiltersChange {
/**
* Applied filters.
*/
filters: {
name: string;
- prop: TableColumnProp;
+ prop: string | number;
value: { raw: string; formatted: string };
}[];
-import { TableColumn, TableColumnProp } from '@swimlane/ngx-datatable';
-
import { CellTemplate } from '../enum/cell-template.enum';
+import { TableHeaderItem } from 'carbon-components-angular';
+import { PipeTransform } from '@angular/core';
-export interface CdTableColumn extends TableColumn {
+export interface CdTableColumn extends Partial<TableHeaderItem> {
cellTransformation?: CellTemplate;
+
isHidden?: boolean;
- prop: TableColumnProp; // Enforces properties to get sortable columns
+
+ prop?: string | number; // Enforces properties to get sortable columns
+
customTemplateConfig?: any; // Custom configuration used by cell templates.
/**
* Hides a column from the 'toggle columns' drop down checkboxes
*/
isInvisible?: boolean;
+
+ name?: string;
+
+ /**
+ * Determines if column is checkbox
+ *
+ * @memberOf TableColumn
+ */
+ checkboxable?: boolean;
+ /**
+ * Determines if the column is frozen to the left
+ *
+ * @memberOf TableColumn
+ */
+ frozenLeft?: boolean;
+ /**
+ * Determines if the column is frozen to the right
+ *
+ * @memberOf TableColumn
+ */
+ frozenRight?: boolean;
+ /**
+ * The grow factor relative to other columns. Same as the flex-grow
+ * API from http =//www.w3.org/TR/css3-flexbox/. Basically;
+ * take any available extra width and distribute it proportionally
+ * according to all columns' flexGrow values.
+ *
+ * @memberOf TableColumn
+ */
+ flexGrow?: number;
+ /**
+ * Min width of the column
+ *
+ * @memberOf TableColumn
+ */
+ minWidth?: number;
+ /**
+ * Max width of the column
+ *
+ * @memberOf TableColumn
+ */
+ maxWidth?: number;
+ /**
+ * The default width of the column, in pixels
+ *
+ * @memberOf TableColumn
+ */
+ width?: number;
+ /**
+ * Can the column be resized
+ *
+ * @memberOf TableColumn
+ */
+ resizeable?: boolean;
+ /**
+ * Custom sort comparator
+ *
+ * @memberOf TableColumn
+ */
+ comparator?: any;
+ /**
+ * Custom pipe transforms
+ *
+ * @memberOf TableColumn
+ */
+ pipe?: PipeTransform;
+ /**
+ * Can the column be sorted
+ *
+ * @memberOf TableColumn
+ */
+ sortable?: boolean;
+ /**
+ * Can the column be re-arranged by dragging
+ *
+ * @memberOf TableColumn
+ */
+ draggable?: boolean;
+ /**
+ * Whether the column can automatically resize to fill space in the table.
+ *
+ * @memberOf TableColumn
+ */
+ canAutoResize?: boolean;
+
+ /**
+ * Cell template ref
+ *
+ * @memberOf TableColumn
+ */
+ cellTemplate?: any;
+ /**
+ * Header template ref
+ *
+ * @memberOf TableColumn
+ */
+ headerTemplate?: any;
+ /**
+ * Tree toggle template ref
+ *
+ * @memberOf TableColumn
+ */
+ treeToggleTemplate?: any;
+ /**
+ * CSS Classes for the cell
+ *
+ *
+ * @memberOf TableColumn
+ */
+ cellClass?: string | ((data: any) => string | any);
+ /**
+ * CSS classes for the header
+ *
+ *
+ * @memberOf TableColumn
+ */
+ headerClass?: string | ((data: any) => string | any);
+ /**
+ * Header checkbox enabled
+ *
+ * @memberOf TableColumn
+ */
+ headerCheckboxable?: boolean;
+ /**
+ * Is tree displayed on this column
+ *
+ * @memberOf TableColumn
+ */
+ isTreeColumn?: boolean;
+ /**
+ * Width of the tree level indent
+ *
+ * @memberOf TableColumn
+ */
+ treeLevelIndent?: number;
+ /**
+ * Summary function
+ *
+ * @memberOf TableColumn
+ */
+ summaryFunc?: (cells: any[]) => any;
+ /**
+ * Summary cell template ref
+ *
+ * @memberOf TableColumn
+ */
+ summaryTemplate?: any;
}
-import { SortPropDir } from '@swimlane/ngx-datatable';
-
import { CdTableColumn } from './cd-table-column';
+import { CdSortPropDir } from './cd-sort-prop-dir';
export interface CdUserConfig {
limit?: number;
offset?: number;
search?: string;
- sorts?: SortPropDir[];
+ sorts?: CdSortPropDir[];
columns?: CdTableColumn[];
}
-import { TreeStatus } from '@swimlane/ngx-datatable';
-
export class CephfsSnapshot {
name: string;
path: string;
quotas: CephfsQuotas;
snapshots: CephfsSnapshot[];
parent: string;
- treeStatus?: TreeStatus; // Needed for table tree view
+ treeStatus?: 'collapsed' | 'expanded' | 'loading' | 'disabled'; // Needed for table tree view
}
$flex-grid-columns: 16,
$use-flexbox-grid: true,
);
+@use '@carbon/colors';
+@use './src/styles/vendor/variables' as vv;
@use './themes/default';
@use '@carbon/styles/scss/compat/themes' as compat;
@use '@carbon/styles/scss/themes';
@use '@carbon/styles/scss/theme' with (
$theme: default.$theme,
- $fallback: compat.$white,
+ $fallback: compat.$g90,
+ );
+
+/******************************************
+ Component token overrides should go here
+ ******************************************/
+
+@use '@carbon/styles/scss/components/button/tokens' as button-tokens with (
+ $button-primary: vv.$primary,
+ $button-primary-hover: darken(vv.$primary, 5%),
+ $button-primary-active: darken(vv.$primary, 10%),
+ $button-secondary: vv.$secondary,
+ $button-secondary-hover: darken(vv.$secondary, 5%),
+ $button-secondary-active: darken(vv.$secondary, 10%),
+ $button-tertiary: vv.$primary,
+ $button-tertiary-hover: darken(vv.$primary, 5%),
+ $button-tertiary-active: darken(vv.$primary, 10%)
);
+
@use '@carbon/styles';
@use '@carbon/type';
-@use '@carbon/colors';
-@use './src/styles/vendor/variables' as vv;
-
-/**********************************************************************************
-These are meant to be temporary style overrides.
-The sizing of some Carbon components clash with a requirement
-of one third party component - the data table - that needs
-to set the body's font-size at 12px.
-Once this component is removed we should be ok to remove the overrides below
-**********************************************************************************/
/******************************************
-Side nav
+Custom theme
******************************************/
+@forward './themes/content';
-$sidenav-block-size: 2.7rem;
-
-.cds--side-nav__submenu {
- block-size: $sidenav-block-size;
-}
-
-a.cds--side-nav__link {
- min-block-size: $sidenav-block-size;
-}
-
-.cds--side-nav__menu a.cds--side-nav__link {
- block-size: $sidenav-block-size;
-}
-
-.cds--side-nav__submenu-title,
-a.cds--side-nav__link > .cds--side-nav__link-text,
-a.cds--side-nav__link--active > .cds--side-nav__link-text,
-.cds--side-nav__item--active .cds--side-nav__icon > svg {
- color: vv.$body-bg-alt;
- fill: vv.$body-bg-alt;
- font-size: calc(type.type-scale(4) + 0.5px);
-}
-
-a.cds--header__menu-item,
-.cds--overflow-menu-options__btn,
-.cds--side-nav__menu
- a.cds--side-nav__link:not(.cds--side-nav__link--current):not([aria-current='page']):hover
- > span,
-.cds--side-nav__item:not(.cds--side-nav__item--active) > .cds--side-nav__link:hover > span,
-a.cds--header__menu-item:hover > svg {
- color: vv.$body-bg-alt;
- fill: vv.$body-bg-alt;
-
- &:hover,
- &:focus {
- color: vv.$body-bg-alt;
- fill: vv.$body-bg-alt;
- }
-}
-
-.cds--overflow-menu-options__option:hover,
-.cds--overflow-menu:hover,
-.cds--header__menu-title[aria-expanded='true'] + .cds--header__menu .cds--header__menu-item:hover {
- background-color: vv.$gray-600;
-}
-
-a.cds--side-nav__link[aria-current='page'] .cds--side-nav__link-text > span {
- color: vv.$body-bg-alt;
-}
+/******************************************
+Datatable
+******************************************/
-.cds--side-nav__icon > svg {
- block-size: 20px;
- inline-size: 20px;
+tr.cds--expandable-row > td:first-of-type {
+ background-color: vv.$white !important;
+ padding-inline-start: 1rem !important;
}
-.cds--side-nav--expanded {
- min-width: 20.8rem !important;
+th {
+ padding-block: 0 !important;
}
-.cds--side-nav__navigation {
- min-width: 4.2rem;
-}
+/******************************************
+Side nav
+******************************************/
.cds--side-nav__navigation {
left: -4.8rem;
left: 0;
transition: 250ms ease;
}
-/******************************************
-Header
-******************************************/
-$header-block-size: 3.9rem;
-
-a.cds--header__menu-item,
-.cds--header__action,
-.cds--header {
- block-size: $header-block-size;
- border: 0;
- font-size: calc(type.type-scale(4) + 0.5px);
-
- .cds--header__menu-trigger {
- border: 1px solid vv.$gray-700;
- }
-
- .cds--header__menu-trigger > svg {
- fill: vv.$body-bg-alt;
- }
-}
-
-button.cds--header__menu-trigger.cds--header__action.cds--header__menu-toggle {
- inline-size: $header-block-size;
-}
-
-button.cds--overflow-menu {
- block-size: $header-block-size;
- inline-size: calc($header-block-size - 1rem);
-}
-
-/******************************************
-Modals
-******************************************/
-
-.modal-dialog {
- margin-top: 5rem !important;
-}
/******************************************
Overflow menu
html,
body {
// WARNING: This was clashing with Carbon's font-size
- font-size: 12px;
+ // font-size: 12px;
height: 100%;
width: 100%;
}
--- /dev/null
+@use 'sass:map';
+@use '@carbon/styles/scss/theme' as t;
+@use '@carbon/styles/scss/compat/themes' as compat;
+@use './src/styles/vendor/variables' as vv;
+@use '@carbon/colors';
+
+/*
+Color documentation:
+https://carbondesignsystem.com/elements/color/overview/
+
+More info on color that can be overriden
+https://github.com/carbon-design-system/carbon/blob/main/packages/themes/docs/sass.md
+*/
+
+$content-theme: map-merge(
+ compat.$g10,
+ (
+ background-brand: vv.$secondary,
+ background-inverse: vv.$light,
+ background-inverse-hover: vv.$gray-300,
+ link-primary: vv.$primary,
+ link-primary-hover: vv.$secondary,
+ link-secondary: vv.$secondary,
+ link-inverse: vv.$secondary,
+ link-visited: vv.$secondary,
+ focus: vv.$primary,
+ focus-inset: lighten(vv.$primary, 45%),
+ focus-inverse: lighten(vv.$primary, 25%),
+ text-inverse: vv.$dark,
+ support-info: vv.$info,
+ layer-01: vv.$secondary,
+ layer-hover-01: vv.$gray-600,
+ text-primary: vv.$dark,
+ text-secondary: vv.$body-bg-alt,
+ text-disabled: vv.$gray-500,
+ icon-secondary: vv.$body-bg-alt
+ )
+);
+
+.content-theme {
+ @include t.theme($content-theme);
+}
+@use 'sass:map';
@use './src/styles/vendor/variables' as vv;
-$theme: (
+/*
+Color documentation:
+https://carbondesignsystem.com/elements/color/overview/
+
+More info on color that can be overriden
+https://github.com/carbon-design-system/carbon/blob/main/packages/themes/docs/sass.md
+*/
+
+$base: (
text-disabled: vv.$gray-500,
text-error: vv.$danger,
text-helper: vv.$body-color,
- text-inverse: vv.$white,
+ text-inverse: vv.$black,
text-on-color: vv.$white,
text-on-color-disabled: vv.$gray-700,
text-placeholder: vv.$gray-700,
+ text-primary: vv.$body-bg-alt,
+ text-secondary: vv.$body-bg-alt,
+ btn-primary: vv.$primary,
border-interactive: vv.$primary,
background: vv.$secondary,
layer-01: vv.$secondary,
- icon-primary: vv.$gray-900,
+ icon-primary: vv.$gray-100,
icon-secondary: vv.$gray-300,
link-primary: vv.$primary,
focus: vv.$primary,
heading-03: 1.75rem,
spacing-03: 0.5rem
);
+
+$layers: (
+ layer-hover-01: vv.$gray-600,
+ layer-hover-02: vv.$gray-100
+);
+
+$theme: map-merge($base, $layers);
[action: string]: { disabled: boolean; disableDesc: string };
}
) => {
- // click dropdown to update all actions buttons
- const dropDownToggle = fixture.debugElement.query(By.css('.dropdown-toggle'));
- dropDownToggle.triggerEventHandler('click', null);
- fixture.detectChanges();
- await fixture.whenStable();
-
+ const component = fixture.componentInstance;
+ const selection = component.selection;
const tableActionElement = fixture.debugElement.query(By.directive(TableActionsComponent));
- const toClassName = TestBed.inject(TableActionsComponent).toClassName;
- const getActionElement = (action: CdTableAction) =>
- tableActionElement.query(By.css(`[ngbDropdownItem].${toClassName(action)}`));
+ const tableActionComponent: TableActionsComponent = tableActionElement.componentInstance;
+ tableActionComponent.selection = selection;
const actions = {};
tableActions.forEach((action) => {
- const actionElement = getActionElement(action);
if (expectResult[action.name]) {
actions[action.name] = {
- disabled: actionElement.classes.disabled ? true : false,
- disableDesc: actionElement.properties.title
+ disabled: tableActionComponent.disableSelectionAction(action),
+ disableDesc: tableActionComponent.useDisableDesc(action) || ''
};
}
});