mirror of https://github.com/apache/druid.git
Web console: fix error when querying with grand totals (#8795)
* fix error when querying with grand totals * also support object * improve tests
This commit is contained in:
parent
3ff5e02237
commit
ed6be81d12
|
@ -0,0 +1,17 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`table cell unparseable matches snapshot not timestamp 1`] = `
|
||||||
|
<div
|
||||||
|
class="table-cell-unparseable"
|
||||||
|
>
|
||||||
|
unparseable
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`table cell unparseable matches snapshot timestamp 1`] = `
|
||||||
|
<div
|
||||||
|
class="table-cell-unparseable"
|
||||||
|
>
|
||||||
|
unparseable timestamp
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.table-cell-unparseable {
|
||||||
|
color: #9e2b0e;
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { TableCellUnparseable } from './table-cell-unparseable';
|
||||||
|
|
||||||
|
describe('table cell unparseable', () => {
|
||||||
|
it('matches snapshot not timestamp', () => {
|
||||||
|
const tableCellUnparseable = <TableCellUnparseable />;
|
||||||
|
|
||||||
|
const { container } = render(tableCellUnparseable);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches snapshot timestamp', () => {
|
||||||
|
const tableCellUnparseable = <TableCellUnparseable timestamp />;
|
||||||
|
|
||||||
|
const { container } = render(tableCellUnparseable);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import './table-cell-unparseable.scss';
|
||||||
|
|
||||||
|
export interface TableCellUnparseableProps {
|
||||||
|
timestamp?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TableCellUnparseable = React.memo(function TableCellUnparseable(
|
||||||
|
props: TableCellUnparseableProps,
|
||||||
|
) {
|
||||||
|
const { timestamp } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="table-cell-unparseable">
|
||||||
|
{timestamp ? 'unparseable timestamp' : 'unparseable'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
File diff suppressed because one or more lines are too long
|
@ -21,10 +21,6 @@
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.unparseable {
|
|
||||||
color: #9e2b0e;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.timestamp {
|
&.timestamp {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,49 +23,59 @@ import { TableCell } from './table-cell';
|
||||||
|
|
||||||
describe('table cell', () => {
|
describe('table cell', () => {
|
||||||
it('matches snapshot null', () => {
|
it('matches snapshot null', () => {
|
||||||
const tableCell = <TableCell value={null} unparseable={false} timestamp={false} />;
|
const tableCell = <TableCell value={null} />;
|
||||||
|
|
||||||
const { container } = render(tableCell);
|
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('matches snapshot null timestamp', () => {
|
|
||||||
const tableCell = <TableCell value={null} unparseable={false} timestamp />;
|
|
||||||
|
|
||||||
const { container } = render(tableCell);
|
const { container } = render(tableCell);
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('matches snapshot simple', () => {
|
it('matches snapshot simple', () => {
|
||||||
const tableCell = <TableCell value="Hello World" unparseable={false} timestamp={false} />;
|
const tableCell = <TableCell value="Hello World" />;
|
||||||
|
|
||||||
const { container } = render(tableCell);
|
const { container } = render(tableCell);
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('matches snapshot array short', () => {
|
it('matches snapshot array short', () => {
|
||||||
const tableCell = <TableCell value={['a', 'b', 'c']} unparseable={false} timestamp={false} />;
|
const tableCell = <TableCell value={['a', 'b', 'c']} />;
|
||||||
|
|
||||||
const { container } = render(tableCell);
|
const { container } = render(tableCell);
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('matches snapshot array long', () => {
|
it('matches snapshot array long', () => {
|
||||||
const tableCell = (
|
const tableCell = <TableCell value={Array.from(new Array(100)).map((_, i) => i)} />;
|
||||||
<TableCell
|
|
||||||
value={Array.from(new Array(100)).map((_, i) => i)}
|
const { container } = render(tableCell);
|
||||||
unparseable={false}
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
timestamp={false}
|
});
|
||||||
/>
|
|
||||||
);
|
it('matches snapshot object', () => {
|
||||||
|
const tableCell = <TableCell value={{ hello: 'world' }} />;
|
||||||
|
|
||||||
const { container } = render(tableCell);
|
const { container } = render(tableCell);
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('matches snapshot truncate', () => {
|
it('matches snapshot truncate', () => {
|
||||||
const longString = new Array(100).join('test');
|
const longString = new Array(100).join('test_');
|
||||||
const tableCell = <TableCell value={longString} unparseable={false} timestamp={false} />;
|
const tableCell = <TableCell value={longString} />;
|
||||||
|
|
||||||
|
const { container } = render(tableCell);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches snapshot unlimited', () => {
|
||||||
|
const longString = new Array(100).join('test_');
|
||||||
|
const tableCell = <TableCell value={longString} unlimited />;
|
||||||
|
|
||||||
|
const { container } = render(tableCell);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches snapshot unlimited (absolute max)', () => {
|
||||||
|
const longString = new Array(5000).join('test_');
|
||||||
|
const tableCell = <TableCell value={longString} unlimited />;
|
||||||
|
|
||||||
const { container } = render(tableCell);
|
const { container } = render(tableCell);
|
||||||
expect(container.firstChild).toMatchSnapshot();
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { ActionIcon } from '../action-icon/action-icon';
|
||||||
import './table-cell.scss';
|
import './table-cell.scss';
|
||||||
|
|
||||||
const MAX_CHARS_TO_SHOW = 50;
|
const MAX_CHARS_TO_SHOW = 50;
|
||||||
|
const ABSOLUTE_MAX_CHARS_TO_SHOW = 5000;
|
||||||
|
|
||||||
interface ShortParts {
|
interface ShortParts {
|
||||||
prefix: string;
|
prefix: string;
|
||||||
|
@ -46,13 +47,12 @@ function shortenString(str: string): ShortParts {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TableCellProps {
|
export interface TableCellProps {
|
||||||
value?: any;
|
value: any;
|
||||||
timestamp?: boolean;
|
unlimited?: boolean;
|
||||||
unparseable?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TableCell = React.memo(function TableCell(props: TableCellProps) {
|
export const TableCell = React.memo(function TableCell(props: TableCellProps) {
|
||||||
const { value, timestamp, unparseable } = props;
|
const { value, unlimited } = props;
|
||||||
const [showValue, setShowValue] = useState();
|
const [showValue, setShowValue] = useState();
|
||||||
|
|
||||||
function renderShowValueDialog(): JSX.Element | undefined {
|
function renderShowValueDialog(): JSX.Element | undefined {
|
||||||
|
@ -62,7 +62,19 @@ export const TableCell = React.memo(function TableCell(props: TableCellProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTruncated(str: string): JSX.Element {
|
function renderTruncated(str: string): JSX.Element {
|
||||||
if (str.length <= MAX_CHARS_TO_SHOW) return <span className="table-cell plain">{str}</span>;
|
if (str.length <= MAX_CHARS_TO_SHOW) {
|
||||||
|
return <span className="table-cell plain">{str}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unlimited) {
|
||||||
|
return (
|
||||||
|
<span className="table-cell plain">
|
||||||
|
{str.length < ABSOLUTE_MAX_CHARS_TO_SHOW
|
||||||
|
? str
|
||||||
|
: `${str.substr(0, ABSOLUTE_MAX_CHARS_TO_SHOW)}...`}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { prefix, omitted, suffix } = shortenString(str);
|
const { prefix, omitted, suffix } = shortenString(str);
|
||||||
return (
|
return (
|
||||||
|
@ -76,25 +88,21 @@ export const TableCell = React.memo(function TableCell(props: TableCellProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unparseable) {
|
if (value !== '' && value != null) {
|
||||||
return <span className="table-cell unparseable">error</span>;
|
if (value instanceof Date) {
|
||||||
} else if (value !== '' && value != null) {
|
|
||||||
if (timestamp) {
|
|
||||||
return (
|
return (
|
||||||
<span className="table-cell timestamp" title={value}>
|
<span className="table-cell timestamp" title={String(value.valueOf())}>
|
||||||
{new Date(value).toISOString()}
|
{value.toISOString()}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else if (Array.isArray(value)) {
|
} else if (Array.isArray(value)) {
|
||||||
return renderTruncated(`[${value.join(', ')}]`);
|
return renderTruncated(`[${value.join(', ')}]`);
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
return renderTruncated(JSON.stringify(value));
|
||||||
} else {
|
} else {
|
||||||
return renderTruncated(String(value));
|
return renderTruncated(String(value));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (timestamp) {
|
return <span className="table-cell null">null</span>;
|
||||||
return <span className="table-cell unparseable">unparseable timestamp</span>;
|
|
||||||
} else {
|
|
||||||
return <span className="table-cell null">null</span>;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -92,7 +92,7 @@ export const FilterTable = React.memo(function FilterTable(props: FilterTablePro
|
||||||
className: columnClassName,
|
className: columnClassName,
|
||||||
id: String(i),
|
id: String(i),
|
||||||
accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
|
accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
|
||||||
Cell: row => <TableCell value={row.value} timestamp={timestamp} />,
|
Cell: row => <TableCell value={timestamp ? new Date(row.value) : row.value} />,
|
||||||
};
|
};
|
||||||
})}
|
})}
|
||||||
defaultPageSize={50}
|
defaultPageSize={50}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import React from 'react';
|
||||||
import ReactTable from 'react-table';
|
import ReactTable from 'react-table';
|
||||||
|
|
||||||
import { TableCell } from '../../../components';
|
import { TableCell } from '../../../components';
|
||||||
|
import { TableCellUnparseable } from '../../../components/table-cell-unparseable/table-cell-unparseable';
|
||||||
import { caseInsensitiveContains, filterMap, parseJson } from '../../../utils';
|
import { caseInsensitiveContains, filterMap, parseJson } from '../../../utils';
|
||||||
import { FlattenField } from '../../../utils/ingestion-spec';
|
import { FlattenField } from '../../../utils/ingestion-spec';
|
||||||
import { HeaderAndRows, SampleEntry } from '../../../utils/sampler';
|
import { HeaderAndRows, SampleEntry } from '../../../utils/sampler';
|
||||||
|
@ -74,7 +75,7 @@ export const ParseDataTable = React.memo(function ParseDataTable(props: ParseDat
|
||||||
accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
|
accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
|
||||||
Cell: row => {
|
Cell: row => {
|
||||||
if (row.original.unparseable) {
|
if (row.original.unparseable) {
|
||||||
return <TableCell unparseable />;
|
return <TableCellUnparseable />;
|
||||||
}
|
}
|
||||||
return <TableCell value={row.value} />;
|
return <TableCell value={row.value} />;
|
||||||
},
|
},
|
||||||
|
|
|
@ -21,6 +21,7 @@ import React from 'react';
|
||||||
import ReactTable from 'react-table';
|
import ReactTable from 'react-table';
|
||||||
|
|
||||||
import { TableCell } from '../../../components';
|
import { TableCell } from '../../../components';
|
||||||
|
import { TableCellUnparseable } from '../../../components/table-cell-unparseable/table-cell-unparseable';
|
||||||
import { caseInsensitiveContains, filterMap } from '../../../utils';
|
import { caseInsensitiveContains, filterMap } from '../../../utils';
|
||||||
import { possibleDruidFormatForValues } from '../../../utils/druid-time';
|
import { possibleDruidFormatForValues } from '../../../utils/druid-time';
|
||||||
import {
|
import {
|
||||||
|
@ -122,9 +123,9 @@ export const ParseTimeTable = React.memo(function ParseTimeTable(props: ParseTim
|
||||||
return <TableCell value={row.original.error} />;
|
return <TableCell value={row.original.error} />;
|
||||||
}
|
}
|
||||||
if (row.original.unparseable) {
|
if (row.original.unparseable) {
|
||||||
return <TableCell unparseable />;
|
return <TableCellUnparseable timestamp={timestamp} />;
|
||||||
}
|
}
|
||||||
return <TableCell value={row.value} timestamp={timestamp} />;
|
return <TableCell value={timestamp ? new Date(row.value) : row.value} />;
|
||||||
},
|
},
|
||||||
minWidth: timestamp ? 200 : 100,
|
minWidth: timestamp ? 200 : 100,
|
||||||
resizable: !timestamp,
|
resizable: !timestamp,
|
||||||
|
|
|
@ -147,7 +147,7 @@ export const SchemaTable = React.memo(function SchemaTable(props: SchemaTablePro
|
||||||
className: columnClassName,
|
className: columnClassName,
|
||||||
id: String(i),
|
id: String(i),
|
||||||
accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
|
accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
|
||||||
Cell: row => <TableCell value={row.value} timestamp={timestamp} />,
|
Cell: row => <TableCell value={timestamp ? new Date(row.value) : row.value} />,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -101,7 +101,7 @@ export const TransformTable = React.memo(function TransformTable(props: Transfor
|
||||||
className: columnClassName,
|
className: columnClassName,
|
||||||
id: String(i),
|
id: String(i),
|
||||||
accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
|
accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
|
||||||
Cell: row => <TableCell value={row.value} timestamp={timestamp} />,
|
Cell: row => <TableCell value={timestamp ? new Date(row.value) : row.value} />,
|
||||||
};
|
};
|
||||||
})}
|
})}
|
||||||
defaultPageSize={50}
|
defaultPageSize={50}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -49,40 +49,7 @@ ORDER BY "Count" DESC`);
|
||||||
['JavaScript', 166, 1, 0],
|
['JavaScript', 166, 1, 0],
|
||||||
['Python', 62, 1, 0],
|
['Python', 62, 1, 0],
|
||||||
['HTML', 46, 1, 0],
|
['HTML', 46, 1, 0],
|
||||||
['Java', 42, 1, 0],
|
[],
|
||||||
['C++', 28, 1, 0],
|
|
||||||
['Go', 24, 1, 0],
|
|
||||||
['Ruby', 20, 1, 0],
|
|
||||||
['C#', 14, 1, 0],
|
|
||||||
['C', 13, 1, 0],
|
|
||||||
['CSS', 13, 1, 0],
|
|
||||||
['Shell', 12, 1, 0],
|
|
||||||
['Makefile', 10, 1, 0],
|
|
||||||
['PHP', 9, 1, 0],
|
|
||||||
['Scala', 8, 1, 0],
|
|
||||||
['HCL', 6, 1, 0],
|
|
||||||
['Jupyter Notebook', 6, 1, 0],
|
|
||||||
['Smarty', 4, 1, 0],
|
|
||||||
['Elm', 4, 1, 0],
|
|
||||||
['Roff', 3, 1, 0],
|
|
||||||
['Dockerfile', 3, 1, 0],
|
|
||||||
['Rust', 3, 1, 0],
|
|
||||||
['Dart', 2, 1, 0],
|
|
||||||
['LLVM', 2, 1, 0],
|
|
||||||
['Objective-C', 2, 1, 0],
|
|
||||||
['Julia', 2, 1, 0],
|
|
||||||
['PowerShell', 2, 1, 0],
|
|
||||||
['Swift', 2, 1, 0],
|
|
||||||
['Nim', 2, 1, 0],
|
|
||||||
['XSLT', 1, 1, 0],
|
|
||||||
['Lua', 1, 1, 0],
|
|
||||||
['Vim script', 1, 1, 0],
|
|
||||||
['Vue', 1, 1, 0],
|
|
||||||
['Lasso', 1, 1, 0],
|
|
||||||
['Clojure', 1, 1, 0],
|
|
||||||
['OCaml', 1, 1, 0],
|
|
||||||
['Chapel', 1, 1, 0],
|
|
||||||
['Kotlin', 1, 1, 0],
|
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
parsedQuery={parsedQuery}
|
parsedQuery={parsedQuery}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import ReactTable from 'react-table';
|
import ReactTable from 'react-table';
|
||||||
|
|
||||||
|
import { TableCell } from '../../../components';
|
||||||
import { ShowValueDialog } from '../../../dialogs/show-value-dialog/show-value-dialog';
|
import { ShowValueDialog } from '../../../dialogs/show-value-dialog/show-value-dialog';
|
||||||
import { copyAndAlert } from '../../../utils';
|
import { copyAndAlert } from '../../../utils';
|
||||||
import { BasicAction, basicActionsToMenu } from '../../../utils/basic-action';
|
import { BasicAction, basicActionsToMenu } from '../../../utils/basic-action';
|
||||||
|
@ -288,11 +289,10 @@ export const QueryOutput = React.memo(function QueryOutput(props: QueryOutputPro
|
||||||
accessor: String(i),
|
accessor: String(i),
|
||||||
Cell: row => {
|
Cell: row => {
|
||||||
const value = row.value;
|
const value = row.value;
|
||||||
if (!value) return value == null ? null : value;
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Popover content={getCellMenu(h, value)}>
|
<Popover content={getCellMenu(h, value)}>
|
||||||
<div>{value}</div>
|
<TableCell value={value} unlimited />
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue