Merge pull request #4608 from Budibase/table-cell-config

Table V2
This commit is contained in:
Andrew Kingston 2022-02-23 14:46:27 +00:00 committed by GitHub
commit b2ab0954fc
26 changed files with 2374 additions and 1028 deletions

View File

@ -38,6 +38,7 @@
], ],
"dependencies": { "dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1", "@adobe/spectrum-css-workflow-icons": "^1.2.1",
"@budibase/string-templates": "^1.0.72-alpha.0",
"@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2", "@spectrum-css/avatar": "^3.0.2",

View File

@ -10,6 +10,7 @@
export let value export let value
export let size = "M" export let size = "M"
export let spectrumTheme export let spectrumTheme
export let alignRight = false
let open = false let open = false
@ -133,6 +134,7 @@
use:clickOutside={() => (open = false)} use:clickOutside={() => (open = false)}
transition:fly={{ y: -20, duration: 200 }} transition:fly={{ y: -20, duration: 200 }}
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
class:spectrum-Popover--align-right={alignRight}
> >
{#each categories as category} {#each categories as category}
<div class="category"> <div class="category">
@ -250,6 +252,9 @@
align-items: stretch; align-items: stretch;
gap: var(--spacing-xl); gap: var(--spacing-xl);
} }
.spectrum-Popover--align-right {
right: 0;
}
.colors { .colors {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;

View File

@ -1,5 +1,4 @@
<script> <script>
import { slide } from "svelte/transition"
import Portal from "svelte-portal" import Portal from "svelte-portal"
import Button from "../Button/Button.svelte" import Button from "../Button/Button.svelte"
import Body from "../Typography/Body.svelte" import Body from "../Typography/Body.svelte"
@ -7,7 +6,9 @@
export let title export let title
export let fillWidth export let fillWidth
let visible = false let visible = false
export function show() { export function show() {
if (visible) { if (visible) {
return return
@ -21,11 +22,27 @@
} }
visible = false visible = false
} }
const easeInOutQuad = x => {
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2
}
// Use a custom svelte transition here because the built-in slide
// transition has a horrible overshoot
const slide = () => {
return {
duration: 360,
css: t => {
const translation = 100 - Math.round(easeInOutQuad(t) * 100)
return `transform: translateY(${translation}%);`
},
}
}
</script> </script>
{#if visible} {#if visible}
<Portal> <Portal>
<section class:fillWidth class="drawer" transition:slide> <section class:fillWidth class="drawer" transition:slide|local>
<header> <header>
<div class="text"> <div class="text">
<Heading size="XS">{title}</Heading> <Heading size="XS">{title}</Heading>

View File

@ -17,14 +17,16 @@
{#each attachments as attachment} {#each attachments as attachment}
{#if isImage(attachment.extension)} {#if isImage(attachment.extension)}
<Link quiet target="_blank" href={attachment.url}> <Link quiet target="_blank" href={attachment.url}>
<img src={attachment.url} alt={attachment.extension} /> <div class="center">
<img src={attachment.url} alt={attachment.extension} />
</div>
</Link> </Link>
{:else} {:else}
<Tooltip text={attachment.name} direction="right"> <Tooltip text={attachment.name} direction="right">
<div class="file"> <div class="file">
<Link quiet target="_blank" href={attachment.url} <Link quiet target="_blank" href={attachment.url}>
>{attachment.extension}</Link {attachment.extension}
> </Link>
</div> </div>
</Tooltip> </Tooltip>
{/if} {/if}
@ -38,12 +40,15 @@
height: 32px; height: 32px;
max-width: 64px; max-width: 64px;
} }
.center,
.file { .file {
height: 32px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
}
.file {
height: 32px;
padding: 0 8px; padding: 0 8px;
color: var(--spectrum-global-color-gray-800); color: var(--spectrum-global-color-gray-800);
border: 1px solid var(--spectrum-global-color-gray-300); border: 1px solid var(--spectrum-global-color-gray-300);

View File

@ -7,5 +7,9 @@
<style> <style>
.bold { .bold {
font-weight: bold; font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: var(--max-cell-width);
} }
</style> </style>

View File

@ -6,6 +6,7 @@
import AttachmentRenderer from "./AttachmentRenderer.svelte" import AttachmentRenderer from "./AttachmentRenderer.svelte"
import ArrayRenderer from "./ArrayRenderer.svelte" import ArrayRenderer from "./ArrayRenderer.svelte"
import InternalRenderer from "./InternalRenderer.svelte" import InternalRenderer from "./InternalRenderer.svelte"
import { processStringSync } from "@budibase/string-templates"
export let row export let row
export let schema export let schema
@ -28,10 +29,33 @@
$: type = schema?.type ?? "string" $: type = schema?.type ?? "string"
$: customRenderer = customRenderers?.find(x => x.column === schema?.name) $: customRenderer = customRenderers?.find(x => x.column === schema?.name)
$: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer $: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer
$: width = schema?.width || "150px"
$: cellValue = getCellValue(value, schema.template)
const getCellValue = (value, template) => {
if (!template) {
return value
}
return processStringSync(template, { value })
}
</script> </script>
{#if renderer && (customRenderer || (value != null && value !== ""))} {#if renderer && (customRenderer || (cellValue != null && cellValue !== ""))}
<svelte:component this={renderer} {row} {schema} {value} on:clickrelationship> <div style="--max-cell-width: {schema.width ? 'none' : '200px'};">
<slot /> <svelte:component
</svelte:component> this={renderer}
{row}
{schema}
value={cellValue}
on:clickrelationship
>
<slot />
</svelte:component>
</div>
{/if} {/if}
<style>
div {
display: contents;
}
</style>

View File

@ -3,3 +3,12 @@
</script> </script>
<code>{value}</code> <code>{value}</code>
<style>
code {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: var(--max-cell-width);
}
</style>

View File

@ -17,6 +17,8 @@
<style> <style>
div { div {
width: 200px; overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
</style> </style>

View File

@ -43,11 +43,3 @@
<div on:click|stopPropagation={onClick}> <div on:click|stopPropagation={onClick}>
<Icon size="S" name="Copy" /> <Icon size="S" name="Copy" />
</div> </div>
<style>
div {
overflow: hidden;
text-overflow: ellipsis;
width: 150px;
}
</style>

View File

@ -8,6 +8,7 @@
div { div {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
width: 150px; white-space: nowrap;
max-width: var(--max-cell-width);
} }
</style> </style>

View File

@ -4,6 +4,7 @@
import CellRenderer from "./CellRenderer.svelte" import CellRenderer from "./CellRenderer.svelte"
import SelectEditRenderer from "./SelectEditRenderer.svelte" import SelectEditRenderer from "./SelectEditRenderer.svelte"
import { cloneDeep, deepGet } from "../helpers" import { cloneDeep, deepGet } from "../helpers"
import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte"
/** /**
* The expected schema is our normal couch schemas for our tables. * The expected schema is our normal couch schemas for our tables.
@ -14,6 +15,11 @@
* sortable: Set to false to disable sorting data by a certain column * sortable: Set to false to disable sorting data by a certain column
* editable: Set to false to disable editing a certain column if the * editable: Set to false to disable editing a certain column if the
* allowEditColumns prop is true * allowEditColumns prop is true
* width: the width of the column
* align: the alignment of the column
* template: a HBS or JS binding to use as the value
* background: the background color
* color: the text color
*/ */
export let data = [] export let data = []
export let schema = {} export let schema = {}
@ -28,13 +34,14 @@
export let editColumnTitle = "Edit" export let editColumnTitle = "Edit"
export let customRenderers = [] export let customRenderers = []
export let disableSorting = false export let disableSorting = false
export let autoSortColumns = true
export let compact = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
// Config // Config
const rowHeight = 55
const headerHeight = 36 const headerHeight = 36
const rowPreload = 5 $: rowHeight = compact ? 46 : 55
// Sorting state // Sorting state
let sortColumn let sortColumn
@ -45,32 +52,20 @@
let loaded = false let loaded = false
$: schema = fixSchema(schema) $: schema = fixSchema(schema)
$: if (!loading) loaded = true $: if (!loading) loaded = true
$: rows = data ?? [] $: fields = getFields(schema, showAutoColumns, autoSortColumns)
$: visibleRowCount = getVisibleRowCount(loaded, height, rows.length, rowCount) $: rows = fields?.length ? data || [] : []
$: contentStyle = getContentStyle(visibleRowCount, rowCount) $: visibleRowCount = getVisibleRowCount(
$: sortedRows = sortRows(rows, sortColumn, sortOrder) loaded,
$: fields = getFields(schema, showAutoColumns) height,
$: showEditColumn = allowEditRows || allowSelectRows rows.length,
rowCount,
// Scrolling state rowHeight
let timeout
let nextScrollTop = 0
let scrollTop = 0
$: firstVisibleRow = calculateFirstVisibleRow(scrollTop)
$: lastVisibleRow = calculateLastVisibleRow(
firstVisibleRow,
visibleRowCount,
rows.length
) )
$: contentStyle = getContentStyle(visibleRowCount, rowCount, rowHeight)
// Reset state when data changes $: sortedRows = sortRows(rows, sortColumn, sortOrder)
$: rows.length, reset() $: gridStyle = getGridStyle(fields, schema, showEditColumn)
const reset = () => { $: showEditColumn = allowEditRows || allowSelectRows
nextScrollTop = 0 $: cellStyles = computeCellStyles(schema)
scrollTop = 0
clearTimeout(timeout)
timeout = null
}
const fixSchema = schema => { const fixSchema = schema => {
let fixedSchema = {} let fixedSchema = {}
@ -90,7 +85,7 @@
return fixedSchema return fixedSchema
} }
const getVisibleRowCount = (loaded, height, allRows, rowCount) => { const getVisibleRowCount = (loaded, height, allRows, rowCount, rowHeight) => {
if (!loaded) { if (!loaded) {
return rowCount || 0 return rowCount || 0
} }
@ -100,11 +95,28 @@
return Math.min(allRows, Math.ceil(height / rowHeight)) return Math.min(allRows, Math.ceil(height / rowHeight))
} }
const getContentStyle = (visibleRows, rowCount) => { const getContentStyle = (visibleRows, rowCount, rowHeight) => {
if (!rowCount || !visibleRows) { if (!rowCount || !visibleRows) {
return "" return ""
} }
return `height: ${headerHeight + visibleRows * (rowHeight + 1)}px;` return `height: ${headerHeight + visibleRows * rowHeight}px;`
}
const getGridStyle = (fields, schema, showEditColumn) => {
let style = "grid-template-columns:"
if (showEditColumn) {
style += " auto"
}
fields?.forEach(field => {
const fieldSchema = schema[field]
if (fieldSchema.width) {
style += ` ${fieldSchema.width}`
} else {
style += " minmax(auto, 1fr)"
}
})
style += ";"
return style
} }
const sortRows = (rows, sortColumn, sortOrder) => { const sortRows = (rows, sortColumn, sortOrder) => {
@ -143,14 +155,14 @@
return name || "" return name || ""
} }
const getFields = (schema, showAutoColumns) => { const getFields = (schema, showAutoColumns, autoSortColumns) => {
let columns = [] let columns = []
let autoColumns = [] let autoColumns = []
Object.entries(schema || {}).forEach(([field, fieldSchema]) => { Object.entries(schema || {}).forEach(([field, fieldSchema]) => {
if (!field || !fieldSchema) { if (!field || !fieldSchema) {
return return
} }
if (!fieldSchema?.autocolumn) { if (!autoSortColumns || !fieldSchema?.autocolumn) {
columns.push(fieldSchema) columns.push(fieldSchema)
} else if (showAutoColumns) { } else if (showAutoColumns) {
autoColumns.push(fieldSchema) autoColumns.push(fieldSchema)
@ -171,28 +183,6 @@
.map(column => column.name) .map(column => column.name)
} }
const onScroll = event => {
nextScrollTop = event.target.scrollTop
if (timeout) {
return
}
timeout = setTimeout(() => {
scrollTop = nextScrollTop
timeout = null
}, 50)
}
const calculateFirstVisibleRow = scrollTop => {
return Math.max(Math.floor(scrollTop / (rowHeight + 1)) - rowPreload, 0)
}
const calculateLastVisibleRow = (firstRow, visibleRowCount, allRowCount) => {
if (visibleRowCount === 0) {
return -1
}
return Math.min(firstRow + visibleRowCount + 2 * rowPreload, allRowCount)
}
const editColumn = (e, field) => { const editColumn = (e, field) => {
e.stopPropagation() e.stopPropagation()
dispatch("editcolumn", field) dispatch("editcolumn", field)
@ -213,170 +203,233 @@
selectedRows = [...selectedRows, row] selectedRows = [...selectedRows, row]
} }
} }
const computeCellStyles = schema => {
let styles = {}
Object.keys(schema || {}).forEach(field => {
styles[field] = ""
if (schema[field].color) {
styles[field] += `color: ${schema[field].color};`
}
if (schema[field].background) {
styles[field] += `background-color: ${schema[field].background};`
}
if (schema[field].align === "Center") {
styles[field] += "justify-content: center; text-align: center;"
}
if (schema[field].align === "Right") {
styles[field] += "justify-content: flex-end; text-align: right;"
}
})
return styles
}
</script> </script>
<div class="wrapper" bind:offsetHeight={height}> <div
class="wrapper"
class:wrapper--quiet={quiet}
class:wrapper--compact={compact}
bind:offsetHeight={height}
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
>
{#if !loaded} {#if !loaded}
<div class="loading" style={contentStyle} /> <div class="loading" style={contentStyle}>
<ProgressCircle />
</div>
{:else} {:else}
<div <div class="spectrum-Table" style={`${contentStyle}${gridStyle}`}>
on:scroll={onScroll} {#if fields.length}
class:quiet <div class="spectrum-Table-head">
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`} {#if showEditColumn}
class="container" <div
> class="spectrum-Table-headCell spectrum-Table-headCell--divider spectrum-Table-headCell--edit"
<div style={contentStyle}> >
<table class="spectrum-Table" class:spectrum-Table--quiet={quiet}> {editColumnTitle || ""}
{#if fields.length} </div>
<thead class="spectrum-Table-head">
<tr>
{#if showEditColumn}
<th class="spectrum-Table-headCell">
<div class="spectrum-Table-headCell-content">
{editColumnTitle || ""}
</div>
</th>
{/if}
{#each fields as field}
<th
class="spectrum-Table-headCell"
class:is-sortable={schema[field].sortable !== false}
class:is-sorted-desc={sortColumn === field &&
sortOrder === "Descending"}
class:is-sorted-asc={sortColumn === field &&
sortOrder === "Ascending"}
on:click={() => sortBy(schema[field])}
>
<div class="spectrum-Table-headCell-content">
<div class="title">{getDisplayName(schema[field])}</div>
{#if schema[field]?.autocolumn}
<svg
class="spectrum-Icon spectrum-Table-autoIcon"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-MagicWand" />
</svg>
{/if}
{#if sortColumn === field}
<svg
class="spectrum-Icon spectrum-UIIcon-ArrowDown100 spectrum-Table-sortedIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-css-icon-Arrow100" />
</svg>
{/if}
{#if allowEditColumns && schema[field]?.editable !== false}
<svg
class="spectrum-Icon spectrum-Table-editIcon"
focusable="false"
on:click={e => editColumn(e, field)}
>
<use xlink:href="#spectrum-icon-18-Edit" />
</svg>
{/if}
</div>
</th>
{/each}
</tr>
</thead>
{/if} {/if}
<tbody class="spectrum-Table-body"> {#each fields as field}
{#if sortedRows?.length && fields.length} <div
{#each sortedRows as row, idx} class="spectrum-Table-headCell"
<tr class:spectrum-Table-headCell--alignCenter={schema[field]
on:click={() => dispatch("click", row)} .align === "Center"}
on:click={() => toggleSelectRow(row)} class:spectrum-Table-headCell--alignRight={schema[field].align ===
class="spectrum-Table-row" "Right"}
class:hidden={idx < firstVisibleRow || idx > lastVisibleRow} class:is-sortable={schema[field].sortable !== false}
class:is-sorted-desc={sortColumn === field &&
sortOrder === "Descending"}
class:is-sorted-asc={sortColumn === field &&
sortOrder === "Ascending"}
on:click={() => sortBy(schema[field])}
>
<div class="title">{getDisplayName(schema[field])}</div>
{#if schema[field]?.autocolumn}
<svg
class="spectrum-Icon spectrum-Table-autoIcon"
focusable="false"
> >
{#if idx >= firstVisibleRow && idx <= lastVisibleRow} <use xlink:href="#spectrum-icon-18-MagicWand" />
{#if showEditColumn} </svg>
<td {/if}
class="spectrum-Table-cell spectrum-Table-cell--divider" {#if sortColumn === field}
> <svg
<div class="spectrum-Table-cell-content"> class="spectrum-Icon spectrum-UIIcon-ArrowDown100 spectrum-Table-sortedIcon"
<SelectEditRenderer focusable="false"
data={row} aria-hidden="true"
selected={selectedRows.includes(row)} >
onToggleSelection={() => toggleSelectRow(row)} <use xlink:href="#spectrum-css-icon-Arrow100" />
onEdit={e => editRow(e, row)} </svg>
{allowSelectRows} {/if}
{allowEditRows} {#if allowEditColumns && schema[field]?.editable !== false}
/> <svg
</div> class="spectrum-Icon spectrum-Table-editIcon"
</td> focusable="false"
{/if} on:click={e => editColumn(e, field)}
{#each fields as field} >
<td <use xlink:href="#spectrum-icon-18-Edit" />
class="spectrum-Table-cell" </svg>
class:spectrum-Table-cell--divider={!!schema[field] {/if}
.divider} </div>
> {/each}
<div class="spectrum-Table-cell-content"> </div>
<CellRenderer {/if}
{customRenderers} {#if sortedRows?.length}
{row} {#each sortedRows as row, idx}
schema={schema[field]} <div
value={deepGet(row, field)} class="spectrum-Table-row"
on:clickrelationship on:click={() => dispatch("click", row)}
> on:click={() => toggleSelectRow(row)}
<slot /> >
</CellRenderer> {#if showEditColumn}
</div> <div
</td> class="spectrum-Table-cell spectrum-Table-cell--divider spectrum-Table-cell--edit"
{/each} >
{/if} <SelectEditRenderer
</tr> data={row}
{/each} selected={selectedRows.includes(row)}
{:else} onToggleSelection={() => toggleSelectRow(row)}
<tr class="placeholder-row"> onEdit={e => editRow(e, row)}
{#if showEditColumn} {allowSelectRows}
<td class="placeholder-offset" /> {allowEditRows}
{/if} />
{#each fields as field} </div>
<td />
{/each}
<div class="placeholder" class:has-fields={fields.length > 0}>
<div class="placeholder-content">
<svg
class="spectrum-Icon spectrum-Icon--sizeXXL"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-Table" />
</svg>
<div>No rows found</div>
</div>
</div>
</tr>
{/if} {/if}
</tbody> {#each fields as field}
</table> <div
</div> class="spectrum-Table-cell"
class:spectrum-Table-cell--divider={!!schema[field].divider}
style={cellStyles[field]}
>
<CellRenderer
{customRenderers}
{row}
schema={schema[field]}
value={deepGet(row, field)}
on:clickrelationship
>
<slot />
</CellRenderer>
</div>
{/each}
</div>
{/each}
{:else}
<div class="placeholder" class:placeholder--no-fields={!fields?.length}>
<div class="placeholder-content">
<svg class="spectrum-Icon spectrum-Icon--sizeXXL" focusable="false">
<use xlink:href="#spectrum-icon-18-Table" />
</svg>
<div>No rows found</div>
</div>
</div>
{/if}
</div> </div>
{/if} {/if}
</div> </div>
<style> <style>
/* Wrapper */
.wrapper { .wrapper {
background-color: var(--spectrum-alias-background-color-secondary);
overflow: hidden;
position: relative; position: relative;
z-index: 0; z-index: 0;
--table-bg: var(--spectrum-global-color-gray-50);
--table-border: 1px solid var(--spectrum-alias-border-color-mid);
--cell-padding: var(--spectrum-global-dimension-size-250);
}
.wrapper--quiet {
--table-bg: var(--spectrum-alias-background-color-transparent);
}
.wrapper--compact {
--cell-padding: var(--spectrum-global-dimension-size-150);
} }
.container { /* Loading */
height: 100%; .loading {
position: relative; display: grid;
place-items: center;
min-height: 100px;
}
/* Table */
.spectrum-Table {
width: 100%;
border-radius: 0;
display: grid;
overflow: auto; overflow: auto;
} }
.container.quiet {
border: none;
}
table {
width: 100%;
}
/* Header */
.spectrum-Table-head {
display: contents;
}
.spectrum-Table-head > :first-child {
border-left: 1px solid transparent;
padding-left: var(--cell-padding);
}
.spectrum-Table-head > :last-child {
border-right: 1px solid transparent;
padding-right: var(--cell-padding);
}
.spectrum-Table-headCell {
height: var(--header-height);
position: sticky;
top: 0;
text-overflow: ellipsis;
white-space: nowrap;
background-color: var(--spectrum-alias-background-color-secondary);
z-index: 2;
border-bottom: var(--table-border);
padding: 0 calc(var(--cell-padding) / 1.33);
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
user-select: none;
}
.spectrum-Table-headCell--alignCenter {
justify-content: center;
}
.spectrum-Table-headCell--alignRight {
justify-content: flex-end;
}
.spectrum-Table-headCell--divider {
padding-right: var(--cell-padding);
}
.spectrum-Table-headCell--divider + .spectrum-Table-headCell {
padding-left: var(--cell-padding);
}
.spectrum-Table-headCell--edit {
position: sticky;
left: 0;
z-index: 3;
}
.spectrum-Table-headCell .title {
overflow: hidden;
text-overflow: ellipsis;
}
.spectrum-Table-headCell:hover .spectrum-Table-editIcon {
opacity: 1;
transition: opacity 0.2s ease;
}
.spectrum-Table-headCell .spectrum-Icon { .spectrum-Table-headCell .spectrum-Icon {
pointer-events: all; pointer-events: all;
margin-left: var( margin-left: var(
@ -392,63 +445,93 @@
.spectrum-Table-editIcon { .spectrum-Table-editIcon {
opacity: 0; opacity: 0;
} }
.spectrum-Table-headCell:hover .spectrum-Table-editIcon {
opacity: 1; /* Table rows */
transition: opacity 0.2s ease; .spectrum-Table-row {
display: contents;
}
.spectrum-Table-row:hover .spectrum-Table-cell {
/*background-color: var(--hover-bg) !important;*/
}
.spectrum-Table-row:hover .spectrum-Table-cell:after {
background-color: var(--spectrum-alias-highlight-hover);
}
.wrapper--quiet .spectrum-Table-row {
border-left: none;
border-right: none;
}
.spectrum-Table-row > :first-child {
border-left: var(--table-border);
padding-left: var(--cell-padding);
}
.spectrum-Table-row > :last-child {
border-right: var(--table-border);
padding-right: var(--cell-padding);
} }
th { /* Table cells */
vertical-align: middle; .spectrum-Table-cell {
height: var(--header-height); flex: 1 1 auto;
position: sticky; padding: 0 calc(var(--cell-padding) / 1.33);
top: 0; border-top: none;
z-index: 2; border-bottom: none;
background-color: var(--spectrum-alias-background-color-secondary); border-radius: 0;
border-bottom: 1px solid text-overflow: ellipsis;
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
}
.spectrum-Table-headCell-content {
white-space: nowrap; white-space: nowrap;
height: var(--row-height);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
user-select: none; gap: 4px;
border-bottom: 1px solid var(--spectrum-alias-border-color-mid);
background-color: var(--table-bg);
z-index: 1;
} }
.spectrum-Table-headCell-content .title { .spectrum-Table-cell--divider {
overflow: hidden; padding-right: var(--cell-padding);
text-overflow: ellipsis; }
.spectrum-Table-cell--divider + .spectrum-Table-cell {
padding-left: var(--cell-padding);
}
.spectrum-Table-cell--edit {
position: sticky;
left: 0;
z-index: 2;
}
.spectrum-Table-cell:after {
content: "";
position: absolute;
width: 100%;
height: 100%;
background-color: transparent;
top: 0;
left: 0;
pointer-events: none;
transition: background-color
var(--spectrum-global-animation-duration-100, 0.13s) ease-in-out;
} }
.placeholder-row { /* Placeholder */
position: relative;
height: 150px;
}
.placeholder-row td {
border-top: none !important;
border-bottom: none !important;
}
.placeholder-offset {
width: 1px;
}
.placeholder { .placeholder {
top: 0;
height: 100%;
left: 0;
width: 100%;
position: absolute;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border: var(--table-border);
border-top: none;
grid-column: 1 / -1;
background-color: var(--table-bg);
} }
.placeholder.has-fields { .placeholder--no-fields {
top: var(--header-height); border-top: var(--table-border);
height: calc(100% - var(--header-height)); }
.wrapper--quiet .placeholder {
border-left: none;
border-right: none;
} }
.placeholder-content { .placeholder-content {
padding: 20px; padding: 40px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
@ -466,41 +549,4 @@
); );
text-align: center; text-align: center;
} }
tbody {
z-index: 1;
}
tbody tr {
height: var(--row-height);
}
tbody tr.hidden {
height: calc(var(--row-height) + 1px);
}
td {
padding-top: 0;
padding-bottom: 0;
border-bottom: none;
border-top: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
border-radius: 0;
}
tr:first-child td {
border-top: none;
}
tr:last-child td {
border-bottom: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
}
td.spectrum-Table-cell--divider {
width: 1px;
}
.spectrum-Table-cell-content {
height: var(--row-height);
white-space: nowrap;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: 4px;
}
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import filterTests from "../support/filterTests" import filterTests from "../support/filterTests"
filterTests(['smoke', 'all'], () => { filterTests(["smoke", "all"], () => {
context("Create a Table", () => { context("Create a Table", () => {
before(() => { before(() => {
cy.login() cy.login()
@ -55,7 +55,7 @@ filterTests(['smoke', 'all'], () => {
it("Adds 15 rows and checks pagination", () => { it("Adds 15 rows and checks pagination", () => {
// 10 rows per page, 15 rows should create 2 pages within table // 10 rows per page, 15 rows should create 2 pages within table
const totalRows = 16 const totalRows = 16
for (let i = 1; i < totalRows; i++){ for (let i = 1; i < totalRows; i++) {
cy.addRow([i]) cy.addRow([i])
} }
cy.wait(1000) cy.wait(1000)
@ -71,14 +71,14 @@ filterTests(['smoke', 'all'], () => {
// Delete rows, removing second page of rows from table // Delete rows, removing second page of rows from table
const deleteRows = 5 const deleteRows = 5
cy.get(".spectrum-Checkbox-input").check({ force: true }) cy.get(".spectrum-Checkbox-input").check({ force: true })
cy.get(".spectrum-Table-body") cy.get(".spectrum-Table")
cy.contains("Delete 5 row(s)").click() cy.contains("Delete 5 row(s)").click()
cy.get(".spectrum-Modal").contains("Delete").click() cy.get(".spectrum-Modal").contains("Delete").click()
cy.wait(1000) cy.wait(1000)
// Confirm table only has one page // Confirm table only has one page
cy.get(".spectrum-Pagination").within(() => { cy.get(".spectrum-Pagination").within(() => {
cy.get(".spectrum-ActionButton").eq(1).should('not.be.enabled') cy.get(".spectrum-ActionButton").eq(1).should("not.be.enabled")
}) })
}) })
} }

View File

@ -1,7 +1,6 @@
import filterTests from "../support/filterTests" import filterTests from "../support/filterTests"
filterTests(["smoke", "all"], () => {
filterTests(['smoke', 'all'], () => {
context("Create a User and Assign Roles", () => { context("Create a User and Assign Roles", () => {
before(() => { before(() => {
cy.login() cy.login()
@ -9,7 +8,7 @@ filterTests(['smoke', 'all'], () => {
it("should create a user", () => { it("should create a user", () => {
cy.createUser("bbuser@test.com") cy.createUser("bbuser@test.com")
cy.get(".spectrum-Table-body").should('contain', 'bbuser') cy.get(".spectrum-Table").should("contain", "bbuser")
}) })
it("should confirm there is No Access for a New User", () => { it("should confirm there is No Access for a New User", () => {
@ -17,24 +16,24 @@ filterTests(['smoke', 'all'], () => {
cy.contains("bbuser").click() cy.contains("bbuser").click()
cy.wait(500) cy.wait(500)
// Get No Access table - Confirm it has apps in it // Get No Access table - Confirm it has apps in it
cy.get(".spectrum-Table").eq(1).should('not.contain', 'No rows found') cy.get(".spectrum-Table").eq(1).should("not.contain", "No rows found")
// Get Configure Roles table - Confirm it has no apps // Get Configure Roles table - Confirm it has no apps
cy.get(".spectrum-Table").eq(0).contains('No rows found') cy.get(".spectrum-Table").eq(0).contains("No rows found")
}) })
it("should assign role types", () => { it("should assign role types", () => {
// 3 apps minimum required - to assign an app to each role type // 3 apps minimum required - to assign an app to each role type
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
.its("body") .its("body")
.then(val => { .then(val => {
if (val.length < 3) { if (val.length < 3) {
for (let i = 1; i < 3; i++) { for (let i = 1; i < 3; i++) {
const uuid = () => Cypress._.random(0, 1e6) const uuid = () => Cypress._.random(0, 1e6)
const name = uuid() const name = uuid()
cy.createApp(name) cy.createApp(name)
}
} }
} })
})
// Navigate back to the user // Navigate back to the user
cy.visit(`${Cypress.config().baseUrl}/builder`) cy.visit(`${Cypress.config().baseUrl}/builder`)
cy.wait(500) cy.wait(500)
@ -43,90 +42,139 @@ filterTests(['smoke', 'all'], () => {
cy.get(".spectrum-Table").contains("bbuser").click() cy.get(".spectrum-Table").contains("bbuser").click()
cy.wait(1000) cy.wait(1000)
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
cy.get(".spectrum-Table-body").eq(1).find('tr').eq(0).click() cy.get(".spectrum-Table")
.eq(1)
.find(".spectrum-Table-row")
.eq(0)
.find(".spectrum-Table-cell")
.eq(0)
.click()
cy.wait(500) cy.wait(500)
cy.get(".spectrum-Dialog-grid").contains("Choose an option").click().then(() => { cy.get(".spectrum-Dialog-grid")
cy.wait(1000) .contains("Choose an option")
if (i == 0) { .click()
cy.get(".spectrum-Popover").contains("Admin").click() .then(() => {
} cy.wait(1000)
if (i == 1) { if (i == 0) {
cy.get(".spectrum-Popover").contains("Power").click() cy.get(".spectrum-Popover").contains("Admin").click()
} }
if (i == 2) { if (i == 1) {
cy.get(".spectrum-Popover").contains("Basic").click() cy.get(".spectrum-Popover").contains("Power").click()
} }
cy.wait(1000) if (i == 2) {
cy.get(".spectrum-Button").contains("Update role").click({ force: true }) cy.get(".spectrum-Popover").contains("Basic").click()
}
cy.wait(1000)
cy.get(".spectrum-Button")
.contains("Update role")
.click({ force: true })
}) })
} }
// Confirm roles exist within Configure roles table // Confirm roles exist within Configure roles table
cy.wait(2000) cy.wait(2000)
cy.get(".spectrum-Table-body").eq(0).within((assginedRoles) => { cy.get(".spectrum-Table")
.eq(0)
.within(assginedRoles => {
expect(assginedRoles).to.contain("Admin") expect(assginedRoles).to.contain("Admin")
expect(assginedRoles).to.contain("Power") expect(assginedRoles).to.contain("Power")
expect(assginedRoles).to.contain("Basic") expect(assginedRoles).to.contain("Basic")
}) })
}) })
it("should unassign role types", () => { it("should unassign role types", () => {
// Set each app within Configure roles table to 'No Access' // Set each app within Configure roles table to 'No Access'
cy.get(".spectrum-Table-body").eq(0).find('tr').its('length').then((len) => { cy.get(".spectrum-Table")
for (let i = 0; i < len; i ++){ .eq(0)
cy.get(".spectrum-Table-body").eq(0).find('tr').eq(0).click().then(() => { .find(".spectrum-Table-row")
cy.get(".spectrum-Picker").eq(1).click({ force: true }) .its("length")
cy.wait(500) .then(len => {
cy.get(".spectrum-Popover").contains("No Access").click() for (let i = 0; i < len; i++) {
}) cy.get(".spectrum-Table")
cy.get(".spectrum-Button").contains("Update role").click({ force: true }) .eq(0)
.find(".spectrum-Table-row")
.eq(0)
.find(".spectrum-Table-cell")
.eq(0)
.click()
.then(() => {
cy.get(".spectrum-Picker").eq(1).click({ force: true })
cy.wait(500)
cy.get(".spectrum-Popover").contains("No Access").click()
})
cy.get(".spectrum-Button")
.contains("Update role")
.click({ force: true })
cy.wait(1000) cy.wait(1000)
} }
}) })
// Confirm Configure roles table no longer has any apps in it // Confirm Configure roles table no longer has any apps in it
cy.get(".spectrum-Table-body").eq(0).contains('No rows found') cy.get(".spectrum-Table").eq(0).contains("No rows found")
}) })
it("should enable Developer access", () => { it("should enable Developer access", () => {
// Enable Developer access // Enable Developer access
cy.get(".field").eq(4).within(() => { cy.get(".field")
cy.get(".spectrum-Switch-input").click({ force: true }) .eq(4)
}) .within(() => {
cy.get(".spectrum-Switch-input").click({ force: true })
})
// No Access table should now be empty // No Access table should now be empty
cy.get(".container").contains("No Access").parent().within(() => { cy.get(".container")
cy.get(".spectrum-Table").contains("No rows found") .contains("No Access")
}) .parent()
.within(() => {
cy.get(".spectrum-Table").contains("No rows found")
})
// Each app within Configure roles should have Admin access // Each app within Configure roles should have Admin access
cy.get(".spectrum-Table-body").eq(0).find('tr').its('length').then((len) => { cy.get(".spectrum-Table")
for (let i = 0; i < len; i++) { .eq(0)
cy.get(".spectrum-Table-body").eq(0).find('tr').eq(i).contains("Admin") .find(".spectrum-Table-row")
cy.wait(500) .its("length")
} .then(len => {
}) for (let i = 0; i < len; i++) {
cy.get(".spectrum-Table")
.eq(0)
.find(".spectrum-Table-row")
.eq(i)
.contains("Admin")
cy.wait(500)
}
})
}) })
it("should disable Developer access", () => { it("should disable Developer access", () => {
// Disable Developer access // Disable Developer access
cy.get(".field").eq(4).within(() => { cy.get(".field")
cy.get(".spectrum-Switch-input").click({ force: true }) .eq(4)
}) .within(() => {
cy.get(".spectrum-Switch-input").click({ force: true })
})
// Configure roles table should now be empty // Configure roles table should now be empty
cy.get(".container").contains("Configure roles").parent().within(() => { cy.get(".container")
cy.get(".spectrum-Table").contains("No rows found") .contains("Configure roles")
}) .parent()
.within(() => {
cy.get(".spectrum-Table").contains("No rows found")
})
}) })
it("should delete a user", () => { it("should delete a user", () => {
// Click Delete user button // Click Delete user button
cy.get(".spectrum-Button").contains("Delete user").click({force: true}).then(() => { cy.get(".spectrum-Button")
// Confirm deletion within modal .contains("Delete user")
cy.wait(500) .click({ force: true })
cy.get(".spectrum-Dialog-grid").within(() => { .then(() => {
cy.get(".spectrum-Button").contains("Delete user").click({force: true}) // Confirm deletion within modal
cy.wait(4000) cy.wait(500)
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Button")
.contains("Delete user")
.click({ force: true })
cy.wait(4000)
})
}) })
}) cy.get(".spectrum-Table").should("not.have.text", "bbuser")
cy.get(".spectrum-Table-body").should("not.have.text", "bbuser")
}) })
}) })
}) })

View File

@ -1,191 +1,222 @@
import filterTests from "../../support/filterTests" import filterTests from "../../support/filterTests"
filterTests(['all'], () => { filterTests(["all"], () => {
context("MySQL Datasource Testing", () => { context("MySQL Datasource Testing", () => {
if (Cypress.env("TEST_ENV")) { if (Cypress.env("TEST_ENV")) {
before(() => {
cy.login()
cy.createTestApp()
})
const datasource = "MySQL"
const queryName = "Cypress Test Query"
const queryRename = "CT Query Rename"
before(() => { it("Should add MySQL data source without configuration", () => {
cy.login() // Select MySQL data source
cy.createTestApp() cy.selectExternalDatasource(datasource)
}) // Attempt to fetch tables without applying configuration
const datasource = "MySQL" cy.intercept("**/datasources").as("datasource")
const queryName = "Cypress Test Query" cy.get(".spectrum-Button")
const queryRename = "CT Query Rename" .contains("Save and fetch tables")
.click({ force: true })
// Intercept Request after button click & apply assertions
cy.wait("@datasource")
cy.get("@datasource")
.its("response.body")
.should(
"have.property",
"message",
"connect ECONNREFUSED 127.0.0.1:3306"
)
cy.get("@datasource")
.its("response.body")
.should("have.property", "status", 500)
})
it("Should add MySQL data source without configuration", () => { it("should add MySQL data source and fetch tables", () => {
// Select MySQL data source // Add & configure MySQL data source
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
// Attempt to fetch tables without applying configuration cy.intercept("**/datasources").as("datasource")
cy.intercept('**/datasources').as('datasource') cy.addDatasourceConfig(datasource)
// Check response from datasource after adding configuration
cy.wait("@datasource")
cy.get("@datasource").its("response.statusCode").should("eq", 200)
// Confirm fetch tables was successful
cy.get(".spectrum-Table")
.eq(0)
.find(".spectrum-Table-row")
.its("length")
.should("be.gt", 0)
})
it("should check table fetching error", () => {
// MySQL test data source contains tables without primary keys
cy.get(".spectrum-InLineAlert")
.should("contain", "Error fetching tables")
.and("contain", "No primary key constraint found")
})
it("should define a One relationship type", () => {
// Select relationship type & configure
cy.get(".spectrum-Button")
.contains("Define relationship")
.click({ force: true })
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Picker").eq(0).click()
cy.get(".spectrum-Popover").contains("One").click()
cy.get(".spectrum-Picker").eq(1).click()
cy.get(".spectrum-Popover").contains("REGIONS").click()
cy.get(".spectrum-Picker").eq(2).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click()
cy.get(".spectrum-Picker").eq(3).click()
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
cy.get(".spectrum-Picker").eq(4).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click()
// Save relationship & reload page
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.reload()
})
// Confirm table length & column name
cy.get(".spectrum-Table")
.eq(1)
.find(".spectrum-Table-row")
.its("length")
.should("eq", 1)
cy.get(".spectrum-Table-cell").should("contain", "COUNTRIES to REGIONS")
})
it("should define a Many relationship type", () => {
// Select relationship type & configure
cy.get(".spectrum-Button")
.contains("Define relationship")
.click({ force: true })
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Picker").eq(0).click()
cy.get(".spectrum-Popover").contains("Many").click()
cy.get(".spectrum-Picker").eq(1).click()
cy.get(".spectrum-Popover").contains("LOCATIONS").click()
cy.get(".spectrum-Picker").eq(2).click()
cy.get(".spectrum-Popover").contains("REGIONS").click()
cy.get(".spectrum-Picker").eq(3).click()
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
cy.get(".spectrum-Picker").eq(4).click()
cy.get(".spectrum-Popover").contains("COUNTRY_ID").click()
cy.get(".spectrum-Picker").eq(5).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click()
// Save relationship & reload page
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.reload()
cy.wait(1000)
})
// Confirm table length & relationship name
cy.get(".spectrum-Table")
.eq(1)
.find(".spectrum-Table-row")
.its("length")
.should("eq", 2)
cy.get(".spectrum-Table-cell").should(
"contain",
"LOCATIONS through COUNTRIES → REGIONS"
)
})
it("should delete relationships", () => {
// Delete both relationships
cy.get(".spectrum-Table")
.eq(1)
.find(".spectrum-Table-row")
.its("length")
.then(len => {
for (let i = 0; i < len; i++) {
cy.get(".spectrum-Table")
.eq(1)
.within(() => {
cy.get(".spectrum-Table-row").eq(0).click()
cy.wait(500)
})
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Button") cy.get(".spectrum-Button")
.contains("Save and fetch tables") .contains("Delete")
.click({ force: true }) .click({ force: true })
// Intercept Request after button click & apply assertions })
cy.wait("@datasource") cy.reload()
cy.get("@datasource").its('response.body') }
.should('have.property', 'message', 'connect ECONNREFUSED 127.0.0.1:3306') // Confirm relationships no longer exist
cy.get("@datasource").its('response.body') cy.get(".spectrum-Body").should(
.should('have.property', 'status', 500) "contain",
}) "No relationships configured"
)
})
})
it("should add MySQL data source and fetch tables", () => { it("should add a query", () => {
// Add & configure MySQL data source // Add query
cy.selectExternalDatasource(datasource) cy.get(".spectrum-Button").contains("Add query").click({ force: true })
cy.intercept('**/datasources').as('datasource') cy.get(".spectrum-Form-item")
cy.addDatasourceConfig(datasource) .eq(0)
// Check response from datasource after adding configuration .within(() => {
cy.wait("@datasource") cy.get("input").type(queryName)
cy.get("@datasource").its('response.statusCode') })
.should('eq', 200) // Insert Query within Fields section
// Confirm fetch tables was successful cy.get(".CodeMirror textarea")
cy.get(".spectrum-Table-body").eq(0) .eq(0)
.find('tr') .type("SELECT * FROM books", { force: true })
.its('length') // Intercept query execution
.should('be.gt', 0) cy.intercept("**/queries/preview").as("query")
}) cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
cy.wait(500)
cy.wait("@query")
// Assert against Status Code & Body
cy.get("@query").its("response.statusCode").should("eq", 200)
cy.get("@query").its("response.body").should("not.be.empty")
// Save query
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
cy.get(".nav-item").should("contain", queryName)
})
it("should check table fetching error", () => { it("should duplicate a query", () => {
// MySQL test data source contains tables without primary keys // Get last nav item - The query
cy.get(".spectrum-InLineAlert") cy.get(".nav-item")
.should('contain', 'Error fetching tables') .last()
.and('contain', 'No primary key constraint found') .within(() => {
}) cy.get(".icon").eq(1).click({ force: true })
})
// Select and confirm duplication
cy.get(".spectrum-Menu").contains("Duplicate").click()
cy.get(".nav-item").should("contain", queryName + " (1)")
})
it("should define a One relationship type", () => { it("should edit a query name", () => {
// Select relationship type & configure // Rename query
cy.get(".spectrum-Button").contains("Define relationship").click({ force: true }) cy.get(".spectrum-Form-item")
cy.get(".spectrum-Dialog-grid").within(() => { .eq(0)
cy.get(".spectrum-Picker").eq(0).click() .within(() => {
cy.get(".spectrum-Popover").contains("One").click() cy.get("input").clear().type(queryRename)
cy.get(".spectrum-Picker").eq(1).click() })
cy.get(".spectrum-Popover").contains("REGIONS").click() // Save query
cy.get(".spectrum-Picker").eq(2).click() cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
cy.get(".spectrum-Popover").contains("REGION_ID").click() cy.get(".nav-item").should("contain", queryRename)
cy.get(".spectrum-Picker").eq(3).click() })
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
cy.get(".spectrum-Picker").eq(4).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click()
// Save relationship & reload page
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.reload()
})
// Confirm table length & column name
cy.get(".spectrum-Table-body").eq(1)
.find('tr')
.its('length')
.should('eq', 1)
cy.get(".spectrum-Table-cell").should('contain', "COUNTRIES to REGIONS")
})
it("should define a Many relationship type", () => { it("should delete a query", () => {
// Select relationship type & configure // Get last nav item - The query
cy.get(".spectrum-Button").contains("Define relationship").click({ force: true }) for (let i = 0; i < 2; i++) {
cy.get(".spectrum-Dialog-grid").within(() => { cy.get(".nav-item")
cy.get(".spectrum-Picker").eq(0).click() .last()
cy.get(".spectrum-Popover").contains("Many").click() .within(() => {
cy.get(".spectrum-Picker").eq(1).click() cy.get(".icon").eq(1).click({ force: true })
cy.get(".spectrum-Popover").contains("LOCATIONS").click()
cy.get(".spectrum-Picker").eq(2).click()
cy.get(".spectrum-Popover").contains("REGIONS").click()
cy.get(".spectrum-Picker").eq(3).click()
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
cy.get(".spectrum-Picker").eq(4).click()
cy.get(".spectrum-Popover").contains("COUNTRY_ID").click()
cy.get(".spectrum-Picker").eq(5).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click()
// Save relationship & reload page
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.reload()
cy.wait(1000)
})
// Confirm table length & relationship name
cy.get(".spectrum-Table-body").eq(1)
.find('tr')
.its('length')
.should('eq', 2)
cy.get(".spectrum-Table-cell")
.should('contain', "LOCATIONS through COUNTRIES → REGIONS")
})
it("should delete relationships", () => {
// Delete both relationships
cy.get(".spectrum-Table-body")
.eq(1).find('tr').its('length')
.then((len) => {
for (let i = 0; i < len; i++) {
cy.get(".spectrum-Table-body").eq(1).within(() => {
cy.get(".spectrum-Table-row").eq(0).click()
cy.wait(500)
})
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Button").contains("Delete").click({ force: true })
})
cy.reload()
}
// Confirm relationships no longer exist
cy.get(".spectrum-Body").should('contain', 'No relationships configured')
})
})
it("should add a query", () => {
// Add query
cy.get(".spectrum-Button").contains("Add query").click({ force: true })
cy.get(".spectrum-Form-item").eq(0).within(() => {
cy.get("input").type(queryName)
})
// Insert Query within Fields section
cy.get(".CodeMirror textarea").eq(0)
.type("SELECT * FROM books", { force: true })
// Intercept query execution
cy.intercept('**/queries/preview').as('query')
cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
cy.wait(500)
cy.wait("@query")
// Assert against Status Code & Body
cy.get("@query").its('response.statusCode')
.should('eq', 200)
cy.get("@query").its('response.body')
.should('not.be.empty')
// Save query
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
cy.get(".nav-item").should('contain', queryName)
})
it("should duplicate a query", () => {
// Get last nav item - The query
cy.get(".nav-item").last().within(() => {
cy.get(".icon").eq(1).click({ force: true })
})
// Select and confirm duplication
cy.get(".spectrum-Menu").contains("Duplicate").click()
cy.get(".nav-item").should('contain', queryName + ' (1)')
})
it("should edit a query name", () => {
// Rename query
cy.get(".spectrum-Form-item").eq(0).within(() => {
cy.get("input").clear().type(queryRename)
})
// Save query
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
cy.get(".nav-item").should('contain', queryRename)
})
it("should delete a query", () => {
// Get last nav item - The query
for (let i = 0; i < 2; i++) {
cy.get(".nav-item").last().within(() => {
cy.get(".icon").eq(1).click({ force: true })
})
// Select Delete
cy.get(".spectrum-Menu").contains("Delete").click()
cy.get(".spectrum-Button").contains("Delete Query").click({ force: true })
cy.wait(1000)
}
// Confirm deletion
cy.get(".nav-item").should('not.contain', queryName)
cy.get(".nav-item").should('not.contain', queryRename)
}) })
// Select Delete
cy.get(".spectrum-Menu").contains("Delete").click()
cy.get(".spectrum-Button")
.contains("Delete Query")
.click({ force: true })
cy.wait(1000)
} }
}) // Confirm deletion
cy.get(".nav-item").should("not.contain", queryName)
cy.get(".nav-item").should("not.contain", queryRename)
})
}
})
}) })

View File

@ -1,196 +1,230 @@
import filterTests from "../../support/filterTests" import filterTests from "../../support/filterTests"
filterTests(['all'], () => { filterTests(["all"], () => {
context("Oracle Datasource Testing", () => { context("Oracle Datasource Testing", () => {
if (Cypress.env("TEST_ENV")) { if (Cypress.env("TEST_ENV")) {
before(() => {
cy.login()
cy.createTestApp()
})
const datasource = "Oracle"
const queryName = "Cypress Test Query"
const queryRename = "CT Query Rename"
before(() => { it("Should add Oracle data source and skip table fetch", () => {
cy.login() // Select Oracle data source
cy.createTestApp() cy.selectExternalDatasource(datasource)
}) // Skip table fetch - no config added
const datasource = "Oracle" cy.get(".spectrum-Button")
const queryName = "Cypress Test Query" .contains("Skip table fetch")
const queryRename = "CT Query Rename" .click({ force: true })
cy.wait(500)
// Confirm config contains localhost
cy.get(".spectrum-Textfield-input")
.eq(1)
.should("have.value", "localhost")
// Add another Oracle data source, configure & skip table fetch
cy.selectExternalDatasource(datasource)
cy.addDatasourceConfig(datasource, true)
// Confirm config and no tables
cy.get(".spectrum-Textfield-input")
.eq(1)
.should("have.value", Cypress.env("oracle").HOST)
cy.get(".spectrum-Body").eq(2).should("contain", "No tables found.")
})
it("Should add Oracle data source and skip table fetch", () => { it("Should add Oracle data source and fetch tables without configuration", () => {
// Select Oracle data source // Select Oracle data source
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
// Skip table fetch - no config added // Attempt to fetch tables without applying configuration
cy.get(".spectrum-Button").contains("Skip table fetch").click({ force: true }) cy.intercept("**/datasources").as("datasource")
cy.wait(500) cy.get(".spectrum-Button")
// Confirm config contains localhost .contains("Save and fetch tables")
cy.get(".spectrum-Textfield-input").eq(1).should('have.value', 'localhost') .click({ force: true })
// Add another Oracle data source, configure & skip table fetch // Intercept Request after button click & apply assertions
cy.selectExternalDatasource(datasource) cy.wait("@datasource")
cy.addDatasourceConfig(datasource, true) cy.get("@datasource")
// Confirm config and no tables .its("response.body")
cy.get(".spectrum-Textfield-input").eq(1).should('have.value', Cypress.env("oracle").HOST) .should("have.property", "status", 500)
cy.get(".spectrum-Body").eq(2).should('contain', 'No tables found.') })
})
it("Should add Oracle data source and fetch tables without configuration", () => { it("should add Oracle data source and fetch tables", () => {
// Select Oracle data source // Add & configure Oracle data source
cy.selectExternalDatasource(datasource) cy.selectExternalDatasource(datasource)
// Attempt to fetch tables without applying configuration cy.intercept("**/datasources").as("datasource")
cy.intercept('**/datasources').as('datasource') cy.addDatasourceConfig(datasource)
// Check response from datasource after adding configuration
cy.wait("@datasource")
cy.get("@datasource").its("response.statusCode").should("eq", 200)
// Confirm fetch tables was successful
cy.get(".spectrum-Table")
.eq(0)
.find(".spectrum-Table-row")
.its("length")
.should("be.gt", 0)
})
it("should define a One relationship type", () => {
// Select relationship type & configure
cy.get(".spectrum-Button")
.contains("Define relationship")
.click({ force: true })
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Picker").eq(0).click()
cy.get(".spectrum-Popover").contains("One").click()
cy.get(".spectrum-Picker").eq(1).click()
cy.get(".spectrum-Popover").contains("REGIONS").click()
cy.get(".spectrum-Picker").eq(2).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click()
cy.get(".spectrum-Picker").eq(3).click()
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
cy.get(".spectrum-Picker").eq(4).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click()
// Save relationship & reload page
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.reload()
})
// Confirm table length & column name
cy.get(".spectrum-Table")
.eq(1)
.find(".spectrum-Table-row")
.its("length")
.should("eq", 1)
cy.get(".spectrum-Table-cell").should("contain", "COUNTRIES to REGIONS")
})
it("should define a Many relationship type", () => {
// Select relationship type & configure
cy.get(".spectrum-Button")
.contains("Define relationship")
.click({ force: true })
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Picker").eq(0).click()
cy.get(".spectrum-Popover").contains("Many").click()
cy.get(".spectrum-Picker").eq(1).click()
cy.get(".spectrum-Popover").contains("LOCATIONS").click()
cy.get(".spectrum-Picker").eq(2).click()
cy.get(".spectrum-Popover").contains("REGIONS").click()
cy.get(".spectrum-Picker").eq(3).click()
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
cy.get(".spectrum-Picker").eq(4).click()
cy.get(".spectrum-Popover").contains("COUNTRY_ID").click()
cy.get(".spectrum-Picker").eq(5).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click()
// Save relationship & reload page
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.reload()
})
// Confirm table length & relationship name
cy.get(".spectrum-Table")
.eq(1)
.find(".spectrum-Table-row")
.its("length")
.should("eq", 2)
cy.get(".spectrum-Table-cell").should(
"contain",
"LOCATIONS through COUNTRIES → REGIONS"
)
})
it("should delete relationships", () => {
// Delete both relationships
cy.get(".spectrum-Table")
.eq(1)
.find(".spectrum-Table-row")
.its("length")
.then(len => {
for (let i = 0; i < len; i++) {
cy.get(".spectrum-Table")
.eq(1)
.within(() => {
cy.get(".spectrum-Table-row").eq(0).click()
cy.wait(500)
})
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Button") cy.get(".spectrum-Button")
.contains("Save and fetch tables") .contains("Delete")
.click({ force: true }) .click({ force: true })
// Intercept Request after button click & apply assertions })
cy.wait("@datasource") cy.reload()
cy.get("@datasource").its('response.body') }
.should('have.property', 'status', 500) // Confirm relationships no longer exist
}) cy.get(".spectrum-Body").should(
"contain",
"No relationships configured"
)
})
})
it("should add Oracle data source and fetch tables", () => { it("should add a query", () => {
// Add & configure Oracle data source // Add query
cy.selectExternalDatasource(datasource) cy.get(".spectrum-Button").contains("Add query").click({ force: true })
cy.intercept('**/datasources').as('datasource') cy.get(".spectrum-Form-item")
cy.addDatasourceConfig(datasource) .eq(0)
// Check response from datasource after adding configuration .within(() => {
cy.wait("@datasource") cy.get("input").type(queryName)
cy.get("@datasource").its('response.statusCode') })
.should('eq', 200) // Insert Query within Fields section
// Confirm fetch tables was successful cy.get(".CodeMirror textarea")
cy.get(".spectrum-Table-body").eq(0) .eq(0)
.find('tr') .type("SELECT * FROM JOBS", { force: true })
.its('length') // Intercept query execution
.should('be.gt', 0) cy.intercept("**/queries/preview").as("query")
}) cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
cy.wait(500)
cy.wait("@query")
// Assert against Status Code & Body
cy.get("@query").its("response.statusCode").should("eq", 200)
cy.get("@query").its("response.body").should("not.be.empty")
// Save query
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
cy.get(".nav-item").should("contain", queryName)
})
it("should define a One relationship type", () => { it("should duplicate a query", () => {
// Select relationship type & configure // Get query nav item
cy.get(".spectrum-Button").contains("Define relationship").click({ force: true }) cy.get(".nav-item")
cy.get(".spectrum-Dialog-grid").within(() => { .contains(queryName)
cy.get(".spectrum-Picker").eq(0).click() .parent()
cy.get(".spectrum-Popover").contains("One").click() .within(() => {
cy.get(".spectrum-Picker").eq(1).click() cy.get(".spectrum-Icon").eq(1).click({ force: true })
cy.get(".spectrum-Popover").contains("REGIONS").click() })
cy.get(".spectrum-Picker").eq(2).click() // Select and confirm duplication
cy.get(".spectrum-Popover").contains("REGION_ID").click() cy.get(".spectrum-Menu").contains("Duplicate").click()
cy.get(".spectrum-Picker").eq(3).click() cy.get(".nav-item").should("contain", queryName + " (1)")
cy.get(".spectrum-Popover").contains("COUNTRIES").click() })
cy.get(".spectrum-Picker").eq(4).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click()
// Save relationship & reload page
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.reload()
})
// Confirm table length & column name
cy.get(".spectrum-Table-body").eq(1)
.find('tr')
.its('length')
.should('eq', 1)
cy.get(".spectrum-Table-cell").should('contain', "COUNTRIES to REGIONS")
})
it("should define a Many relationship type", () => { it("should edit a query name", () => {
// Select relationship type & configure // Rename query
cy.get(".spectrum-Button").contains("Define relationship").click({ force: true }) cy.get(".spectrum-Form-item")
cy.get(".spectrum-Dialog-grid").within(() => { .eq(0)
cy.get(".spectrum-Picker").eq(0).click() .within(() => {
cy.get(".spectrum-Popover").contains("Many").click() cy.get("input").clear().type(queryRename)
cy.get(".spectrum-Picker").eq(1).click() })
cy.get(".spectrum-Popover").contains("LOCATIONS").click() // Save query
cy.get(".spectrum-Picker").eq(2).click() cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
cy.get(".spectrum-Popover").contains("REGIONS").click() cy.get(".nav-item").should("contain", queryRename)
cy.get(".spectrum-Picker").eq(3).click() })
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
cy.get(".spectrum-Picker").eq(4).click()
cy.get(".spectrum-Popover").contains("COUNTRY_ID").click()
cy.get(".spectrum-Picker").eq(5).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click()
// Save relationship & reload page
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.reload()
})
// Confirm table length & relationship name
cy.get(".spectrum-Table-body").eq(1)
.find('tr')
.its('length')
.should('eq', 2)
cy.get(".spectrum-Table-cell")
.should('contain', "LOCATIONS through COUNTRIES → REGIONS")
})
it("should delete relationships", () => { it("should delete a query", () => {
// Delete both relationships // Get query nav item - QueryName
cy.get(".spectrum-Table-body") cy.get(".nav-item")
.eq(1).find('tr').its('length') .contains(queryName)
.then((len) => { .parent()
for (let i = 0; i < len; i++) { .within(() => {
cy.get(".spectrum-Table-body").eq(1).within(() => { cy.get(".spectrum-Icon").eq(1).click({ force: true })
cy.get(".spectrum-Table-row").eq(0).click() })
cy.wait(500)
})
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Button").contains("Delete").click({ force: true })
})
cy.reload()
}
// Confirm relationships no longer exist
cy.get(".spectrum-Body").should('contain', 'No relationships configured')
})
})
it("should add a query", () => { // Select Delete
// Add query cy.get(".spectrum-Menu").contains("Delete").click()
cy.get(".spectrum-Button").contains("Add query").click({ force: true }) cy.get(".spectrum-Button")
cy.get(".spectrum-Form-item").eq(0).within(() => { .contains("Delete Query")
cy.get("input").type(queryName) .click({ force: true })
}) cy.wait(1000)
// Insert Query within Fields section
cy.get(".CodeMirror textarea").eq(0)
.type("SELECT * FROM JOBS", { force: true })
// Intercept query execution
cy.intercept('**/queries/preview').as('query')
cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
cy.wait(500)
cy.wait("@query")
// Assert against Status Code & Body
cy.get("@query").its('response.statusCode')
.should('eq', 200)
cy.get("@query").its('response.body')
.should('not.be.empty')
// Save query
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
cy.get(".nav-item").should('contain', queryName)
})
it("should duplicate a query", () => { // Confirm deletion
// Get query nav item cy.get(".nav-item").should("not.contain", queryName)
cy.get(".nav-item").contains(queryName).parent().within(() => { })
cy.get(".spectrum-Icon").eq(1).click({ force: true }) }
}) })
// Select and confirm duplication
cy.get(".spectrum-Menu").contains("Duplicate").click()
cy.get(".nav-item").should('contain', queryName + ' (1)')
})
it("should edit a query name", () => {
// Rename query
cy.get(".spectrum-Form-item").eq(0).within(() => {
cy.get("input").clear().type(queryRename)
})
// Save query
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
cy.get(".nav-item").should('contain', queryRename)
})
it("should delete a query", () => {
// Get query nav item - QueryName
cy.get(".nav-item").contains(queryName).parent().within(() => {
cy.get(".spectrum-Icon").eq(1).click({ force: true })
})
// Select Delete
cy.get(".spectrum-Menu").contains("Delete").click()
cy.get(".spectrum-Button").contains("Delete Query").click({ force: true })
cy.wait(1000)
// Confirm deletion
cy.get(".nav-item").should('not.contain', queryName)
})
}
})
}) })

View File

@ -1,241 +1,285 @@
import filterTests from "../../support/filterTests" import filterTests from "../../support/filterTests"
filterTests(['all'], () => { filterTests(["all"], () => {
context("PostgreSQL Datasource Testing", () => { context("PostgreSQL Datasource Testing", () => {
if (Cypress.env("TEST_ENV")) { if (Cypress.env("TEST_ENV")) {
before(() => {
cy.login()
cy.createTestApp()
})
const datasource = "PostgreSQL"
const queryName = "Cypress Test Query"
const queryRename = "CT Query Rename"
before(() => { it("Should add PostgreSQL data source without configuration", () => {
cy.login() // Select PostgreSQL data source
cy.createTestApp() cy.selectExternalDatasource(datasource)
// Attempt to fetch tables without applying configuration
cy.intercept("**/datasources").as("datasource")
cy.get(".spectrum-Button")
.contains("Save and fetch tables")
.click({ force: true })
// Intercept Request after button click & apply assertions
cy.wait("@datasource")
cy.get("@datasource")
.its("response.body")
.should(
"have.property",
"message",
"connect ECONNREFUSED 127.0.0.1:5432"
)
cy.get("@datasource")
.its("response.body")
.should("have.property", "status", 500)
})
it("should add PostgreSQL data source and fetch tables", () => {
// Add & configure PostgreSQL data source
cy.selectExternalDatasource(datasource)
cy.intercept("**/datasources").as("datasource")
cy.addDatasourceConfig(datasource)
// Check response from datasource after adding configuration
cy.wait("@datasource")
cy.get("@datasource").its("response.statusCode").should("eq", 200)
// Confirm fetch tables was successful
cy.get(".spectrum-Table")
.eq(0)
.find(".spectrum-Table-row")
.its("length")
.should("be.gt", 0)
})
it("should define a One relationship type", () => {
// Select relationship type & configure
cy.get(".spectrum-Button")
.contains("Define relationship")
.click({ force: true })
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Picker").eq(0).click()
cy.get(".spectrum-Popover").contains("One").click()
cy.get(".spectrum-Picker").eq(1).click()
cy.get(".spectrum-Popover").contains("REGIONS").click()
cy.get(".spectrum-Picker").eq(2).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click()
cy.get(".spectrum-Picker").eq(3).click()
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
cy.get(".spectrum-Picker").eq(4).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click()
// Save relationship & reload page
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.reload()
})
// Confirm table length & column name
cy.get(".spectrum-Table")
.eq(1)
.find(".spectrum-Table-row")
.its("length")
.should("eq", 1)
cy.get(".spectrum-Table-cell").should("contain", "COUNTRIES to REGIONS")
})
it("should define a Many relationship type", () => {
// Select relationship type & configure
cy.get(".spectrum-Button")
.contains("Define relationship")
.click({ force: true })
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Picker").eq(0).click()
cy.get(".spectrum-Popover").contains("Many").click()
cy.get(".spectrum-Picker").eq(1).click()
cy.get(".spectrum-Popover").contains("LOCATIONS").click()
cy.get(".spectrum-Picker").eq(2).click()
cy.get(".spectrum-Popover").contains("REGIONS").click()
cy.get(".spectrum-Picker").eq(3).click()
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
cy.get(".spectrum-Picker").eq(4).click()
cy.get(".spectrum-Popover").contains("COUNTRY_ID").click()
cy.get(".spectrum-Picker").eq(5).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click()
// Save relationship & reload page
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.reload()
})
// Confirm table length & relationship name
cy.get(".spectrum-Table")
.eq(1)
.find(".spectrum-Table-row")
.its("length")
.should("eq", 2)
cy.get(".spectrum-Table-cell").should(
"contain",
"LOCATIONS through COUNTRIES → REGIONS"
)
})
it("should delete a relationship", () => {
cy.get(".hierarchy-items-container").contains(datasource).click()
cy.reload()
// Delete one relationship
cy.get(".spectrum-Table")
.eq(1)
.within(() => {
cy.get(".spectrum-Table-row").eq(0).click()
cy.wait(500)
})
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Button").contains("Delete").click({ force: true })
})
cy.reload()
// Confirm relationship was deleted
cy.get(".spectrum-Table")
.eq(1)
.find(".spectrum-Table-row")
.its("length")
.should("eq", 1)
})
it("should add a query", () => {
// Add query
cy.get(".spectrum-Button").contains("Add query").click({ force: true })
cy.get(".spectrum-Form-item")
.eq(0)
.within(() => {
cy.get("input").type(queryName)
})
// Insert Query within Fields section
cy.get(".CodeMirror textarea")
.eq(0)
.type("SELECT * FROM books", { force: true })
// Intercept query execution
cy.intercept("**/queries/preview").as("query")
cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
cy.wait(500)
cy.wait("@query")
// Assert against Status Code & Body
cy.get("@query").its("response.statusCode").should("eq", 200)
cy.get("@query").its("response.body").should("not.be.empty")
// Save query
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
cy.get(".hierarchy-items-container").should("contain", queryName)
})
it("should switch to schema with no tables", () => {
// Switch Schema - To one without any tables
cy.get(".hierarchy-items-container").contains(datasource).click()
switchSchema("randomText")
// No tables displayed
cy.get(".spectrum-Body").eq(2).should("contain", "No tables found")
// Previously created query should be visible
cy.get(".spectrum-Table").should("contain", queryName)
})
it("should switch schemas", () => {
// Switch schema - To one with tables
switchSchema("1")
// Confirm tables exist - Check for specific one
cy.get(".spectrum-Table").eq(0).should("contain", "test")
cy.get(".spectrum-Table")
.eq(0)
.find(".spectrum-Table-row")
.its("length")
.should("eq", 1)
// Confirm specific table visible within left nav bar
cy.get(".hierarchy-items-container").should("contain", "test")
// Switch back to public schema
switchSchema("public")
// Confirm tables exist - again
cy.get(".spectrum-Table").eq(0).should("contain", "REGIONS")
cy.get(".spectrum-Table")
.eq(0)
.find(".spectrum-Table-row")
.its("length")
.should("be.gt", 1)
// Confirm specific table visible within left nav bar
cy.get(".hierarchy-items-container").should("contain", "REGIONS")
// No relationships and one query
cy.get(".spectrum-Body")
.eq(3)
.should("contain", "No relationships configured.")
cy.get(".spectrum-Table").eq(1).should("contain", queryName)
})
it("should duplicate a query", () => {
// Get last nav item - The query
cy.get(".nav-item")
.last()
.within(() => {
cy.get(".icon").eq(1).click({ force: true })
})
// Select and confirm duplication
cy.get(".spectrum-Menu").contains("Duplicate").click()
cy.get(".nav-item").should("contain", queryName + " (1)")
})
it("should edit a query name", () => {
// Access query
cy.get(".hierarchy-items-container")
.contains(queryName + " (1)")
.click()
// Rename query
cy.get(".spectrum-Form-item")
.eq(0)
.within(() => {
cy.get("input").clear().type(queryRename)
})
// Run and Save query
cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
cy.wait(500)
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
cy.get(".nav-item").should("contain", queryRename)
})
it("should delete a query", () => {
// Get last nav item - The query
for (let i = 0; i < 2; i++) {
cy.get(".nav-item")
.last()
.within(() => {
cy.get(".icon").eq(1).click({ force: true })
}) })
const datasource = "PostgreSQL" // Select Delete
const queryName = "Cypress Test Query" cy.get(".spectrum-Menu").contains("Delete").click()
const queryRename = "CT Query Rename" cy.get(".spectrum-Button")
.contains("Delete Query")
it("Should add PostgreSQL data source without configuration", () => { .click({ force: true })
// Select PostgreSQL data source cy.wait(1000)
cy.selectExternalDatasource(datasource)
// Attempt to fetch tables without applying configuration
cy.intercept('**/datasources').as('datasource')
cy.get(".spectrum-Button")
.contains("Save and fetch tables")
.click({ force: true })
// Intercept Request after button click & apply assertions
cy.wait("@datasource")
cy.get("@datasource").its('response.body')
.should('have.property', 'message', 'connect ECONNREFUSED 127.0.0.1:5432')
cy.get("@datasource").its('response.body')
.should('have.property', 'status', 500)
})
it("should add PostgreSQL data source and fetch tables", () => {
// Add & configure PostgreSQL data source
cy.selectExternalDatasource(datasource)
cy.intercept('**/datasources').as('datasource')
cy.addDatasourceConfig(datasource)
// Check response from datasource after adding configuration
cy.wait("@datasource")
cy.get("@datasource").its('response.statusCode')
.should('eq', 200)
// Confirm fetch tables was successful
cy.get(".spectrum-Table-body").eq(0)
.find('tr')
.its('length')
.should('be.gt', 0)
})
it("should define a One relationship type", () => {
// Select relationship type & configure
cy.get(".spectrum-Button").contains("Define relationship").click({ force: true })
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Picker").eq(0).click()
cy.get(".spectrum-Popover").contains("One").click()
cy.get(".spectrum-Picker").eq(1).click()
cy.get(".spectrum-Popover").contains("REGIONS").click()
cy.get(".spectrum-Picker").eq(2).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click()
cy.get(".spectrum-Picker").eq(3).click()
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
cy.get(".spectrum-Picker").eq(4).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click()
// Save relationship & reload page
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.reload()
})
// Confirm table length & column name
cy.get(".spectrum-Table-body").eq(1)
.find('tr')
.its('length')
.should('eq', 1)
cy.get(".spectrum-Table-cell").should('contain', "COUNTRIES to REGIONS")
})
it("should define a Many relationship type", () => {
// Select relationship type & configure
cy.get(".spectrum-Button").contains("Define relationship").click({ force: true })
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Picker").eq(0).click()
cy.get(".spectrum-Popover").contains("Many").click()
cy.get(".spectrum-Picker").eq(1).click()
cy.get(".spectrum-Popover").contains("LOCATIONS").click()
cy.get(".spectrum-Picker").eq(2).click()
cy.get(".spectrum-Popover").contains("REGIONS").click()
cy.get(".spectrum-Picker").eq(3).click()
cy.get(".spectrum-Popover").contains("COUNTRIES").click()
cy.get(".spectrum-Picker").eq(4).click()
cy.get(".spectrum-Popover").contains("COUNTRY_ID").click()
cy.get(".spectrum-Picker").eq(5).click()
cy.get(".spectrum-Popover").contains("REGION_ID").click()
// Save relationship & reload page
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.reload()
})
// Confirm table length & relationship name
cy.get(".spectrum-Table-body").eq(1)
.find('tr')
.its('length')
.should('eq', 2)
cy.get(".spectrum-Table-cell")
.should('contain', "LOCATIONS through COUNTRIES → REGIONS")
})
it("should delete a relationship", () => {
cy.get(".hierarchy-items-container").contains(datasource).click()
cy.reload()
// Delete one relationship
cy.get(".spectrum-Table-body").eq(1).within(() => {
cy.get(".spectrum-Table-row").eq(0).click()
cy.wait(500)
})
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Button").contains("Delete").click({ force: true })
})
cy.reload()
// Confirm relationship was deleted
cy.get(".spectrum-Table-body")
.eq(1).find('tr').its('length').should('eq', 1)
})
it("should add a query", () => {
// Add query
cy.get(".spectrum-Button").contains("Add query").click({ force: true })
cy.get(".spectrum-Form-item").eq(0).within(() => {
cy.get("input").type(queryName)
})
// Insert Query within Fields section
cy.get(".CodeMirror textarea").eq(0)
.type("SELECT * FROM books", { force: true })
// Intercept query execution
cy.intercept('**/queries/preview').as('query')
cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
cy.wait(500)
cy.wait("@query")
// Assert against Status Code & Body
cy.get("@query").its('response.statusCode')
.should('eq', 200)
cy.get("@query").its('response.body')
.should('not.be.empty')
// Save query
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
cy.get(".hierarchy-items-container").should('contain', queryName)
})
it("should switch to schema with no tables", () => {
// Switch Schema - To one without any tables
cy.get(".hierarchy-items-container").contains(datasource).click()
switchSchema("randomText")
// No tables displayed
cy.get(".spectrum-Body").eq(2).should('contain', 'No tables found')
// Previously created query should be visible
cy.get(".spectrum-Table-body").should('contain', queryName)
})
it("should switch schemas", () => {
// Switch schema - To one with tables
switchSchema("1")
// Confirm tables exist - Check for specific one
cy.get(".spectrum-Table-body").eq(0).should('contain', 'test')
cy.get(".spectrum-Table-body").eq(0).find('tr').its('length').should('eq', 1)
// Confirm specific table visible within left nav bar
cy.get(".hierarchy-items-container").should('contain', 'test')
// Switch back to public schema
switchSchema("public")
// Confirm tables exist - again
cy.get(".spectrum-Table-body").eq(0).should('contain', 'REGIONS')
cy.get(".spectrum-Table-body").eq(0)
.find('tr').its('length').should('be.gt', 1)
// Confirm specific table visible within left nav bar
cy.get(".hierarchy-items-container").should('contain', 'REGIONS')
// No relationships and one query
cy.get(".spectrum-Body").eq(3).should('contain', 'No relationships configured.')
cy.get(".spectrum-Table-body").eq(1).should('contain', queryName)
})
it("should duplicate a query", () => {
// Get last nav item - The query
cy.get(".nav-item").last().within(() => {
cy.get(".icon").eq(1).click({ force: true })
})
// Select and confirm duplication
cy.get(".spectrum-Menu").contains("Duplicate").click()
cy.get(".nav-item").should('contain', queryName + ' (1)')
})
it("should edit a query name", () => {
// Access query
cy.get(".hierarchy-items-container").contains(queryName + ' (1)').click()
// Rename query
cy.get(".spectrum-Form-item").eq(0).within(() => {
cy.get("input").clear().type(queryRename)
})
// Run and Save query
cy.get(".spectrum-Button").contains("Run Query").click({ force: true })
cy.wait(500)
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
cy.get(".nav-item").should('contain', queryRename)
})
it("should delete a query", () => {
// Get last nav item - The query
for (let i = 0; i < 2; i++) {
cy.get(".nav-item").last().within(() => {
cy.get(".icon").eq(1).click({ force: true })
})
// Select Delete
cy.get(".spectrum-Menu").contains("Delete").click()
cy.get(".spectrum-Button").contains("Delete Query").click({ force: true })
cy.wait(1000)
}
// Confirm deletion
cy.get(".nav-item").should('not.contain', queryName)
cy.get(".nav-item").should('not.contain', queryRename)
})
const switchSchema = (schema) => {
// Edit configuration - Change Schema
cy.get(".spectrum-Textfield").eq(6).within(() => {
cy.get('input').clear().type(schema)
})
// Save configuration & fetch
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.get(".spectrum-Button").contains("Fetch tables").click({ force: true })
// Click fetch tables again within modal
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Button").contains("Fetch tables").click({ force: true })
})
cy.reload()
cy.wait(5000)
}
} }
}) // Confirm deletion
cy.get(".nav-item").should("not.contain", queryName)
cy.get(".nav-item").should("not.contain", queryRename)
})
const switchSchema = schema => {
// Edit configuration - Change Schema
cy.get(".spectrum-Textfield")
.eq(6)
.within(() => {
cy.get("input").clear().type(schema)
})
// Save configuration & fetch
cy.get(".spectrum-Button").contains("Save").click({ force: true })
cy.get(".spectrum-Button")
.contains("Fetch tables")
.click({ force: true })
// Click fetch tables again within modal
cy.get(".spectrum-Dialog-grid").within(() => {
cy.get(".spectrum-Button")
.contains("Fetch tables")
.click({ force: true })
})
cy.reload()
cy.wait(5000)
}
}
})
}) })

View File

@ -98,7 +98,7 @@
tableId={id} tableId={id}
data={$fetch.rows} data={$fetch.rows}
bind:hideAutocolumns bind:hideAutocolumns
loading={$fetch.loading} loading={!$fetch.loaded}
on:sort={onSort} on:sort={onSort}
allowEditing allowEditing
disableSorting disableSorting

View File

@ -136,7 +136,7 @@
</div> </div>
</div> </div>
{#key tableId} {#key tableId}
<div class="table-wrapper" in:fade={{ delay: 200, duration: 100 }}> <div class="table-wrapper">
<Table <Table
{data} {data}
{schema} {schema}

View File

@ -0,0 +1,66 @@
<script>
import {
Input,
Select,
ColorPicker,
DrawerContent,
Layout,
Label,
} from "@budibase/bbui"
import { store } from "builderStore"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let column
</script>
<DrawerContent>
<div class="container">
<Layout noPadding gap="S">
<Input bind:value={column.width} label="Width" placeholder="Auto" />
<Select
label="Alignment"
bind:value={column.align}
options={["Left", "Center", "Right"]}
placeholder="Default"
/>
<DrawerBindableInput
label="Value"
value={column.template}
on:change={e => (column.template = e.detail)}
placeholder={`{{ Value }}`}
bindings={[
{
readableBinding: "Value",
runtimeBinding: "[value]",
},
]}
/>
<Layout noPadding gap="XS">
<Label>Background color</Label>
<ColorPicker
value={column.background}
on:change={e => (column.background = e.detail)}
alignRight
spectrumTheme={$store.theme}
/>
</Layout>
<Layout noPadding gap="XS">
<Label>Text color</Label>
<ColorPicker
value={column.color}
on:change={e => (column.color = e.detail)}
alignRight
spectrumTheme={$store.theme}
/>
</Layout>
</Layout>
</div>
</DrawerContent>
<style>
.container {
width: 100%;
max-width: 240px;
margin: 0 auto;
}
</style>

View File

@ -0,0 +1,34 @@
<script>
import { Drawer, Button, Icon } from "@budibase/bbui"
import CellDrawer from "./CellDrawer.svelte"
export let column
let boundValue
let drawer
$: updateBoundValue(column)
const updateBoundValue = value => {
boundValue = { ...value }
}
const open = () => {
updateBoundValue(column)
drawer.show()
}
const save = () => {
column = boundValue
drawer.hide()
}
</script>
<Icon name="Settings" hoverable size="S" on:click={open} />
<Drawer bind:this={drawer} title="Table Columns">
<svelte:fragment slot="description">
"{column.name}" column settings
</svelte:fragment>
<Button cta slot="buttons" on:click={save}>Save</Button>
<CellDrawer slot="body" bind:column={boundValue} />
</Drawer>

View File

@ -4,17 +4,19 @@
Icon, Icon,
DrawerContent, DrawerContent,
Layout, Layout,
Input,
Select, Select,
Label, Label,
Body, Body,
Input,
} from "@budibase/bbui" } from "@budibase/bbui"
import { flip } from "svelte/animate" import { flip } from "svelte/animate"
import { dndzone } from "svelte-dnd-action" import { dndzone } from "svelte-dnd-action"
import { generate } from "shortid" import { generate } from "shortid"
import CellEditor from "./CellEditor.svelte"
export let columns = [] export let columns = []
export let options = [] export let options = []
export let schema = {}
const flipDurationMs = 150 const flipDurationMs = 150
let dragDisabled = true let dragDisabled = true
@ -61,11 +63,23 @@
dragDisabled = true dragDisabled = true
} }
const addAllColumns = () => {
let newColumns = columns || []
options.forEach(field => {
const fieldSchema = schema[field]
const hasCol = columns && columns.findIndex(x => x.name === field) !== -1
if (!fieldSchema?.autocolumn && !hasCol) {
newColumns.push({
name: field,
displayName: field,
})
}
})
columns = newColumns
}
const reset = () => { const reset = () => {
columns = options.map(col => ({ columns = []
name: col,
displayName: col,
}))
} }
</script> </script>
@ -79,6 +93,7 @@
<Label size="L">Column</Label> <Label size="L">Column</Label>
<Label size="L">Label</Label> <Label size="L">Label</Label>
<div /> <div />
<div />
</div> </div>
<div <div
class="columns" class="columns"
@ -108,6 +123,7 @@
on:change={e => (column.displayName = e.detail)} on:change={e => (column.displayName = e.detail)}
/> />
<Input bind:value={column.displayName} placeholder="Label" /> <Input bind:value={column.displayName} placeholder="Label" />
<CellEditor bind:column />
<Icon <Icon
name="Close" name="Close"
hoverable hoverable
@ -121,19 +137,25 @@
</Layout> </Layout>
{:else} {:else}
<div class="column"> <div class="column">
<div /> <div class="wide">
<Body size="S">Add the first column to your table.</Body> <Body size="S">
By default, all table columns will automatically be shown.
<br />
You can manually control which columns are included in your table,
and their appearance, by adding them below.
</Body>
</div>
</div> </div>
{/if} {/if}
<div class="columns"> <div class="column">
<div class="column"> <div class="buttons wide">
<div /> <Button secondary icon="Add" on:click={addColumn}>Add column</Button>
<div class="buttons"> <Button secondary quiet on:click={addAllColumns}>
<Button secondary icon="Add" on:click={addColumn}> Add all columns
Add column </Button>
</Button> {#if columns?.length}
<Button secondary quiet on:click={reset}>Reset columns</Button> <Button secondary quiet on:click={reset}>Reset columns</Button>
</div> {/if}
</div> </div>
</div> </div>
</Layout> </Layout>
@ -156,7 +178,7 @@
.column { .column {
gap: var(--spacing-l); gap: var(--spacing-l);
display: grid; display: grid;
grid-template-columns: 20px 1fr 1fr 20px; grid-template-columns: 20px 1fr 1fr auto auto;
align-items: center; align-items: center;
border-radius: var(--border-radius-s); border-radius: var(--border-radius-s);
transition: background-color ease-in-out 130ms; transition: background-color ease-in-out 130ms;
@ -168,6 +190,9 @@
display: grid; display: grid;
place-items: center; place-items: center;
} }
.wide {
grid-column: 2 / -1;
}
.buttons { .buttons {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@ -18,22 +18,30 @@
let boundValue let boundValue
$: datasource = getDatasourceForProvider($currentAsset, componentInstance) $: datasource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchemaForDatasource($currentAsset, datasource).schema $: schema = getSchema($currentAsset, datasource)
$: options = Object.keys(schema || {}) $: options = Object.keys(schema || {})
$: sanitisedValue = getValidColumns(value, options) $: sanitisedValue = getValidColumns(value, options)
$: updateBoundValue(sanitisedValue) $: updateBoundValue(sanitisedValue)
const getSchema = (asset, datasource) => {
const schema = getSchemaForDatasource(asset, datasource).schema
// Don't show ID and rev in tables
if (schema) {
delete schema._id
delete schema._rev
}
return schema
}
const updateBoundValue = value => { const updateBoundValue = value => {
boundValue = cloneDeep(value) boundValue = cloneDeep(value)
} }
const getValidColumns = (columns, options) => { const getValidColumns = (columns, options) => {
// If no columns then default to all columns
if (!Array.isArray(columns) || !columns.length) { if (!Array.isArray(columns) || !columns.length) {
return options.map(col => ({ return []
name: col,
displayName: col,
}))
} }
// We need to account for legacy configs which would just be an array // We need to account for legacy configs which would just be an array
// of strings // of strings
@ -48,17 +56,22 @@
}) })
} }
const open = () => {
updateBoundValue(sanitisedValue)
drawer.show()
}
const save = () => { const save = () => {
dispatch("change", getValidColumns(boundValue, options)) dispatch("change", getValidColumns(boundValue, options))
drawer.hide() drawer.hide()
} }
</script> </script>
<ActionButton on:click={drawer.show}>Configure columns</ActionButton> <ActionButton on:click={open}>Configure columns</ActionButton>
<Drawer bind:this={drawer} title="Table Columns"> <Drawer bind:this={drawer} title="Table Columns">
<svelte:fragment slot="description"> <svelte:fragment slot="description">
Configure the columns in your table. Configure the columns in your table.
</svelte:fragment> </svelte:fragment>
<Button cta slot="buttons" on:click={save}>Save</Button> <Button cta slot="buttons" on:click={save}>Save</Button>
<ColumnDrawer slot="body" bind:columns={boundValue} {options} /> <ColumnDrawer slot="body" bind:columns={boundValue} {options} {schema} />
</Drawer> </Drawer>

View File

@ -2679,7 +2679,8 @@
"type": "columns", "type": "columns",
"label": "Columns", "label": "Columns",
"key": "columns", "key": "columns",
"dependsOn": "dataProvider" "dependsOn": "dataProvider",
"nested": true
}, },
{ {
"type": "select", "type": "select",
@ -2702,6 +2703,11 @@
"label": "Quiet", "label": "Quiet",
"key": "quiet" "key": "quiet"
}, },
{
"type": "boolean",
"label": "Compact",
"key": "compact"
},
{ {
"type": "boolean", "type": "boolean",
"label": "Show auto columns", "label": "Show auto columns",
@ -2957,6 +2963,11 @@
"label": "Quiet table variant", "label": "Quiet table variant",
"key": "quiet" "key": "quiet"
}, },
{
"type": "boolean",
"label": "Compact",
"key": "compact"
},
{ {
"type": "boolean", "type": "boolean",
"label": "Show auto columns", "label": "Show auto columns",

View File

@ -16,6 +16,7 @@
export let showAutoColumns export let showAutoColumns
export let rowCount export let rowCount
export let quiet export let quiet
export let compact
export let size export let size
export let linkRows export let linkRows
export let linkURL export let linkURL
@ -162,6 +163,7 @@
showAutoColumns, showAutoColumns,
rowCount, rowCount,
quiet, quiet,
compact,
size, size,
linkRows, linkRows,
linkURL, linkURL,

View File

@ -14,6 +14,7 @@
export let linkURL export let linkURL
export let linkColumn export let linkColumn
export let linkPeek export let linkPeek
export let compact
const component = getContext("component") const component = getContext("component")
const { styleable, getAction, ActionTypes, routeStore } = getContext("sdk") const { styleable, getAction, ActionTypes, routeStore } = getContext("sdk")
@ -72,6 +73,7 @@
order: 0, order: 0,
sortable: false, sortable: false,
divider: true, divider: true,
width: "auto",
} }
} }
@ -84,8 +86,13 @@
if (UnsortableTypes.includes(schema[columnName].type)) { if (UnsortableTypes.includes(schema[columnName].type)) {
newSchema[columnName].sortable = false newSchema[columnName].sortable = false
} }
if (field?.displayName) {
newSchema[columnName].displayName = field?.displayName // Add additional settings like width etc
if (typeof field === "object") {
newSchema[columnName] = {
...newSchema[columnName],
...field,
}
} }
}) })
return newSchema return newSchema
@ -119,12 +126,14 @@
{loading} {loading}
{rowCount} {rowCount}
{quiet} {quiet}
{compact}
{customRenderers} {customRenderers}
allowSelectRows={false} allowSelectRows={false}
allowEditRows={false} allowEditRows={false}
allowEditColumns={false} allowEditColumns={false}
showAutoColumns={true} showAutoColumns={true}
disableSorting disableSorting
autoSortColumns={!columns?.length}
on:sort={onSort} on:sort={onSort}
on:click={onClick} on:click={onClick}
> >