From cf3bd93952bbf0a7b1b8f3c2cb3f7d6d6b7dc5f8 Mon Sep 17 00:00:00 2001
From: Jeremy Foster
Date: Mon, 18 Mar 2024 16:11:35 -0700
Subject: [PATCH] HOSTSD-237 Export to Excel
---
.../Controllers/ServerItemController.cs | 67 ++++++++++-
src/api/Helpers/IXlsExporter.cs | 8 ++
src/api/Helpers/XlsExporter.cs | 109 ++++++++++++++++++
.../server-items/history/export/route.ts | 6 +
src/dashboard/src/app/client/servers/page.tsx | 16 ++-
src/dashboard/src/app/hsb/servers/page.tsx | 16 ++-
.../allocationTable/AllocationTable.tsx | 23 +++-
.../bar/allocationByOS/AllocationByOS.tsx | 11 +-
.../AllocationByStorageVolume.tsx | 19 ++-
.../segmentedBarChart/SegmentedBarChart.tsx | 19 ++-
.../charts/bar/smallBar/SmallBarChart.tsx | 8 +-
.../allOrganizations/AllOrganizations.tsx | 19 ++-
.../src/components/charts/line/LineChart.tsx | 13 ++-
.../storageTrends/StorageTrendsChart.tsx | 47 ++++----
.../src/components/dashboard/Dashboard.tsx | 77 ++++++++++++-
.../hooks/api/interfaces/IServerItemFilter.ts | 2 +
.../src/hooks/api/useApiServerItems.ts | 27 ++++-
src/dashboard/src/hooks/index.ts | 1 +
src/dashboard/src/hooks/useDownload.ts | 34 ++++++
src/dashboard/src/styles/_globals.scss | 1 -
.../dal/Services/IServerHistoryItemService.cs | 8 +-
src/libs/dal/Services/IServerItemService.cs | 4 +-
.../dal/Services/ServerHistoryItemService.cs | 76 ++++++++++--
src/libs/dal/Services/ServerItemService.cs | 24 +++-
src/libs/models/Filters/ServerItemFilter.cs | 8 ++
25 files changed, 567 insertions(+), 76 deletions(-)
create mode 100644 src/dashboard/src/app/api/dashboard/server-items/history/export/route.ts
create mode 100644 src/dashboard/src/hooks/useDownload.ts
diff --git a/src/api/Areas/Dashboard/Controllers/ServerItemController.cs b/src/api/Areas/Dashboard/Controllers/ServerItemController.cs
index fd5bfb41..498d7a1d 100644
--- a/src/api/Areas/Dashboard/Controllers/ServerItemController.cs
+++ b/src/api/Areas/Dashboard/Controllers/ServerItemController.cs
@@ -214,8 +214,6 @@ public IActionResult FindHistory()
}
}
- // TODO: Complete functionality
- // TODO: Limit based on role and tenant.
///
/// Export the server items to Excel.
///
@@ -228,11 +226,72 @@ public IActionResult FindHistory()
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
[SwaggerOperation(Tags = new[] { "Server Item" })]
- public IActionResult Export(string format, string name = "service-now")
+ public IActionResult Export(string format = "excel", string name = "service-now")
{
+ var uri = new Uri(this.Request.GetDisplayUrl());
+ var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query);
+ var filter = new HSB.Models.Filters.ServerItemFilter(query);
+
if (format == "excel")
{
- var items = _serverItemService.Find(a => true);
+ IEnumerable items;
+ var isHSB = this.User.HasClientRole(ClientRole.HSB);
+ if (isHSB)
+ {
+ items = _serverItemService.Find(filter, true);
+ }
+ else
+ {
+ var user = _authorization.GetUser();
+ if (user == null) return Forbid();
+ items = _serverItemService.FindForUser(user.Id, filter, true);
+ }
+
+ var workbook = _exporter.GenerateExcel(name, items);
+
+ using var stream = new MemoryStream();
+ workbook.Write(stream);
+ var bytes = stream.ToArray();
+
+ return File(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+ }
+
+ throw new NotImplementedException("Format 'csv' not implemented yet");
+ }
+
+ ///
+ /// Export the server history items to Excel.
+ ///
+ ///
+ ///
+ ///
+ ///
+ [HttpGet("history/export")]
+ [Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")]
+ [ProducesResponseType((int)HttpStatusCode.OK)]
+ [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
+ [SwaggerOperation(Tags = new[] { "Server Item" })]
+ public IActionResult ExportHistory(string format = "excel", string name = "service-now")
+ {
+ var uri = new Uri(this.Request.GetDisplayUrl());
+ var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query);
+ var filter = new HSB.Models.Filters.ServerHistoryItemFilter(query);
+
+ if (format == "excel")
+ {
+ IEnumerable items;
+ var isHSB = this.User.HasClientRole(ClientRole.HSB);
+ if (isHSB)
+ {
+ items = _serverHistoryItemService.FindHistoryByMonth(filter.StartDate ?? DateTime.UtcNow, filter.EndDate, filter.TenantId, filter.OrganizationId, filter.OperatingSystemItemId, filter.ServiceNowKey, true);
+ }
+ else
+ {
+ var user = _authorization.GetUser();
+ if (user == null) return Forbid();
+ items = _serverHistoryItemService.FindHistoryByMonthForUser(user.Id, filter.StartDate ?? DateTime.UtcNow, filter.EndDate, filter.TenantId, filter.OrganizationId, filter.OperatingSystemItemId, filter.ServiceNowKey, true);
+ }
+
var workbook = _exporter.GenerateExcel(name, items);
using var stream = new MemoryStream();
diff --git a/src/api/Helpers/IXlsExporter.cs b/src/api/Helpers/IXlsExporter.cs
index 38c00608..b26b1566 100644
--- a/src/api/Helpers/IXlsExporter.cs
+++ b/src/api/Helpers/IXlsExporter.cs
@@ -31,5 +31,13 @@ public interface IXlsExporter
///
///
XSSFWorkbook GenerateExcel(string sheetName, IEnumerable items);
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ XSSFWorkbook GenerateExcel(string sheetName, IEnumerable items);
#endregion
}
diff --git a/src/api/Helpers/XlsExporter.cs b/src/api/Helpers/XlsExporter.cs
index 2c501d97..3b9a5a6b 100644
--- a/src/api/Helpers/XlsExporter.cs
+++ b/src/api/Helpers/XlsExporter.cs
@@ -80,9 +80,118 @@ public XSSFWorkbook GenerateExcel(string sheetName, IEnumerable
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public XSSFWorkbook GenerateExcel(string sheetName, IEnumerable items)
+ {
+ if (items == null) throw new ArgumentNullException(nameof(items));
+
+ var workbook = new XSSFWorkbook();
+ var sheet = (XSSFSheet)workbook.CreateSheet(sheetName);
+ var rowIndex = 0;
+
+ // Header
+ AddHeaders(sheet.CreateRow(rowIndex++), new[] { "Date", "Tenant", "Org Code", "Organization", "ServiceNowKey", "Name", "OS", "Capacity (bytes)", "Available Space (bytes)" });
+
+ // Content
+ foreach (var item in items)
+ {
+ AddContent(workbook, sheet.CreateRow(rowIndex++), item);
+ }
+
+ return workbook;
+ }
+
+ private static void AddHeaders(IRow row, string[] labels)
+ {
+ for (var i = 0; i < labels.Length; i++)
+ {
+ var cell = row.CreateCell(i);
+ cell.SetCellValue(labels[i]);
+ }
+ }
+
+ private static void AddContent(IWorkbook workbook, IRow row, Entities.ServerItem item)
+ {
+ var numberStyle = workbook.CreateCellStyle();
+ var numberFormat = workbook.CreateDataFormat().GetFormat("#,#0");
+ numberStyle.DataFormat = numberFormat;
+
+ var cell0 = row.CreateCell(0);
+ cell0.SetCellValue(item.Tenant?.Code);
+ var cell1 = row.CreateCell(1);
+ cell1.SetCellValue(item.Organization?.Code);
+ var cell2 = row.CreateCell(2);
+ cell2.SetCellValue(item.Organization?.Name);
+ var cell3 = row.CreateCell(3);
+ cell3.SetCellValue(item.ServiceNowKey);
+ var cell4 = row.CreateCell(4);
+ cell4.SetCellValue(item.Name);
+ var cell5 = row.CreateCell(5);
+ cell5.SetCellValue(item.OperatingSystemItem?.Name);
+ var cell6 = row.CreateCell(6);
+ cell6.SetCellType(CellType.Numeric);
+ cell6.CellStyle = numberStyle;
+ cell6.SetCellValue(item.Capacity ?? 0);
+ var cell7 = row.CreateCell(7);
+ cell7.SetCellType(CellType.Numeric);
+ cell7.CellStyle = numberStyle;
+ cell7.SetCellValue(item.AvailableSpace ?? 0);
+ }
+
+ private static void AddContent(IWorkbook workbook, IRow row, Entities.ServerHistoryItem item)
+ {
+ var numberStyle = workbook.CreateCellStyle();
+ var numberFormat = workbook.CreateDataFormat().GetFormat("#,#0");
+ numberStyle.DataFormat = numberFormat;
+
+ var dateStyle = workbook.CreateCellStyle();
+ var dateFormat = workbook.CreateDataFormat().GetFormat("yyyy/MM/dd");
+ dateStyle.DataFormat = dateFormat;
+
+ var cell0 = row.CreateCell(0);
+ cell0.SetCellValue(item.CreatedOn.Date);
+ cell0.CellStyle = dateStyle;
+ var cell1 = row.CreateCell(1);
+ cell1.SetCellValue(item.Tenant?.Code);
+ var cell2 = row.CreateCell(2);
+ cell2.SetCellValue(item.Organization?.Code);
+ var cell3 = row.CreateCell(3);
+ cell3.SetCellValue(item.Organization?.Name);
+ var cell4 = row.CreateCell(4);
+ cell4.SetCellValue(item.ServiceNowKey);
+ var cell5 = row.CreateCell(5);
+ cell5.SetCellValue(item.Name);
+ var cell6 = row.CreateCell(6);
+ cell6.SetCellValue(item.OperatingSystemItem?.Name);
+ var cell7 = row.CreateCell(7);
+ cell7.SetCellType(CellType.Numeric);
+ cell7.CellStyle = numberStyle;
+ cell7.SetCellValue(item.Capacity ?? 0);
+ var cell8 = row.CreateCell(8);
+ cell8.SetCellType(CellType.Numeric);
+ cell8.CellStyle = numberStyle;
+ cell8.SetCellValue(item.AvailableSpace ?? 0);
+ }
#endregion
#region Helpers
diff --git a/src/dashboard/src/app/api/dashboard/server-items/history/export/route.ts b/src/dashboard/src/app/api/dashboard/server-items/history/export/route.ts
new file mode 100644
index 00000000..6876ca4f
--- /dev/null
+++ b/src/dashboard/src/app/api/dashboard/server-items/history/export/route.ts
@@ -0,0 +1,6 @@
+import { dispatch } from '@/app/api/utils';
+
+export async function GET(req: Request, context: { params: any }) {
+ const url = new URL(req.url);
+ return await dispatch(`/v1/dashboard/server-items/history/export${url.search}`);
+}
diff --git a/src/dashboard/src/app/client/servers/page.tsx b/src/dashboard/src/app/client/servers/page.tsx
index fabda785..d80c18b8 100644
--- a/src/dashboard/src/app/client/servers/page.tsx
+++ b/src/dashboard/src/app/client/servers/page.tsx
@@ -2,7 +2,7 @@
import { AllocationTable, useDashboardFilter } from '@/components';
import { LoadingAnimation } from '@/components/loadingAnimation';
-import { useAuth } from '@/hooks';
+import { useApiServerItems, useAuth } from '@/hooks';
import {
useOperatingSystemItems,
useOrganizations,
@@ -11,6 +11,7 @@ import {
} from '@/hooks/lists';
import { useFilteredStore } from '@/store';
import { redirect, useRouter } from 'next/navigation';
+import { toast } from 'react-toastify';
export default function Page() {
const router = useRouter();
@@ -27,6 +28,7 @@ export default function Page() {
const setFilteredValues = useFilteredStore((state) => state.setValues);
const setFilteredOrganizations = useFilteredStore((state) => state.setOrganizations);
const setFilteredServerItems = useFilteredStore((state) => state.setServerItems);
+ const { download } = useApiServerItems();
const updateDashboard = useDashboardFilter();
@@ -74,6 +76,18 @@ export default function Page() {
router.push(`/client/dashboard?serverItem=${serverItem?.serviceNowKey}`);
}
}}
+ showExport
+ onExport={async (search) => {
+ try {
+ await download({
+ search: search ? search : undefined,
+ });
+ } catch (ex) {
+ const error = ex as Error;
+ toast.error('Failed to download data. ' + error.message);
+ console.error(error);
+ }
+ }}
/>
);
}
diff --git a/src/dashboard/src/app/hsb/servers/page.tsx b/src/dashboard/src/app/hsb/servers/page.tsx
index 0268e46a..84e80e50 100644
--- a/src/dashboard/src/app/hsb/servers/page.tsx
+++ b/src/dashboard/src/app/hsb/servers/page.tsx
@@ -2,7 +2,7 @@
import { AllocationTable, useDashboardFilter } from '@/components';
import { LoadingAnimation } from '@/components/loadingAnimation';
-import { useAuth } from '@/hooks';
+import { useApiServerItems, useAuth } from '@/hooks';
import {
useOperatingSystemItems,
useOrganizations,
@@ -11,6 +11,7 @@ import {
} from '@/hooks/lists';
import { useFilteredStore } from '@/store';
import { redirect, useRouter } from 'next/navigation';
+import { toast } from 'react-toastify';
export default function Page() {
const router = useRouter();
@@ -27,6 +28,7 @@ export default function Page() {
const setFilteredValues = useFilteredStore((state) => state.setValues);
const setFilteredOrganizations = useFilteredStore((state) => state.setOrganizations);
const setFilteredServerItems = useFilteredStore((state) => state.setServerItems);
+ const { download } = useApiServerItems();
const updateDashboard = useDashboardFilter();
@@ -74,6 +76,18 @@ export default function Page() {
router.push(`/hsb/dashboard?serverItem=${serverItem?.serviceNowKey}`);
}
}}
+ showExport
+ onExport={async (search) => {
+ try {
+ await download({
+ search: search ? search : undefined,
+ });
+ } catch (ex) {
+ const error = ex as Error;
+ toast.error('Failed to download data. ' + error.message);
+ console.error(error);
+ }
+ }}
/>
);
}
diff --git a/src/dashboard/src/components/charts/allocationTable/AllocationTable.tsx b/src/dashboard/src/components/charts/allocationTable/AllocationTable.tsx
index 0bbec08b..08a35edf 100644
--- a/src/dashboard/src/components/charts/allocationTable/AllocationTable.tsx
+++ b/src/dashboard/src/components/charts/allocationTable/AllocationTable.tsx
@@ -20,8 +20,11 @@ export interface IAllocationTableProps {
operatingSystemId?: number;
serverItems: IServerItemListModel[];
loading?: boolean;
- onClick?: (serverItem?: IServerItemListModel) => void;
margin?: number;
+ showExport?: boolean;
+ exportDisabled?: boolean;
+ onExport?: (search: string) => void;
+ onClick?: (serverItem?: IServerItemListModel) => void;
}
export const AllocationTable = ({
@@ -29,8 +32,11 @@ export const AllocationTable = ({
operatingSystemId,
serverItems,
loading,
- onClick,
margin,
+ showExport,
+ exportDisabled,
+ onExport,
+ onClick,
}: IAllocationTableProps) => {
const getServerItems = useAllocationByOS(osClassName, operatingSystemId);
@@ -191,9 +197,16 @@ export const AllocationTable = ({
))}
-
+ {showExport && (
+
+ )}
);
};
diff --git a/src/dashboard/src/components/charts/bar/allocationByOS/AllocationByOS.tsx b/src/dashboard/src/components/charts/bar/allocationByOS/AllocationByOS.tsx
index c487f0bb..f1e34554 100644
--- a/src/dashboard/src/components/charts/bar/allocationByOS/AllocationByOS.tsx
+++ b/src/dashboard/src/components/charts/bar/allocationByOS/AllocationByOS.tsx
@@ -10,6 +10,9 @@ export interface IAllocationByOSProps {
serverItems: IServerItemListModel[];
operatingSystemItems: IOperatingSystemItemListModel[];
loading?: boolean;
+ showExport?: boolean;
+ exportDisabled?: boolean;
+ onExport?: () => void;
onClick?: (operatingSystemItem?: IOperatingSystemItemListModel) => void;
}
@@ -17,14 +20,18 @@ export const AllocationByOS = ({
serverItems,
operatingSystemItems,
loading,
+ showExport,
+ exportDisabled,
+ onExport,
onClick,
}: IAllocationByOSProps) => {
return (
{}}
+ showExport={showExport}
+ exportDisabled={exportDisabled}
+ onExport={onExport}
loading={loading}
>
{(data) => {
diff --git a/src/dashboard/src/components/charts/bar/allocationByStorageVolume/AllocationByStorageVolume.tsx b/src/dashboard/src/components/charts/bar/allocationByStorageVolume/AllocationByStorageVolume.tsx
index a8a0fdb3..e29e2d0b 100644
--- a/src/dashboard/src/components/charts/bar/allocationByStorageVolume/AllocationByStorageVolume.tsx
+++ b/src/dashboard/src/components/charts/bar/allocationByStorageVolume/AllocationByStorageVolume.tsx
@@ -18,6 +18,9 @@ export interface IAllocationByStorageVolumeProps {
serverItems: IServerItemListModel[];
loading?: boolean;
onClick?: (organization: IOrganizationListModel) => void;
+ showExport?: boolean;
+ exportDisabled?: boolean;
+ onExport?: (search: string) => void;
}
export const AllocationByStorageVolume = ({
@@ -25,6 +28,9 @@ export const AllocationByStorageVolume = ({
serverItems,
loading,
onClick,
+ showExport,
+ exportDisabled,
+ onExport,
}: IAllocationByStorageVolumeProps) => {
const [sortOption, setSortOption] = React.useState(0);
const [search, setSearch] = React.useState('');
@@ -103,9 +109,16 @@ export const AllocationByStorageVolume = ({
Used
Unused
-
+ {showExport && (
+
+ )}
);
};
diff --git a/src/dashboard/src/components/charts/bar/segmentedBarChart/SegmentedBarChart.tsx b/src/dashboard/src/components/charts/bar/segmentedBarChart/SegmentedBarChart.tsx
index 697fe34a..17eefbe5 100644
--- a/src/dashboard/src/components/charts/bar/segmentedBarChart/SegmentedBarChart.tsx
+++ b/src/dashboard/src/components/charts/bar/segmentedBarChart/SegmentedBarChart.tsx
@@ -24,6 +24,9 @@ export interface ISegmentedBarChart {
loading?: boolean;
dateRange?: string[];
minColumns?: number;
+ showExport?: boolean;
+ exportDisabled?: boolean;
+ onExport?: () => void;
}
export const SegmentedBarChart = ({
@@ -32,6 +35,9 @@ export const SegmentedBarChart = ({
loading,
dateRange: initDateRange,
minColumns = 12,
+ showExport,
+ exportDisabled,
+ onExport,
}: ISegmentedBarChart) => {
const getStorageTrends = useStorageTrendsData();
const dateRange = useStorageTrendsStore((state) => state.dateRangeFileSystemHistoryItems);
@@ -131,9 +137,16 @@ export const SegmentedBarChart = ({
-
+ {showExport && (
+
+ )}
);
};
diff --git a/src/dashboard/src/components/charts/bar/smallBar/SmallBarChart.tsx b/src/dashboard/src/components/charts/bar/smallBar/SmallBarChart.tsx
index d387b8b5..39e5304d 100644
--- a/src/dashboard/src/components/charts/bar/smallBar/SmallBarChart.tsx
+++ b/src/dashboard/src/components/charts/bar/smallBar/SmallBarChart.tsx
@@ -10,6 +10,7 @@ export interface ISmallBarChartProps> {
title?: string;
/** Data to be displayed in the bar chart */
data: IBarChartData;
+ showExport?: boolean;
/** Whether the export to Excel is disabled */
exportDisabled?: boolean;
/** Event fires when the export to Excel is clicked */
@@ -28,9 +29,10 @@ export const SmallBarChart = >({
title,
data,
children,
+ loading,
+ showExport,
exportDisabled,
onExport,
- loading,
}: ISmallBarChartProps) => {
return (
@@ -59,12 +61,12 @@ export const SmallBarChart = >({
})}
- {onExport && (
+ {showExport && (
diff --git a/src/dashboard/src/components/charts/doughnut/allOrganizations/AllOrganizations.tsx b/src/dashboard/src/components/charts/doughnut/allOrganizations/AllOrganizations.tsx
index 18e906f8..e14ff74f 100644
--- a/src/dashboard/src/components/charts/doughnut/allOrganizations/AllOrganizations.tsx
+++ b/src/dashboard/src/components/charts/doughnut/allOrganizations/AllOrganizations.tsx
@@ -16,12 +16,18 @@ export interface IAllOrganizationsProps {
organizations: IOrganizationListModel[];
serverItems: IServerItemListModel[];
loading?: boolean;
+ showExport?: boolean;
+ exportDisabled?: boolean;
+ onExport?: () => void;
}
export const AllOrganizations = ({
organizations,
serverItems,
loading,
+ showExport,
+ exportDisabled,
+ onExport,
}: IAllOrganizationsProps) => {
const [data, setData] = React.useState(generateAllOrganizationsDoughnutChart(serverItems));
@@ -79,9 +85,16 @@ export const AllOrganizations = ({
-
+ {showExport && (
+
+ )}
);
};
diff --git a/src/dashboard/src/components/charts/line/LineChart.tsx b/src/dashboard/src/components/charts/line/LineChart.tsx
index 61c83207..3f1a2581 100644
--- a/src/dashboard/src/components/charts/line/LineChart.tsx
+++ b/src/dashboard/src/components/charts/line/LineChart.tsx
@@ -29,9 +29,10 @@ interface LineChartProps, TLabel = unknown> {
filter?: React.ReactNode;
disclaimer?: React.ReactNode;
data: ChartData<'line', TData, TLabel>;
+ loading?: boolean;
showExport?: boolean;
exportDisabled?: boolean;
- loading?: boolean;
+ onExport?: () => void;
}
export const LineChart = <
@@ -45,9 +46,10 @@ export const LineChart = <
options = defaultChartOptions,
filter,
disclaimer,
+ loading,
showExport,
exportDisabled,
- loading,
+ onExport,
}: LineChartProps) => {
return (
@@ -59,7 +61,12 @@ export const LineChart = <
{showExport && (
-