Skip to content

Commit 405f7ec

Browse files
authored
PDF download and error with maximum limit message (#162)
1 parent 1b8c87c commit 405f7ec

File tree

10 files changed

+114
-34
lines changed

10 files changed

+114
-34
lines changed

src/backend/api/v1/report/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ def csv(self, request: Request) -> Response:
336336

337337
@action(methods=["post"], detail=False)
338338
def pdf(self, request: Request) -> WeasyTemplateResponse:
339-
table_qs = self.filter_queryset(self.get_base_queryset())
339+
table_qs = self.filter_queryset(self.get_base_queryset())[:settings.MAX_PDF_JOB_TEMPLATES]
340340

341341
currency_value = Settings.currency()
342342
currency_sign = "$"

src/backend/api/v1/template_options/views.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
Job,
2929
Costs, max_minutes_input, min_minutes_input)
3030
from backend.apps.common.models import Currency, Settings, FilterSet
31+
from backend.django_config import settings
3132

3233

3334
class TemplateOptionsView(AdminOnlyViewSet):
@@ -65,6 +66,7 @@ def list(self, request: Request) -> Response:
6566
"currency": Settings.currency(),
6667
"enable_template_creation_time": Settings.enable_template_creation_time(),
6768
"filter_sets": FilterSetSerializer(filter_sets, many=True).data,
69+
"max_pdf_job_templates": settings.MAX_PDF_JOB_TEMPLATES,
6870
}
6971

7072
return Response(

src/backend/django_config/settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,10 @@
348348
# Initial sync date (overrides INITIAL_SYNC_DAYS)
349349
INITIAL_SYNC_SINCE = '2025-08-08'
350350

351+
352+
# PDF Download (Do not exceed the number 4000)
353+
MAX_PDF_JOB_TEMPLATES = 4000
354+
351355
### Local settings
352356
local_config_file = os.path.join(BASE_DIR, "django_config", "local_settings.py")
353357
try:

src/backend/tests/unit/test_views.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
from backend.apps.common.models import FilterSet, Currency, Settings, SettingsChoices
1515

1616
test_template_expected_data = {
17-
'clusters': [{'id': 1, 'address': 'localhost'}],
17+
'clusters': [
18+
{'id': 1, 'address': 'localhost'}
19+
],
1820
'currencies': [
1921
{'id': 2, 'name': 'EUR', 'iso_code': 'EUR', 'symbol': '€'},
2022
{'id': 1, 'name': 'United States Dollar', 'iso_code': 'USD', 'symbol': '$'}
@@ -23,9 +25,9 @@
2325
{'key': 2, 'value': 'Organization A', 'cluster_id': 1}
2426
],
2527
'labels': [
26-
{'cluster_id': 1, 'key': 1, 'value': 'Label A'},
27-
{'cluster_id': 1, 'key': 2, 'value': 'Label B'},
28-
{'cluster_id': 1, 'key': 3, 'value': 'Label C'}
28+
{'key': 1, 'value': 'Label A', 'cluster_id': 1},
29+
{'key': 2, 'value': 'Label B', 'cluster_id': 1},
30+
{'key': 3, 'value': 'Label C', 'cluster_id': 1}
2931
],
3032
'date_ranges': [
3133
{'key': 'last_year', 'value': 'Past year'},
@@ -45,14 +47,32 @@
4547
],
4648
'projects': [
4749
{'key': 1, 'value': 'Project A', 'cluster_id': 1},
48-
{'key': 2, 'value': 'Project B', 'cluster_id': 1}],
50+
{'key': 2, 'value': 'Project B', 'cluster_id': 1}
51+
],
4952
'manual_cost_automation': 50.0,
5053
'automated_process_cost': 20.0,
5154
'currency': 1,
5255
'enable_template_creation_time': True,
5356
'filter_sets': [
54-
{'id': 1, 'name': 'Report 1', 'filters': {'date_range': 'last_month', 'organization': [1, 2]}},
55-
{'id': 2, 'name': 'Report 2', 'filters': {'date_range': 'last_year', 'template': [1, 2]}}]}
57+
{
58+
'id': 1,
59+
'name': 'Report 1',
60+
'filters': {
61+
'date_range': 'last_month',
62+
'organization': [1, 2]
63+
}
64+
},
65+
{
66+
'id': 2,
67+
'name': 'Report 2',
68+
'filters': {
69+
'template': [1, 2],
70+
'date_range': 'last_year'
71+
}
72+
}
73+
],
74+
'max_pdf_job_templates': 4000
75+
}
5676

