diff --git a/superset-frontend/cypress-base/cypress/integration/chart_list/filter.test.ts b/superset-frontend/cypress-base/cypress/integration/chart_list/filter.test.ts index 7bd0891cbf2..9ba7a4a29e8 100644 --- a/superset-frontend/cypress-base/cypress/integration/chart_list/filter.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/chart_list/filter.test.ts @@ -67,7 +67,7 @@ describe('Charts filters', () => { setFilter('Dashboards', 'Unicode Test'); cy.getBySel('styled-card').should('have.length', 1); setFilter('Dashboards', 'Tabbed Dashboard'); - cy.getBySel('styled-card').should('have.length', 8); + cy.getBySel('styled-card').should('have.length', 9); }); }); @@ -108,7 +108,7 @@ describe('Charts filters', () => { setFilter('Dashboards', 'Unicode Test'); cy.getBySel('table-row').should('have.length', 1); setFilter('Dashboards', 'Tabbed Dashboard'); - cy.getBySel('table-row').should('have.length', 8); + cy.getBySel('table-row').should('have.length', 9); }); }); }); diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/editmode.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/editmode.test.ts index f072bc8f54e..0fffc585952 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/editmode.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/editmode.test.ts @@ -16,11 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import { SAMPLE_DASHBOARD_1 } from 'cypress/utils/urls'; -import { drag, resize } from 'cypress/utils'; +import { SAMPLE_DASHBOARD_1, TABBED_DASHBOARD } from 'cypress/utils/urls'; +import { drag, resize, waitForChartLoad } from 'cypress/utils'; import * as ace from 'brace'; -import { interceptGet, interceptUpdate } from './utils'; -import { interceptFiltering as interceptCharts } from '../explore/utils'; +import { interceptGet, interceptUpdate, openTab } from './utils'; +import { + interceptExploreJson, + interceptFiltering as interceptCharts, +} from '../explore/utils'; function editDashboard() { cy.getBySel('edit-dashboard-button').click(); @@ -68,17 +71,47 @@ function discardChanges() { cy.getBySel('undo-action').click({ force: true }); } -function visitEdit() { +function visitEdit(sampleDashboard = SAMPLE_DASHBOARD_1) { interceptCharts(); interceptGet(); - cy.visit(SAMPLE_DASHBOARD_1); + cy.visit(sampleDashboard); cy.wait('@get'); editDashboard(); cy.wait('@filtering'); cy.wait(500); } +function resetTabbedDashboard(go = false) { + cy.getDashboard('tabbed_dash').then((r: Record) => { + const jsonMetadata = r?.json_metadata || '{}'; + const metadata = JSON.parse(jsonMetadata); + const resetMetadata = JSON.stringify({ + ...metadata, + color_scheme: '', + label_colors: {}, + shared_label_colors: {}, + }); + cy.updateDashboard(r.id, { + certification_details: r.certification_details, + certified_by: r.certified_by, + css: r.css, + dashboard_title: r.dashboard_title, + json_metadata: resetMetadata, + owners: r.owners, + slug: r.slug, + }).then(() => { + if (go) { + visitEdit(TABBED_DASHBOARD); + } + }); + }); +} + +function visitResetTabbedDashboard() { + resetTabbedDashboard(true); +} + function selectColorScheme(color: string) { cy.get( '[data-test="dashboard-edit-properties-form"] [aria-label="Select color scheme"]', @@ -111,6 +144,7 @@ function assertMetadata(text: string) { }); } function clearMetadata() { + cy.wait(500); cy.get('#json_metadata').then($jsonmetadata => { cy.wrap($jsonmetadata).type('{selectall} {backspace}'); }); @@ -122,11 +156,496 @@ function writeMetadata(metadata: string) { }); } +function openExplore(chartName: string) { + interceptExploreJson(); + + cy.get( + `[data-test-chart-name='${chartName}'] [aria-label='More Options']`, + ).click(); + cy.get('.ant-dropdown') + .not('.ant-dropdown-hidden') + .find("[role='menu'] [role='menuitem']") + .eq(2) + .should('contain', 'Edit chart') + .click(); + cy.wait('@getJson'); +} + describe('Dashboard edit', () => { beforeEach(() => { cy.preserveLogin(); }); + describe('Color consistency', () => { + beforeEach(() => { + visitResetTabbedDashboard(); + }); + + after(() => { + resetTabbedDashboard(); + }); + + it('should respect chart color scheme when none is set for the dashboard', () => { + openProperties(); + cy.get('[aria-label="Select color scheme"]').should('have.value', ''); + applyChanges(); + saveChanges(); + + // open nested tab + openTab(1, 1); + waitForChartLoad({ + name: 'Top 10 California Names Timeseries', + viz: 'line', + }); + + // label Anthony + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .first() + .should('have.css', 'fill', 'rgb(31, 168, 201)'); + }); + + it('should apply same color to same labels with color scheme set', () => { + openProperties(); + selectColorScheme('lyftColors'); + applyChanges(); + saveChanges(); + + // open nested tab + openTab(1, 1); + waitForChartLoad({ + name: 'Top 10 California Names Timeseries', + viz: 'line', + }); + + // label Anthony + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .first() + .should('have.css', 'fill', 'rgb(234, 11, 140)'); + + // open 2nd main tab + openTab(0, 1); + waitForChartLoad({ name: 'Trends', viz: 'line' }); + + // label Anthony + cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol') + .eq(2) + .should('have.css', 'fill', 'rgb(234, 11, 140)'); + }); + + it('should apply same color to same labels with no color scheme set', () => { + openProperties(); + cy.get('[aria-label="Select color scheme"]').should('have.value', ''); + applyChanges(); + saveChanges(); + + // open nested tab + openTab(1, 1); + waitForChartLoad({ + name: 'Top 10 California Names Timeseries', + viz: 'line', + }); + + // label Anthony + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .first() + .should('have.css', 'fill', 'rgb(31, 168, 201)'); + + // open 2nd main tab + openTab(0, 1); + waitForChartLoad({ name: 'Trends', viz: 'line' }); + + // label Anthony + cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol') + .eq(2) + .should('have.css', 'fill', 'rgb(31, 168, 201)'); + }); + + it('custom label colors should take the precedence in nested tabs', () => { + openProperties(); + openAdvancedProperties(); + clearMetadata(); + writeMetadata( + '{"color_scheme":"lyftColors","label_colors":{"Anthony":"red","Bangladesh":"red"}}', + ); + applyChanges(); + saveChanges(); + + // open nested tab + openTab(1, 1); + waitForChartLoad({ + name: 'Top 10 California Names Timeseries', + viz: 'line', + }); + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .first() + .should('have.css', 'fill', 'rgb(255, 0, 0)'); + + // open another nested tab + openTab(2, 1); + waitForChartLoad({ name: 'Growth Rate', viz: 'line' }); + cy.get('[data-test-chart-name="Growth Rate"] .line .nv-legend-symbol') + .first() + .should('have.css', 'fill', 'rgb(255, 0, 0)'); + }); + + it('label colors should take the precedence for rendered charts in nested tabs', () => { + // open the tab first time and let chart load + openTab(1, 1); + waitForChartLoad({ + name: 'Top 10 California Names Timeseries', + viz: 'line', + }); + + // go to previous tab + openTab(1, 0); + openProperties(); + openAdvancedProperties(); + clearMetadata(); + writeMetadata( + '{"color_scheme":"lyftColors","label_colors":{"Anthony":"red"}}', + ); + applyChanges(); + saveChanges(); + + // re-open the tab + openTab(1, 1); + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .first() + .should('have.css', 'fill', 'rgb(255, 0, 0)'); + }); + + it('should re-apply original color after removing custom label color with color scheme set', () => { + openProperties(); + openAdvancedProperties(); + clearMetadata(); + writeMetadata( + '{"color_scheme":"lyftColors","label_colors":{"Anthony":"red"}}', + ); + applyChanges(); + saveChanges(); + + openTab(1, 1); + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .first() + .should('have.css', 'fill', 'rgb(255, 0, 0)'); + + editDashboard(); + openProperties(); + openAdvancedProperties(); + clearMetadata(); + writeMetadata('{"color_scheme":"lyftColors","label_colors":{}}'); + applyChanges(); + saveChanges(); + + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .first() + .should('have.css', 'fill', 'rgb(234, 11, 140)'); + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .eq(1) + .should('have.css', 'fill', 'rgb(108, 131, 142)'); + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .eq(2) + .should('have.css', 'fill', 'rgb(41, 171, 226)'); + }); + + it('should re-apply original color after removing custom label color with no color scheme set', () => { + // open nested tab + openTab(1, 1); + waitForChartLoad({ + name: 'Top 10 California Names Timeseries', + viz: 'line', + }); + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .first() + .should('have.css', 'fill', 'rgb(31, 168, 201)'); + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .eq(1) + .should('have.css', 'fill', 'rgb(69, 78, 124)'); + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .eq(2) + .should('have.css', 'fill', 'rgb(90, 193, 137)'); + + openProperties(); + cy.get('[aria-label="Select color scheme"]').should('have.value', ''); + openAdvancedProperties(); + clearMetadata(); + writeMetadata('{"color_scheme":"","label_colors":{"Anthony":"red"}}'); + applyChanges(); + saveChanges(); + + openTab(1, 1); + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .first() + .should('have.css', 'fill', 'rgb(255, 0, 0)'); + + editDashboard(); + openProperties(); + openAdvancedProperties(); + clearMetadata(); + writeMetadata('{"color_scheme":"","label_colors":{}}'); + applyChanges(); + saveChanges(); + + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .first() + .should('have.css', 'fill', 'rgb(31, 168, 201)'); + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .eq(1) + .should('have.css', 'fill', 'rgb(69, 78, 124)'); + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .eq(2) + .should('have.css', 'fill', 'rgb(90, 193, 137)'); + }); + + it('should show the same colors in Explore', () => { + openProperties(); + openAdvancedProperties(); + clearMetadata(); + writeMetadata( + '{"color_scheme":"lyftColors","label_colors":{"Anthony":"red"}}', + ); + applyChanges(); + saveChanges(); + + // open nested tab + openTab(1, 1); + waitForChartLoad({ + name: 'Top 10 California Names Timeseries', + viz: 'line', + }); + + // label Anthony + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .first() + .should('have.css', 'fill', 'rgb(255, 0, 0)'); + // label Christopher + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .eq(1) + .should('have.css', 'fill', 'rgb(108, 131, 142)'); + + openExplore('Top 10 California Names Timeseries'); + + // label Anthony + cy.get('[data-test="chart-container"] .line .nv-legend-symbol') + .first() + .should('have.css', 'fill', 'rgb(255, 0, 0)'); + // label Christopher + cy.get('[data-test="chart-container"] .line .nv-legend-symbol') + .eq(1) + .should('have.css', 'fill', 'rgb(108, 131, 142)'); + }); + + it('should change color scheme multiple times', () => { + openProperties(); + selectColorScheme('lyftColors'); + applyChanges(); + saveChanges(); + + // open nested tab + openTab(1, 1); + waitForChartLoad({ + name: 'Top 10 California Names Timeseries', + viz: 'line', + }); + + // label Anthony + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .first() + .should('have.css', 'fill', 'rgb(234, 11, 140)'); + + // open 2nd main tab + openTab(0, 1); + waitForChartLoad({ name: 'Trends', viz: 'line' }); + + // label Anthony + cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol') + .eq(2) + .should('have.css', 'fill', 'rgb(234, 11, 140)'); + + editDashboard(); + openProperties(); + selectColorScheme('bnbColors'); + applyChanges(); + saveChanges(); + + // label Anthony + cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol') + .eq(2) + .should('have.css', 'fill', 'rgb(0, 122, 135)'); + + // open main tab and nested tab + openTab(0, 0); + openTab(1, 1); + + // label Anthony + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .first() + .should('have.css', 'fill', 'rgb(0, 122, 135)'); + }); + + it('should apply the color scheme across main tabs', () => { + openProperties(); + selectColorScheme('lyftColors'); + applyChanges(); + saveChanges(); + + cy.get('.treemap #rect-sum__SP_POP_TOTL').should( + 'have.css', + 'fill', + 'rgb(234, 11, 140)', + ); + + // go to second tab + openTab(0, 1); + waitForChartLoad({ name: 'Trends', viz: 'line' }); + + cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol') + .first() + .should('have.css', 'fill', 'rgb(234, 11, 140)'); + }); + + it('should apply the color scheme across main tabs for rendered charts', () => { + waitForChartLoad({ name: 'Treemap', viz: 'treemap' }); + openProperties(); + selectColorScheme('bnbColors'); + applyChanges(); + saveChanges(); + + cy.get('.treemap #rect-sum__SP_POP_TOTL').should( + 'have.css', + 'fill', + 'rgb(255, 90, 95)', + ); + + // go to second tab + openTab(0, 1); + waitForChartLoad({ name: 'Trends', viz: 'line' }); + + cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol') + .first() + .should('have.css', 'fill', 'rgb(255, 90, 95)'); + + // go back to first tab + openTab(0, 0); + + // change scheme now that charts are rendered across the main tabs + editDashboard(); + openProperties(); + selectColorScheme('lyftColors'); + applyChanges(); + saveChanges(); + + cy.get('.treemap #rect-sum__SP_POP_TOTL').should( + 'have.css', + 'fill', + 'rgb(234, 11, 140)', + ); + + // go to second tab again + openTab(0, 1); + + cy.get('[data-test-chart-name="Trends"] .line .nv-legend-symbol') + .first() + .should('have.css', 'fill', 'rgb(234, 11, 140)'); + }); + + it('should apply the color scheme in nested tabs', () => { + openProperties(); + selectColorScheme('lyftColors'); + applyChanges(); + saveChanges(); + cy.get('.treemap #rect-sum__SP_POP_TOTL').should( + 'have.css', + 'fill', + 'rgb(234, 11, 140)', + ); + + // open nested tab + openTab(1, 1); + waitForChartLoad({ + name: 'Top 10 California Names Timeseries', + viz: 'line', + }); + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .first() + .should('have.css', 'fill', 'rgb(234, 11, 140)'); + + // open another nested tab + openTab(2, 1); + waitForChartLoad({ name: 'Growth Rate', viz: 'line' }); + cy.get('[data-test-chart-name="Growth Rate"] .line .nv-legend-symbol') + .first() + .should('have.css', 'fill', 'rgb(234, 11, 140)'); + }); + + it('should apply a valid color scheme for rendered charts in nested tabs', () => { + // open the tab first time and let chart load + openTab(1, 1); + waitForChartLoad({ + name: 'Top 10 California Names Timeseries', + viz: 'line', + }); + + // go to previous tab + openTab(1, 0); + openProperties(); + selectColorScheme('lyftColors'); + applyChanges(); + saveChanges(); + + // re-open the tab + openTab(1, 1); + + cy.get( + '[data-test-chart-name="Top 10 California Names Timeseries"] .line .nv-legend-symbol', + ) + .first() + .should('have.css', 'fill', 'rgb(234, 11, 140)'); + }); + }); + describe('Edit properties', () => { before(() => { cy.createSampleDashboards(); @@ -266,39 +785,6 @@ describe('Dashboard edit', () => { }); }); - describe('Color schemes', () => { - beforeEach(() => { - cy.createSampleDashboards(); - visitEdit(); - }); - - it('should apply a valid color scheme', () => { - dragComponent('Top 10 California Names Timeseries'); - openProperties(); - selectColorScheme('lyftColors'); - applyChanges(); - saveChanges(); - cy.get('.line .nv-legend-symbol') - .first() - .should('have.css', 'fill', 'rgb(234, 11, 140)'); - }); - - it('label colors should take the precedence', () => { - dragComponent('Top 10 California Names Timeseries'); - openProperties(); - openAdvancedProperties(); - clearMetadata(); - writeMetadata( - '{"color_scheme":"lyftColors","label_colors":{"Anthony":"red"}}', - ); - applyChanges(); - saveChanges(); - cy.get('.line .nv-legend-symbol') - .first() - .should('have.css', 'fill', 'rgb(255, 0, 0)'); - }); - }); - describe('Save', () => { beforeEach(() => { cy.createSampleDashboards(); diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/utils.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/utils.ts index 3aa87ab0731..076b0438ae8 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/utils.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/utils.ts @@ -478,3 +478,12 @@ export function addCountryNameFilter() { testItems.topTenChart.filterColumn, ); } + +export function openTab(tabComponentIndex: number, tabIndex: number) { + return cy + .getBySel('dashboard-component-tabs') + .eq(tabComponentIndex) + .find('[role="tab"]') + .eq(tabIndex) + .click(); +} diff --git a/superset-frontend/cypress-base/cypress/integration/explore/utils.ts b/superset-frontend/cypress-base/cypress/integration/explore/utils.ts index 0972bac5527..99efedcd534 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/utils.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/utils.ts @@ -37,6 +37,10 @@ export function interceptPost() { cy.intercept('POST', `/api/v1/chart/`).as('post'); } +export function interceptExploreJson() { + cy.intercept('POST', `/superset/explore_json/**`).as('getJson'); +} + export function setFilter(filter: string, option: string) { interceptFiltering(); diff --git a/superset-frontend/cypress-base/cypress/support/index.d.ts b/superset-frontend/cypress-base/cypress/support/index.d.ts index c60580247e5..603c490ebfc 100644 --- a/superset-frontend/cypress-base/cypress/support/index.d.ts +++ b/superset-frontend/cypress-base/cypress/support/index.d.ts @@ -65,6 +65,7 @@ declare namespace Cypress { * Get */ getDashboards(): cy; + getDashboard(dashboardId: string | number): Record; getCharts(): cy; /** @@ -80,6 +81,11 @@ declare namespace Cypress { deleteDashboardByName(dashboardName: string, failOnStatusCode: boolean): cy; deleteChartByName(name: string, failOnStatusCode: boolean): cy; deleteChart(id: number, failOnStatusCode: boolean): cy; + + /** + * Update + */ + updateDashboard(dashboardId: number, body: Record): cy; } } diff --git a/superset-frontend/cypress-base/cypress/support/index.ts b/superset-frontend/cypress-base/cypress/support/index.ts index 80a51fc409b..aa2b168e2d0 100644 --- a/superset-frontend/cypress-base/cypress/support/index.ts +++ b/superset-frontend/cypress-base/cypress/support/index.ts @@ -332,6 +332,35 @@ Cypress.Commands.add('getDashboards', () => .then(resp => resp.body.result), ); +Cypress.Commands.add('getDashboard', (dashboardId: string | number) => + cy + .request({ + method: 'GET', + url: `api/v1/dashboard/${dashboardId}`, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${TokenName}`, + }, + }) + .then(resp => resp.body.result), +); + +Cypress.Commands.add( + 'updateDashboard', + (dashboardId: number, body: Record) => + cy + .request({ + method: 'PUT', + url: `api/v1/dashboard/${dashboardId}`, + body, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${TokenName}`, + }, + }) + .then(resp => resp.body.result), +); + Cypress.Commands.add('deleteChart', (id: number, failOnStatusCode = false) => cy .request({ diff --git a/superset/examples/tabbed_dashboard.py b/superset/examples/tabbed_dashboard.py index 7a167bc357d..58c0ba3e4c0 100644 --- a/superset/examples/tabbed_dashboard.py +++ b/superset/examples/tabbed_dashboard.py @@ -137,6 +137,25 @@ def load_tabbed_dashboard(_: bool = False) -> None: ], "type": "CHART" }, + "CHART-dxV7Il666H": { + "children": [], + "id": "CHART-dxV7Il666H", + "meta": { + "chartId": 5539, + "height": 50, + "sliceName": "Trends", + "width": 4 + }, + "parents": [ + "ROOT_ID", + "TABS-lV0r00f4H1", + "TAB-gcQJxApOZS", + "TABS-afnrUvdxYF", + "TAB-jNNd4WWar1", + "ROW-7ygtD666Q" + ], + "type": "CHART" + }, "CHART-jJ5Yj1Ptaz": { "children": [], "id": "CHART-jJ5Yj1Ptaz", @@ -238,6 +257,19 @@ def load_tabbed_dashboard(_: bool = False) -> None: ], "type": "ROW" }, + "ROW-7ygtD666Q": { + "children": ["CHART-dxV7Il666H"], + "id": "ROW-7ygtD666Q", + "meta": { "background": "BACKGROUND_TRANSPARENT" }, + "parents": [ + "ROOT_ID", + "TABS-lV0r00f4H1", + "TAB-gcQJxApOZS", + "TABS-afnrUvdxYF", + "TAB-jNNd4WWar1" + ], + "type": "ROW" + }, "ROW-DnYkJgKQE": { "children": ["CHART-06Kg-rUggO", "CHART-E4rQMdzY9-"], "id": "ROW-DnYkJgKQE", @@ -386,7 +418,7 @@ def load_tabbed_dashboard(_: bool = False) -> None: "type": "TAB" }, "TAB-jNNd4WWar1": { - "children": ["ROW-7ygtDczaQ"], + "children": ["ROW-7ygtDczaQ", "ROW-7ygtD666Q"], "id": "TAB-jNNd4WWar1", "meta": { "text": "New Tab" }, "parents": [