-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ui: add export button for usage logs (PROJQUAY-6420) (quay#2492)
Adds tab for logs in the organization and repository views. Adds modal for exporting the logs to either an email or callback url.
- Loading branch information
Showing
9 changed files
with
328 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
describe('Usage Logs Export', () => { | ||
beforeEach(() => { | ||
cy.exec('npm run quay:seed'); | ||
cy.request('GET', `${Cypress.env('REACT_QUAY_APP_API_URL')}/csrf_token`) | ||
.then((response) => response.body.csrf_token) | ||
.then((token) => { | ||
cy.loginByCSRF(token); | ||
}); | ||
}); | ||
|
||
it('exports repository logs', () => { | ||
cy.intercept( | ||
'POST', | ||
'api/v1/repository/user1/hello-world/exportlogs?starttime=$endtime=', | ||
).as('exportRepositoryLogs'); | ||
cy.visit('/repository/user1/hello-world'); | ||
cy.contains('Logs').click(); | ||
cy.contains('Export').click(); | ||
cy.get('[id="export-logs-callback"]').type('[email protected]'); | ||
cy.contains('Confirm').click(); | ||
cy.contains('Logs exported with id').should('be.visible'); | ||
}); | ||
it('exports repository logs failure', () => { | ||
cy.intercept( | ||
'POST', | ||
'api/v1/repository/user1/hello-world/exportlogs?starttime=$endtime=', | ||
).as('exportRepositoryLogs'); | ||
cy.visit('/repository/user1/hello-world'); | ||
cy.contains('Logs').click(); | ||
cy.contains('Export').click(); | ||
cy.get('[id="export-logs-callback"]').type('blahblah'); | ||
cy.contains('Confirm').should('be.disabled'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import axios from 'src/libs/axios'; | ||
import {ResourceError} from 'src/resources/ErrorHandling'; | ||
|
||
export async function exportLogs( | ||
org: string, | ||
repo: string = null, | ||
starttime: string, | ||
endtime: string, | ||
callback: string, | ||
) { | ||
const exportLogsCallback = {}; | ||
if (callback.includes('@')) { | ||
exportLogsCallback['callback_email'] = callback; | ||
} else { | ||
exportLogsCallback['callback_url'] = callback; | ||
} | ||
|
||
const url = repo | ||
? `/api/v1/repository/${org}/${repo}/exportlogs?starttime=${starttime}&endtime=${endtime}` | ||
: `/api/v1/organization/${org}/exportlogs?starttime=${starttime}&endtime=${endtime}`; | ||
|
||
try { | ||
const response = await axios.post(url, exportLogsCallback); | ||
return response.data; | ||
} catch (error) { | ||
if (error.response) { | ||
return error.response.data; | ||
} else { | ||
throw new ResourceError( | ||
'Unable to export logs', | ||
error.response.status, | ||
error.response.data, | ||
); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 0 additions & 3 deletions
3
web/src/routes/OrganizationsList/Organization/Tabs/UsageLogs/UsageLogsTab.tsx
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import React from 'react'; | ||
import {DatePicker, Split, SplitItem} from '@patternfly/react-core'; | ||
import ExportLogsModal from './UsageLogsExportModal'; | ||
import './css/UsageLogs.scss'; | ||
|
||
interface UsageLogsProps { | ||
organization: string; | ||
repository: string; | ||
type: string; | ||
} | ||
|
||
function defaultStartDate() { | ||
// should be 1 month before current date | ||
const currentDate = new Date(); | ||
currentDate.setMonth(currentDate.getMonth() - 1); | ||
|
||
const year = currentDate.getFullYear(); | ||
const month = (currentDate.getMonth() + 1).toString().padStart(2, '0'); | ||
const day = currentDate.getDate().toString().padStart(2, '0'); | ||
const formattedDate = `${year}-${month}-${day}`; | ||
|
||
return formattedDate; | ||
} | ||
|
||
function defaultEndDate() { | ||
// should be current date | ||
const date = new Date(); | ||
|
||
const year = date.getFullYear(); | ||
const month = (date.getMonth() + 1).toString().padStart(2, '0'); | ||
const day = (date.getDate() + 1).toString().padStart(2, '0'); | ||
const formattedDate = `${year}-${month}-${day}`; | ||
|
||
return formattedDate; | ||
} | ||
|
||
function formatDate(date: string) { | ||
/** | ||
* change date string from y-m-d to m%d%y for api | ||
*/ | ||
const dates = date.split('-'); | ||
const year = dates[0]; | ||
const month = dates[1]; | ||
const day = dates[2]; | ||
|
||
return `${month}/${day}/${year}`; | ||
} | ||
|
||
export default function UsageLogs(props: UsageLogsProps) { | ||
const [logStartDate, setLogStartDate] = React.useState<string>( | ||
formatDate(defaultStartDate()), | ||
); | ||
const [logEndDate, setLogEndDate] = React.useState<string>( | ||
formatDate(defaultEndDate()), | ||
); | ||
const minDate = new Date(defaultStartDate()); | ||
const maxDate = new Date(defaultEndDate()); | ||
const rangeValidator = (date: Date) => { | ||
if (date < minDate) { | ||
return 'Date is before the allowable range'; | ||
} else if (date > maxDate) { | ||
return 'Date is after the allowable range'; | ||
} | ||
return ''; | ||
}; | ||
|
||
return ( | ||
<Split hasGutter className="usage-logs-header"> | ||
<SplitItem isFilled></SplitItem> | ||
<SplitItem> | ||
<DatePicker | ||
value={logStartDate} | ||
onChange={(_event, str, date) => { | ||
setLogStartDate(formatDate(str)); | ||
}} | ||
validators={[rangeValidator]} | ||
/> | ||
</SplitItem> | ||
<SplitItem> | ||
<DatePicker | ||
value={logEndDate} | ||
onChange={(_event, str, date) => { | ||
setLogEndDate(formatDate(str)); | ||
}} | ||
validators={[rangeValidator]} | ||
/> | ||
</SplitItem> | ||
<SplitItem> | ||
<ExportLogsModal | ||
organization={props.organization} | ||
repository={props.repository} | ||
starttime={logStartDate} | ||
endtime={logEndDate} | ||
type={props.type} | ||
/> | ||
</SplitItem> | ||
</Split> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import React from 'react'; | ||
import { | ||
Button, | ||
Text, | ||
Modal, | ||
ModalVariant, | ||
TextInput, | ||
HelperText, | ||
HelperTextItem, | ||
} from '@patternfly/react-core'; | ||
|
||
import {exportLogs} from 'src/hooks/UseExportLogs'; | ||
import {useAlerts} from 'src/hooks/UseAlerts'; | ||
import {AlertVariant} from 'src/atoms/AlertState'; | ||
|
||
export default function ExportLogsModal(props: ExportLogsModalProps) { | ||
const [isModalOpen, setIsModalOpen] = React.useState(false); | ||
const [callbackEmailOrUrl, setCallbackEmailOrUrl] = React.useState(''); | ||
const [userInputValidated, setUserInputValidated] = React.useState< | ||
'success' | 'warning' | 'error' | 'default' | ||
>('default'); | ||
const handleModalToggle = (_event: KeyboardEvent | React.MouseEvent) => { | ||
setIsModalOpen(!isModalOpen); | ||
}; | ||
const {addAlert} = useAlerts(); | ||
|
||
const exportLogsClick = (callback: string) => { | ||
return exportLogs( | ||
props.organization, | ||
props.repository, | ||
props.starttime, | ||
props.endtime, | ||
callback, | ||
); | ||
}; | ||
|
||
const validateUserInput = (userInput: string) => { | ||
return /(http(s)?:.+)|.+@.+/.test(userInput); | ||
}; | ||
|
||
const validateDate = () => { | ||
if (new Date(props.endtime) < new Date(props.starttime)) return false; | ||
return true; | ||
}; | ||
|
||
return ( | ||
<React.Fragment> | ||
<Button | ||
variant="primary" | ||
onClick={handleModalToggle} | ||
isDisabled={!validateDate()} | ||
> | ||
Export | ||
</Button> | ||
<Modal | ||
variant={ModalVariant.medium} | ||
title="Export Usage Logs" | ||
isOpen={isModalOpen} | ||
onClose={handleModalToggle} | ||
actions={[ | ||
<Button | ||
key="confirm" | ||
variant="primary" | ||
onClick={() => | ||
exportLogsClick(callbackEmailOrUrl).then((response) => { | ||
if (response['export_id']) { | ||
addAlert({ | ||
variant: AlertVariant.Success, | ||
title: `Logs exported with id ${response['export_id']}`, | ||
}); | ||
setIsModalOpen(false); | ||
} else { | ||
addAlert({ | ||
variant: AlertVariant.Failure, | ||
title: `Problem exporting logs: ${response['error_message']}`, | ||
}); | ||
setIsModalOpen(false); | ||
} | ||
}) | ||
} | ||
isDisabled={userInputValidated === 'error'} | ||
> | ||
{' '} | ||
Confirm | ||
</Button>, | ||
<Button key="cancel" variant="link" onClick={handleModalToggle}> | ||
Cancel | ||
</Button>, | ||
]} | ||
> | ||
<Text> | ||
Enter an e-mail address or callback URL (must start with http:// or | ||
https://) at which to receive the exported logs once they have been | ||
fully processed: | ||
</Text> | ||
<TextInput | ||
id="export-logs-callback" | ||
value={callbackEmailOrUrl} | ||
type="text" | ||
onChange={(_event, callbackEmailOrUrl) => { | ||
if (validateUserInput(callbackEmailOrUrl)) | ||
setUserInputValidated('success'); | ||
else setUserInputValidated('error'); | ||
setCallbackEmailOrUrl(callbackEmailOrUrl); | ||
}} | ||
validated={userInputValidated} | ||
/> | ||
<HelperText> | ||
<HelperTextItem variant="indeterminate"> | ||
{' '} | ||
Note: The export process can take up to an hour to process if there | ||
are many logs. As well, only a single export process can run at a | ||
time for each namespace. Additional export requests will be queued.{' '} | ||
</HelperTextItem> | ||
</HelperText> | ||
</Modal> | ||
</React.Fragment> | ||
); | ||
} | ||
|
||
interface ExportLogsModalProps { | ||
organization: string; | ||
repository: string; | ||
starttime: string; | ||
endtime: string; | ||
type: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
.usage-logs-header { | ||
margin: 20px; | ||
} | ||
|
||
.usage-logs { | ||
margin: 20px; | ||
} |