diff --git a/changes/12476-ui-export-shows-all-columns b/changes/12476-ui-export-shows-all-columns
new file mode 100644
index 0000000000..35a558c335
--- /dev/null
+++ b/changes/12476-ui-export-shows-all-columns
@@ -0,0 +1 @@
+- Bug fix: Live query UI and Export data tables show all returned columns
diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
index b64349713c..ab6a3b4deb 100644
--- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
+++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
@@ -1285,7 +1285,6 @@ const ManageHostsPage = ({
{ [`${baseClass}__status-dropdown-sandbox`]: isSandboxMode }
);
- console.log("status", status);
return (
void;
onStopQuery: (evt: React.MouseEvent) => void;
setSelectedTargets: (value: ITarget[]) => void;
@@ -32,41 +35,16 @@ interface IQueryResultsProps {
}
const baseClass = "query-results";
-const CSV_QUERY_TITLE = "Query Results";
+const CSV_TITLE = "New Query";
const NAV_TITLES = {
RESULTS: "Results",
ERRORS: "Errors",
};
-const reorderCSVFields = (fields: string[]) => {
- const result = fields.filter((field) => field !== "host_display_name");
- result.unshift("host_display_name");
-
- return result;
-};
-
-const generateExportCSVFile = (rows: Row[], filename: string) => {
- return new global.window.File(
- [
- convertToCSV(
- rows.map((r) => r.original),
- reorderCSVFields
- ),
- ],
- filename,
- {
- type: "text/csv",
- }
- );
-};
-
-const generateExportFilename = (descriptor: string) => {
- return `${descriptor} (${format(new Date(), "MM-dd-yy hh-mm-ss")}).csv`;
-};
-
const QueryResults = ({
campaign,
isQueryFinished,
+ queryName,
onRunQuery,
onStopQuery,
setSelectedTargets,
@@ -82,10 +60,8 @@ const QueryResults = ({
const [showQueryModal, setShowQueryModal] = useState(false);
const [filteredResults, setFilteredResults] = useState([]);
const [filteredErrors, setFilteredErrors] = useState([]);
- const [tableHeaders, setTableHeaders] = useState([]);
- const [errorTableHeaders, setErrorTableHeaders] = useState(
- []
- );
+ const [tableHeaders, setTableHeaders] = useState([]);
+ const [errorTableHeaders, setErrorTableHeaders] = useState([]);
const [queryResultsForTableRender, setQueryResultsForTableRender] = useState(
queryResults
);
@@ -102,32 +78,43 @@ const QueryResults = ({
{ maxWait: 2000 }
);
+ // This is throwing an error not to use hook within a useEffect
useEffect(() => {
debounceQueryResults(queryResults);
}, [queryResults, debounceQueryResults]);
- // set tableHeaders when initial results come in
- // instead of memoizing tableHeaders, since we know the conditions exactly under which we want to
- // set these
useEffect(() => {
- if (tableHeaders?.length === 0 && !!queryResults?.length) {
- setTableHeaders(generateResultsTableHeaders(queryResults));
+ if (queryResults && queryResults.length > 0) {
+ const generatedTableHeaders = generateResultsTableHeaders(queryResults);
+ // Update tableHeaders if new headers are found
+ if (generatedTableHeaders !== tableHeaders) {
+ setTableHeaders(generatedTableHeaders);
+ }
}
- }, [tableHeaders, queryResults]);
+ }, [queryResults]); // Cannot use tableHeaders as it will cause infinite loop with setTableHeaders
useEffect(() => {
if (errorTableHeaders?.length === 0 && !!errors?.length) {
setErrorTableHeaders(generateResultsTableHeaders(errors));
+
+ if (errorTableHeaders && errorTableHeaders.length > 0) {
+ const generatedErrorTableHeaders = generateResultsTableHeaders(errors);
+
+ // Update errorTableHeaders if new headers are found
+ if (generatedErrorTableHeaders !== tableHeaders) {
+ setErrorTableHeaders(generatedErrorTableHeaders);
+ }
+ }
}
- }, [errorTableHeaders, errors]);
+ }, [errors]); // Cannot use errorTableHeaders as it will cause infinite loop with setErrorTableHeaders
const onExportQueryResults = (evt: React.MouseEvent) => {
evt.preventDefault();
-
FileSaver.saveAs(
- generateExportCSVFile(
+ generateCSVQueryResults(
filteredResults,
- generateExportFilename(CSV_QUERY_TITLE)
+ generateCSVFilename(`${queryName || CSV_TITLE} - Results`),
+ tableHeaders
)
);
};
@@ -136,9 +123,10 @@ const QueryResults = ({
evt.preventDefault();
FileSaver.saveAs(
- generateExportCSVFile(
+ generateCSVQueryResults(
filteredErrors,
- generateExportFilename(`${CSV_QUERY_TITLE} Errors`)
+ generateCSVFilename(`${queryName || CSV_TITLE} - Errors`),
+ errorTableHeaders
)
);
};
diff --git a/frontend/pages/queries/QueryPage/components/QueryResults/QueryResultsTableConfig.tsx b/frontend/pages/queries/QueryPage/components/QueryResults/QueryResultsTableConfig.tsx
index 33a19200bd..a9b9307d36 100644
--- a/frontend/pages/queries/QueryPage/components/QueryResults/QueryResultsTableConfig.tsx
+++ b/frontend/pages/queries/QueryPage/components/QueryResults/QueryResultsTableConfig.tsx
@@ -47,28 +47,28 @@ const _unshiftHostname = (headers: IDataColumn[]) => {
return newHeaders;
};
-const generateResultsTableHeaders = (results: unknown[]): Column[] => {
- // Table headers are derived from the shape of the first result.
- // Note: It is possible that results may vary from the shape of the first result.
- // For example, different versions of osquery may have new columns in a table
- // However, this is believed to be a very unlikely scenario and there have been
- // no reported issues.
- const shape = results[0];
- const keys =
- shape && typeof shape === "object" && isPlainObject(shape)
- ? Object.keys(shape)
- : [];
- const headers = keys.map((key) => {
+const generateResultsTableHeaders = (results: any[]): Column[] => {
+ /* Results include an array of objects, each representing a table row
+ Each key value pair in an object represents a column name and value
+ To create headers, use JS set to create an array of all unique column names */
+ const uniqueColumnNames = Array.from(
+ results.reduce(
+ (s, o) => Object.keys(o).reduce((t, k) => t.add(k), s),
+ new Set() // Set prevents listing duplicate headers
+ )
+ );
+
+ const headers = uniqueColumnNames.map((key) => {
return {
- id: key,
- title: key,
+ id: key as string,
+ title: key as string,
Header: (headerProps: IHeaderProps) => (
),
- accessor: key,
+ accessor: key as string,
Cell: (cellProps: ICellProps) => cellProps?.cell?.value || null,
Filter: DefaultColumnFilter,
// filterType: "text",
diff --git a/frontend/pages/queries/QueryPage/screens/RunQuery.tsx b/frontend/pages/queries/QueryPage/screens/RunQuery.tsx
index 34e19d39ec..dbceec8069 100644
--- a/frontend/pages/queries/QueryPage/screens/RunQuery.tsx
+++ b/frontend/pages/queries/QueryPage/screens/RunQuery.tsx
@@ -205,6 +205,7 @@ const RunQuery = ({
isQueryFinished={isQueryFinished}
setSelectedTargets={setSelectedTargets}
goToQueryEditor={goToQueryEditor}
+ queryName={storedQuery?.name}
targetsTotalCount={targetsTotalCount}
/>
);
diff --git a/frontend/utilities/convert_to_csv/convert_to_csv.tests.ts b/frontend/utilities/convert_to_csv/convert_to_csv.tests.ts
index e0f12e9fd2..5add0df5b1 100644
--- a/frontend/utilities/convert_to_csv/convert_to_csv.tests.ts
+++ b/frontend/utilities/convert_to_csv/convert_to_csv.tests.ts
@@ -13,7 +13,7 @@ const objArray = [
describe("convertToCSV - utility", () => {
it("converts an array of objects to CSV format", () => {
- expect(convertToCSV(objArray)).toEqual(
+ expect(convertToCSV({ objArray })).toEqual(
'"first_name","last_name"\n"Mike","Stone"\n"Paul","Simon"'
);
});
diff --git a/frontend/utilities/convert_to_csv/index.ts b/frontend/utilities/convert_to_csv/index.ts
index cd0a9ce5e3..2492b271d5 100644
--- a/frontend/utilities/convert_to_csv/index.ts
+++ b/frontend/utilities/convert_to_csv/index.ts
@@ -1,12 +1,25 @@
-import { keys } from "lodash";
+import { ICampaignError } from "interfaces/campaign";
+import { Row, Column } from "react-table";
const defaultFieldSortFunc = (fields: string[]) => fields;
-const convertToCSV = (
- objArray: any[],
- fieldSortFunc = defaultFieldSortFunc
-) => {
- const fields = fieldSortFunc(keys(objArray[0]));
+interface ConvertToCSV {
+ objArray: any; // TODO: typing
+ fieldSortFunc?: (fields: string[]) => string[];
+ tableHeaders?: any[]; // TODO: typing
+}
+
+const convertToCSV = ({
+ objArray,
+ fieldSortFunc = defaultFieldSortFunc,
+ tableHeaders,
+}: ConvertToCSV) => {
+ const tableHeadersStrings: string[] = tableHeaders
+ ? tableHeaders.map((header: { id: string }) => header.id) // TODO: typing
+ : Object.keys(objArray[0]);
+
+ const fields = fieldSortFunc(tableHeadersStrings);
+
// TODO: Remove after v5 when host_hostname is removed rom API response.
const hostNameIndex = fields.indexOf("host_hostname");
if (hostNameIndex >= 0) {
@@ -14,7 +27,8 @@ const convertToCSV = (
}
// Remove end
const jsonFields = fields.map((field) => JSON.stringify(field));
- const rows = objArray.map((row) => {
+ const rows = objArray.map((row: any) => {
+ // TODO: typing
return fields.map((field) => JSON.stringify(row[field])).join(",");
});
diff --git a/frontend/utilities/generate_csv/index.ts b/frontend/utilities/generate_csv/index.ts
new file mode 100644
index 0000000000..8ee514ef93
--- /dev/null
+++ b/frontend/utilities/generate_csv/index.ts
@@ -0,0 +1,63 @@
+import convertToCSV from "utilities/convert_to_csv";
+import { Row, Column } from "react-table";
+import { ICampaignError } from "interfaces/campaign";
+import { format } from "date-fns";
+
+const reorderCSVFields = (tableHeaders: string[]) => {
+ const result = tableHeaders.filter((field) => field !== "host_display_name");
+ result.unshift("host_display_name");
+
+ return result;
+};
+
+export const generateCSVFilename = (descriptor: string) => {
+ return `${descriptor} (${format(new Date(), "MM-dd-yy hh-mm-ss")}).csv`;
+};
+
+// Query results and query errors
+export const generateCSVQueryResults = (
+ rows: Row[],
+ filename: string,
+ tableHeaders: Column[] | string[]
+) => {
+ return new global.window.File(
+ [
+ convertToCSV({
+ objArray: rows.map((r) => r.original),
+ fieldSortFunc: reorderCSVFields,
+ tableHeaders,
+ }),
+ ],
+ filename,
+ {
+ type: "text/csv",
+ }
+ );
+};
+
+// Policy results only
+export const generateCSVPolicyResults = (
+ rows: { host: string; status: string }[],
+ filename: string
+) => {
+ return new global.window.File([convertToCSV({ objArray: rows })], filename, {
+ type: "text/csv",
+ });
+};
+
+// Policy errors only
+export const generateCSVPolicyErrors = (
+ rows: ICampaignError[],
+ filename: string
+) => {
+ return new global.window.File([convertToCSV({ objArray: rows })], filename, {
+ type: "text/csv",
+ });
+};
+
+export default {
+ generateCSVFilename,
+ generateCSVQueryResults,
+ generateCSVPolicyResults,
+ generateCSVPolicyErrors,
+};