5777
test_report_expected_data = {
5878
'count': 1,
@@ -222,6 +242,7 @@ def mock_auth(superuser):
222242
mock_authenticate.return_value = superuser, None
223243
yield mock_authenticate
224244

245+
225246
@pytest.fixture(scope="function")
226247
def mock_auth_user(regularuser):
227248
with mock.patch("backend.apps.aap_auth.authentication.AAPAuthentication.authenticate") as mock_authenticate:
@@ -264,6 +285,7 @@ def test_template_options(self, mock_auth, currencies, projects, jobs, filter_se
264285
response = client.get("/api/v1/template_options/")
265286
assert response.status_code == 200
266287
data = response.json()
288+
print(data)
267289
assert data == expected
268290

269291
@pytest.mark.parametrize('expected', [test_report_expected_data])

src/frontend/app/Dashboard/Dashboard.tsx

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import '@patternfly/react-styles/css/utilities/Spacing/spacing.css';
44
import '@patternfly/react-styles/css/utilities/Sizing/sizing.css';
55
import '@patternfly/react-styles/css/utilities/Text/text.css';
66
import '@patternfly/react-styles/css/utilities/Flex/flex.css';
7-
import { Alert, Flex, FlexItem, Grid, GridItem, Spinner, Toolbar, ToolbarItem } from '@patternfly/react-core';
7+
import { Alert, Button, Flex, FlexItem, Grid, GridItem, HelperText, HelperTextItem, Modal, ModalBody, ModalFooter, ModalHeader, Spinner, Toolbar, ToolbarItem } from '@patternfly/react-core';
88
import { RestService } from '@app/Services';
99
import { deepClone, svgToPng } from '@app/Utils';
1010
import {
@@ -15,20 +15,20 @@ import {
1515
RequestFilter,
1616
TableResponse,
1717
TableResult,
18-
UrlParams,
18+
UrlParams
1919
} from '@app/Types';
2020
import ErrorState from '@patternfly/react-component-groups/dist/dynamic/ErrorState';
2121
import {
2222
DashboardBarChart,
2323
DashboardLineChart,
2424
DashboardTable,
2525
DashboardTopTable,
26-
DashboardTotalCards,
26+
DashboardTotalCards
2727
} from '@app/Dashboard';
2828
import { CurrencySelector } from '@app/Components';
2929

3030
import useFilterStore from '@app/Store/filterStore';
31-
import useCommonStore from '@app/Store/commonStore';
31+
import useCommonStore from '@app/Store/commonStore';
3232
import {
3333
useAutomatedProcessCost,
3434
useFilterRetrieveError,
@@ -51,7 +51,7 @@ const Dashboard: React.FunctionComponent = () => {
5151
const [requestParams, setRequestParams] = React.useState<RequestFilter>();
5252
const [paginationParams, setPaginationParams] = React.useState<PaginationParams>({
5353
page: 1,
54-
page_size: 10,
54+
page_size: 10
5555
} as PaginationParams);
5656
const [ordering, setOrdering] = React.useState<string>('name');
5757
const hourly_manual_costs = useManualCostAutomation();
@@ -66,6 +66,7 @@ const Dashboard: React.FunctionComponent = () => {
6666
const saveEnableTemplateCreationTime = useCommonStore((state) => state.saveEnableTemplateCreationTime);
6767
const setAutomatedProcessCost = useFilterStore((state) => state.setAutomatedProcessCost);
6868
const setManualProcessCost = useFilterStore((state) => state.setManualProcessCost);
69+
const maxPDFJobTemplates = useFilterStore((state) => state.max_pdf_job_templates);
6970
const reloadData = useFilterStore((state)=>state.reloadData);
7071
const setReloadData = useFilterStore((state) => state.setReloadData);
7172
const handelError = (error: unknown) => {
@@ -75,6 +76,7 @@ const Dashboard: React.FunctionComponent = () => {
7576
setTableLoading(false);
7677
}
7778
};
79+
const [isPDFWarningModalOpen, setPDFWarningModalOpen] = React.useState<boolean>(false);
7880

7981
const fetchServerReportDetails = (signal: AbortSignal) => {
8082
if (!requestParams) {
@@ -112,7 +114,7 @@ const Dashboard: React.FunctionComponent = () => {
112114
RestService.fetchReports(controller.current.signal, {
113115
...requestParams,
114116
...paginationParams,
115-
...{ ordering: ordering },
117+
...{ ordering: ordering }
116118
} as UrlParams)
117119
.then((tableResponse) => {
118120
setTableData(tableResponse);
@@ -137,7 +139,7 @@ const Dashboard: React.FunctionComponent = () => {
137139
() => {
138140
fetchServerTableData(true, false);
139141
},
140-
parseInt(refreshInterval) * 1000,
142+
parseInt(refreshInterval) * 1000
141143
);
142144
};
143145

@@ -244,28 +246,28 @@ const Dashboard: React.FunctionComponent = () => {
244246
{
245247
name: 'project_name',
246248
title: 'Project name',
247-
isVisible: true,
249+
isVisible: true
248250
},
249251
{
250252
name: 'count',
251253
title: 'Total no. of jobs',
252254
type: 'number',
253-
isVisible: true,
254-
},
255+
isVisible: true
256+
}
255257
];
256258

257259
const topUsersColumns: ColumnProps[] = [
258260
{
259261
name: 'user_name',
260262
title: 'User name',
261-
isVisible: true,
263+
isVisible: true
262264
},
263265
{
264266
name: 'count',
265267
title: 'Total no. of jobs',
266268
type: 'number',
267-
isVisible: true,
268-
},
269+
isVisible: true
270+
}
269271
];
270272

271273
const onInputFocus = () => {
@@ -293,7 +295,7 @@ const Dashboard: React.FunctionComponent = () => {
293295
RestService.exportToPDF(
294296
{ ...requestParams, ...{ ordering: ordering } } as RequestFilter & OrderingParams,
295297
jobsChartPng,
296-
hostChartPng,
298+
hostChartPng
297299
)
298300
.then(() => {
299301
setPdfLoading(false);
@@ -304,8 +306,12 @@ const Dashboard: React.FunctionComponent = () => {
304306
};
305307

306308
const onPdfBtnClick = () => {
307-
setPdfLoading(true);
308-
setTimeout(pdfDownload, 150);
309+
if (tableData.count > maxPDFJobTemplates) {
310+
setPDFWarningModalOpen(true);
311+
} else {
312+
setPdfLoading(true);
313+
setTimeout(pdfDownload, 150);
314+
}
309315
};
310316

311317
const onEnableTemplateCreationTimeChange = async (checked: boolean) => {
@@ -324,12 +330,42 @@ const Dashboard: React.FunctionComponent = () => {
324330
<div>
325331
<div>This section lists the top five users of Ansible Automation Platform, with a breakdown of the total number of jobs run by each user.</div>
326332
<ul>
327-
<br/>
328-
<li><strong>○ NOTE:</strong> Scheduled jobs can affect these results, because they do not represent a real, logged-in user. </li>
333+
<br />
334+
<li><strong>○ NOTE:</strong> Scheduled jobs can affect these results, because they do not represent a real, logged-in user.</li>
329335
</ul>
330336
</div>
331337
);
332338

339+
const pdfModalTitle = (
340+
<HelperText>
341+
<HelperTextItem variant="error" style={{fontSize: '20px'}} >PDF Download Failed: Data Volume Exceeded</HelperTextItem>
342+
</HelperText>
343+
);
344+
345+
const pdfWarningModal = (
346+
<Modal
347+
isOpen={isPDFWarningModalOpen}
348+
ouiaId="DFWarningModal"
349+
aria-labelledby="pdf-warning-modal-title"
350+
aria-describedby="pdf-warning-modal-body"
351+
variant={'small'}
352+
>
353+
<ModalHeader title={pdfModalTitle} labelId="pdf-warning-modal-title" />
354+
<ModalBody id="pdf-warning-modal-body">
355+
<HelperText style={{fontSize: '14px'}}>
356+
<HelperTextItem variant={'default'}>We were unable to generate your PDF because the selected report exceeds the maximum record limit.</HelperTextItem>
357+
<HelperTextItem variant={'default'}>Please apply additional filters to narrow your data and retry.</HelperTextItem>
358+
</HelperText>
359+
</ModalBody>
360+
<ModalFooter>
361+
<Button
362+
key="close"
363+
variant="link"
364+
onClick={() => {setPDFWarningModalOpen(false)}}>Close</Button>
365+
</ModalFooter>
366+
</Modal>
367+
);
368+
333369
// @ts-ignore
334370
return (
335371
<div>
@@ -354,13 +390,14 @@ const Dashboard: React.FunctionComponent = () => {
354390
/>
355391
</div>
356392
)}
357-
{logErrorMessage &&
358-
<div className={'main-layout'}>
359-
<Alert variant="danger" isInline title={logErrorMessage} />
360-
</div>
393+
{logErrorMessage &&
394+
<div className={'main-layout'}>
395+
<Alert variant="danger" isInline title={logErrorMessage} />
396+
</div>
361397
}
362398
{!loadDataError && !filterError && !logErrorMessage && (
363399
<div className={'main-layout'}>
400+
{pdfWarningModal}
364401
{(loading || pdfLoading) && (
365402
<div className={'loader'}>
366403
<Spinner className={'spinner'} diameter="80px" aria-label="Loader" />

src/frontend/app/Services/RestService.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,15 @@ const exportToPDF = async (
102102
): Promise<void> => {
103103
const queryString = buildQueryString(params);
104104
return api
105-
.post(`api/v1/report/pdf/${queryString}`, { job_chart: jobChart, host_chart: hostChart }, { responseType: 'blob' })
105+
.post(`api/v1/report/pdf/${queryString}`,
106+
{
107+
job_chart: jobChart,
108+
host_chart: hostChart
109+
},
110+
{
111+
responseType: 'blob',
112+
timeout: 0
113+
})
106114
.then((response) => {
107115
downloadAttachment(response.data as never, 'AAP_Automation_Dashboard_Report.pdf');
108116
Promise.resolve();

src/frontend/app/Store/filterStore.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ const useFilterStore = create<FilterStoreState & FilterStoreActions>((set) => ({
3939
loading: 'idle',
4040
error: false,
4141
reloadData: false,
42+
max_pdf_job_templates: 0,
43+
4244
fetchTemplateOptions: async () => {
4345
const {
4446
setCurrencies,
@@ -73,6 +75,7 @@ const useFilterStore = create<FilterStoreState & FilterStoreActions>((set) => ({
7375
manualCostAutomation: data.manual_cost_automation,
7476
automatedProcessCost: data.automated_process_cost,
7577
projectOptions: data.projects,
78+
max_pdf_job_templates: data.max_pdf_job_templates
7679
});
7780
} catch {
7881
set({ loading: 'failed', error: true });

src/frontend/app/Types/FilterTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface FilterOptionResponse {
3535
currency: number;
3636
enable_template_creation_time: boolean;
3737
filter_sets: FilterSet[];
38+
max_pdf_job_templates: number,
3839
}
3940

4041
export interface Settings {
@@ -55,6 +56,7 @@ export interface FilterState {
5556
organizationOptions: FilterOption[];
5657
loading: 'idle' | 'pending' | 'succeeded' | 'failed';
5758
error: boolean;
59+
max_pdf_job_templates: number,
5860
}
5961

6062
export interface RequestFilter {

tests/playwright/fixtures/templateOptions.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -756,5 +756,6 @@
756756
"date_range": "last_6_month"
757757
}
758758
}
759-
]
759+
],
760+
"max_pdf_job_templates": 4000
760761
}

tests/playwright/fixtures/templateOptionsAddReport.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -763,5 +763,6 @@
763763
"date_range": "month_to_date"
764764
}
765765
}
766-
]
767-
}
766+
],
767+
"max_pdf_job_templates": 4000
768+
}

0 commit comments

Comments
 (0)