Revert "Page Views table I18n Date Fix and React redesign Pt. 2"
This reverts commit c58f903dc0
.
Reason for revert: This commit causes both a bug when reading a null
userAgent string, whole pageview box breaks, and also removes a hover
feature that was previously there. The original reason for this ticket
was to fix a translation bug, but ballooned to a larger scope.
CLOSES OREO-1647
Change-Id: I1dc2d138125b094d6825d2c93fc358cba3fd2fa7
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/291597
Reviewed-by: Bryant Larsen <blarsen@instructure.com>
QA-Review: Kwok Lam <klam@instructure.com>
Product-Review: Noah Rush <noah.rush@instructure.com>
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
This commit is contained in:
parent
bf2a9d226c
commit
bedcf89695
|
@ -16,8 +16,37 @@
|
|||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
%>
|
||||
|
||||
<div id="pageviews" data-user-id="<%= @user.id %>" style="overflow-x:auto; overflow-y:hidden">
|
||||
<div id="pageviews_datefilter">
|
||||
<div class="nobr">
|
||||
<label class="ic-Label" for="page_view_date"><%= t('Filter by date') %></label>
|
||||
<input type="datetime" class="ic-Input" id="page_view_date"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pageviews">
|
||||
<table data-user-id="<%= @user.id %>" class="ic-Table ic-Table--hover-row ic-Table--condensed ic-Table--striped">
|
||||
<thead class='ui-widget-header'>
|
||||
<tr>
|
||||
<th><%= t('headers.url', "URL") %></th>
|
||||
<th><%= t('headers.date', "Date") %></th>
|
||||
<th><%= t('headers.participated', "Participated") %></th>
|
||||
<th><%= t('headers.time', "Time") %></th>
|
||||
<th>
|
||||
<div style="float:<%= direction('right') %>; margin-<%= direction('right') %>: 4px">
|
||||
<%= link_to(t('page views csv'),
|
||||
user_page_views_path(@user, :format => :csv),
|
||||
class: 'icon-ms-excel',
|
||||
id: 'page_views_csv_link',
|
||||
'aria-label' => t('Download Page Views CSV'))
|
||||
%>
|
||||
</div>
|
||||
<%= t('headers.user_agent', "User Agent") %>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="page_view_results">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% css_bundle :page_views %>
|
||||
<% js_bundle :page_views %>
|
||||
|
|
|
@ -58,26 +58,26 @@ describe "users" do
|
|||
end
|
||||
|
||||
it "validates a basic page view" do
|
||||
page_view(user: @student, course: @course, url: "assignments", user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36", http_method: "get")
|
||||
page_view(user: @student, course: @course, url: "assignments")
|
||||
get "/users/#{@student.id}"
|
||||
rows = ff("#page_view_results tr")
|
||||
expect(rows.count).to eq 1
|
||||
page_view = rows.first
|
||||
expect(page_view).to include_text("Chrome")
|
||||
expect(page_view).to include_text("Firefox")
|
||||
expect(page_view).to include_text("assignments")
|
||||
expect(f("#page_view_results")).not_to contain_css("tr [name='IconCheckMark']") # should not have a participation
|
||||
expect(f("#page_view_results")).not_to contain_css("tr img") # should not have a participation
|
||||
end
|
||||
|
||||
it "validates page view with a participation" do
|
||||
page_view(user: @student, course: @course, participated: true)
|
||||
get "/users/#{@student.id}"
|
||||
expect(f("#page_view_results [name='IconCheckMark']")).to be_displayed
|
||||
expect(f("#page_view_results .icon-check")).to be_displayed
|
||||
end
|
||||
|
||||
it "validates a page view url" do
|
||||
second_student_name = "test student for page views"
|
||||
get "/users/#{@student.id}"
|
||||
page_view(user: @student, course: @course, participated: true, url: student_in_course(name: second_student_name).user.id.to_s, user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36", http_method: "get")
|
||||
page_view(user: @student, course: @course, participated: true, url: student_in_course(name: second_student_name).user.id.to_s)
|
||||
refresh_page # in order to get the generated page view
|
||||
page_view_url = f("#page_view_results a")
|
||||
second_student = User.where(name: second_student_name).first
|
||||
|
@ -88,31 +88,31 @@ describe "users" do
|
|||
end
|
||||
|
||||
it "validates all page views were loaded" do
|
||||
page_views_count = 30
|
||||
page_views_count = 100
|
||||
page_views_count.times { |i| page_view(user: @student, course: @course, url: ("%03d" % i).to_s) }
|
||||
get "/users/#{@student.id}"
|
||||
wait_for_ajaximations
|
||||
scroll_page_to_bottom
|
||||
driver.execute_script("$('#scrollContainer').scrollTop($('#scrollContainer')[0].scrollHeight);")
|
||||
wait_for(method: nil, timeout: 0.5) { f("#paginatedView-loading").displayed? }
|
||||
driver.execute_script("$('#pageviews').scrollTop($('#pageviews')[0].scrollHeight);")
|
||||
# wait for loading spinner to finish
|
||||
wait_for(method: nil, timeout: 0.5) { f(".paginatedView-loading").displayed? }
|
||||
wait_for_no_such_element { f(".paginatedView-loading") }
|
||||
expect(ff("#page_view_results tr").length).to eq page_views_count
|
||||
end
|
||||
|
||||
it "filters by date" do
|
||||
old_date = DateTime.new(2022, 2, 10).beginning_of_day
|
||||
page_view(user: @student, course: @course, url: "recent", created_at: old_date + 1.day + 2.hours)
|
||||
page_view(user: @student, course: @course, url: "older", created_at: old_date + 1.hour)
|
||||
old_date = 2.days.ago.beginning_of_day
|
||||
page_view(user: @student, course: @course, url: "recent", created_at: 5.minutes.ago)
|
||||
page_view(user: @student, course: @course, url: "older", created_at: old_date + 1.minute)
|
||||
get "/users/#{@student.id}"
|
||||
wait_for_ajaximations
|
||||
expect(ff("#page_view_results tr").first.text).to include "recent"
|
||||
replace_content(f("[data-testid='inputQueryDate']"), old_date.year.to_s + "-" + old_date.month.to_s + "-" + old_date.day.to_s)
|
||||
replace_content(f("#page_view_date"), old_date.strftime("%Y-%m-%d"))
|
||||
driver.action.send_keys(:tab).perform
|
||||
wait_for_ajaximations
|
||||
expect(ff("#page_view_results tr").first.text).to include "older"
|
||||
match = f("#page_views_csv_link")["href"].match(/start_time=([^&]+)/)
|
||||
expect(match[1]).to include old_date.year.to_s + "-0" + old_date.month.to_s
|
||||
expect(DateTime.parse(match[1]).to_i).to eq old_date.to_i
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ module OtherHelperMethods
|
|||
controller = opts[:controller] || "assignments"
|
||||
summarized = opts[:summarized] || nil
|
||||
url = opts[:url]
|
||||
user_agent = opts[:user_agent] || "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36"
|
||||
user_agent = opts[:user_agent] || "firefox"
|
||||
|
||||
page_view = course.page_views.build(
|
||||
user: user,
|
||||
|
@ -54,7 +54,6 @@ module OtherHelperMethods
|
|||
page_view.summarized = summarized
|
||||
page_view.request_id = SecureRandom.hex(10)
|
||||
page_view.created_at = opts[:created_at] || Time.now
|
||||
page_view.http_method = opts[:http_method] || "get"
|
||||
|
||||
if opts[:participated]
|
||||
page_view.participated = true
|
||||
|
|
|
@ -16,13 +16,57 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import $ from 'jquery'
|
||||
import '@canvas/datetime'
|
||||
import moment from 'moment'
|
||||
import PageViewCollection from './backbone/collections/PageViewCollection'
|
||||
import PageViewView from './backbone/views/PageViewView'
|
||||
import ready from '@instructure/ready'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import {PageViews} from './react/PageViews'
|
||||
|
||||
function renderTable(date) {
|
||||
const $container = $('#pageviews')
|
||||
const $table = $container.find('table')
|
||||
const userId = $table.attr('data-user-id')
|
||||
|
||||
const pageViews = new PageViewCollection()
|
||||
pageViews.url = `/api/v1/users/${userId}/page_views`
|
||||
const $csvLink = $('#page_views_csv_link')
|
||||
let csvUrl = $csvLink.attr('href').split('?')[0]
|
||||
if (date) {
|
||||
const start_time = $.unfudgeDateForProfileTimezone(date)
|
||||
const end_time = moment(start_time).add(1, 'days')
|
||||
const date_params = `?start_time=${start_time.toISOString()}&end_time=${end_time.toISOString()}`
|
||||
pageViews.url += date_params
|
||||
csvUrl += date_params
|
||||
}
|
||||
$csvLink.attr('href', csvUrl)
|
||||
|
||||
const fetchOptions = {reset: true}
|
||||
|
||||
const pageViewsView = new PageViewView({
|
||||
collection: pageViews,
|
||||
el: $table,
|
||||
fetchOptions
|
||||
})
|
||||
|
||||
// Add events
|
||||
pageViews.on('reset', pageViewsView.render, pageViewsView)
|
||||
|
||||
// Fetch page views
|
||||
const fetchParams = {per_page: 100}
|
||||
pageViewsView.$el.disableWhileLoading(pageViews.fetch({data: fetchParams}))
|
||||
|
||||
return pageViewsView
|
||||
}
|
||||
|
||||
ready(() => {
|
||||
const parentElement = document.getElementById('pageviews')
|
||||
const userID = parentElement.getAttribute('data-user-id')
|
||||
ReactDOM.render(<PageViews userID={userID} />, parentElement)
|
||||
let view = renderTable()
|
||||
$('#page_view_date')
|
||||
.datetime_field({dateOnly: true})
|
||||
.change(event => {
|
||||
const date = $(event.target).data('date')
|
||||
view.stopPaginationListener()
|
||||
$('#page_view_results').empty()
|
||||
view = renderTable(date)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {TruncateText} from '@instructure/ui-truncate-text'
|
||||
import {Text} from '@instructure/ui-text'
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import FriendlyDatetime from '@canvas/datetime/react/components/FriendlyDatetime'
|
||||
import {IconCheckMarkSolid} from '@instructure/ui-icons'
|
||||
import {Table} from '@instructure/ui-table'
|
||||
import UAParser from 'ua-parser-js'
|
||||
|
||||
const I18n = useI18nScope('PageViews')
|
||||
|
||||
export default class PageViewRow extends React.Component {
|
||||
static displayName = 'Row'
|
||||
|
||||
parseUserAgentString(userAgent) {
|
||||
const SPEEDGRADER = 'SpeedGrader for iPad'
|
||||
const parser = new UAParser(userAgent)
|
||||
|
||||
let browser = parser.getBrowser() ? parser.getBrowser().name : ''
|
||||
|
||||
if (userAgent.toLowerCase().indexOf('speedgrader') >= 0) {
|
||||
browser = SPEEDGRADER
|
||||
}
|
||||
|
||||
if (!browser) {
|
||||
browser = I18n.t('browsers.unrecognized', 'Unrecognized Browser')
|
||||
} else if (browser !== SPEEDGRADER) {
|
||||
const version = parser.getBrowser().version ? parser.getBrowser().version.split('.')[0] : '0'
|
||||
browser = `${browser} ${version}`
|
||||
}
|
||||
return browser
|
||||
}
|
||||
|
||||
summarizedUserAgent(data) {
|
||||
return data.app_name || this.parseUserAgentString(data.user_agent)
|
||||
}
|
||||
|
||||
readableInteractionTime(data) {
|
||||
const seconds = data.interaction_seconds
|
||||
if (seconds > 5) {
|
||||
return Math.round(seconds)
|
||||
} else {
|
||||
return '--'
|
||||
}
|
||||
}
|
||||
|
||||
urlAsLink(data) {
|
||||
const method = data.http_method
|
||||
const url = data.url || ''
|
||||
|
||||
if (!method || method !== 'get') return <>{url}</>
|
||||
else
|
||||
return (
|
||||
<>
|
||||
<a href={url}>{url}</a>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
particpatedIcon(data) {
|
||||
if (data.participated) return <IconCheckMarkSolid />
|
||||
else return <></>
|
||||
}
|
||||
|
||||
renderRow() {
|
||||
return (
|
||||
<>
|
||||
<Table.Row key={this.props.rowData.session_id}>
|
||||
<Table.Cell key="url">
|
||||
<Text key="url_text">
|
||||
<TruncateText position="end">{this.urlAsLink(this.props.rowData)}</TruncateText>
|
||||
</Text>
|
||||
</Table.Cell>
|
||||
<Table.Cell key="created_at">
|
||||
<FriendlyDatetime
|
||||
key="datetime"
|
||||
dateTime={this.props.rowData.created_at}
|
||||
showTime
|
||||
format="%-d %b %Y %-l:%M%P"
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell key="participated" textAlign="center">
|
||||
<Text>{this.particpatedIcon(this.props.rowData)}</Text>
|
||||
</Table.Cell>
|
||||
<Table.Cell key="interaction_seconds" textAlign="center">
|
||||
<Text>{this.readableInteractionTime(this.props.rowData)}</Text>
|
||||
</Table.Cell>
|
||||
<Table.Cell key="user_agent">
|
||||
<Text>{this.summarizedUserAgent(this.props.rowData)}</Text>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.rowData) return this.renderRow()
|
||||
else return <></>
|
||||
}
|
||||
}
|
|
@ -1,231 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, {useState, useEffect, useCallback} from 'react'
|
||||
import moment from 'moment'
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import {Alert} from '@instructure/ui-alerts'
|
||||
import {Text} from '@instructure/ui-text'
|
||||
import {Link} from '@instructure/ui-link'
|
||||
import {Flex} from '@instructure/ui-flex'
|
||||
import {ScreenReaderContent} from '@instructure/ui-a11y-content'
|
||||
import {Spinner} from '@instructure/ui-spinner'
|
||||
import {Table} from '@instructure/ui-table'
|
||||
import tz from '@canvas/timezone'
|
||||
import CanvasDateInput from '@canvas/datetime/react/components/DateInput'
|
||||
import {unfudgeDateForProfileTimezone} from '@canvas/datetime/date-functions'
|
||||
import doFetchApi from '@canvas/do-fetch-api-effect'
|
||||
import InfiniteScroll from '@canvas/infinite-scroll'
|
||||
import PageViewRow from './PageViewRow'
|
||||
|
||||
const I18n = useI18nScope('PageViews')
|
||||
|
||||
const SIZE = 50
|
||||
|
||||
export const PageViews = ({userID}) => {
|
||||
const getCSVURL = useCallback(
|
||||
date => {
|
||||
const baseCSVURL = `/users/${userID}/page_views.csv`
|
||||
return date
|
||||
? baseCSVURL +
|
||||
'?start_time=' +
|
||||
unfudgeDateForProfileTimezone(date).toISOString() +
|
||||
'&' +
|
||||
'end_time=' +
|
||||
moment(unfudgeDateForProfileTimezone(date)).add(1, 'days').toISOString()
|
||||
: baseCSVURL
|
||||
},
|
||||
[userID]
|
||||
)
|
||||
|
||||
const [jsonData, setPageViewResults] = useState([])
|
||||
const [queryDate, updateQueryDate] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [csvURLLink, setCSVURLLink] = useState(getCSVURL(''))
|
||||
const [page, setPage] = useState('first')
|
||||
const [nextPage, setNextPage] = useState('')
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
const [scrollContainer, setScrollContainer] = useState(null)
|
||||
|
||||
const formatDate = date => {
|
||||
return tz.format(date, 'date.formats.medium')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (error)
|
||||
<Alert
|
||||
liveRegion={() => document.getElementById('aria_alerts')}
|
||||
liveRegionPoliteness="assertive"
|
||||
screenReaderOnly
|
||||
>
|
||||
{error}
|
||||
</Alert>
|
||||
}, [error])
|
||||
|
||||
useEffect(() => {
|
||||
setCSVURLLink(getCSVURL(queryDate))
|
||||
}, [getCSVURL, queryDate])
|
||||
|
||||
const onSuccessApi = data => {
|
||||
if (data.link.next) {
|
||||
setNextPage(data.link.next.page)
|
||||
}
|
||||
|
||||
setPageViewResults(currentJsonData => {
|
||||
setHasMore(data.json.length === SIZE)
|
||||
return currentJsonData.concat(data.json)
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
const params = {
|
||||
start_time: `${queryDate ? unfudgeDateForProfileTimezone(queryDate).toISOString() : ''}`,
|
||||
end_time: `${
|
||||
queryDate
|
||||
? moment(unfudgeDateForProfileTimezone(queryDate)).add(1, 'days').toISOString()
|
||||
: ''
|
||||
}`,
|
||||
per_page: SIZE,
|
||||
page
|
||||
}
|
||||
if (page === 'first') {
|
||||
delete params.page
|
||||
}
|
||||
doFetchApi({
|
||||
path: `/api/v1/users/${userID}/page_views`,
|
||||
params
|
||||
})
|
||||
.then(onSuccessApi)
|
||||
.catch(() => {
|
||||
setError('Error Fetching PageViews')
|
||||
})
|
||||
}, [queryDate, page, userID])
|
||||
|
||||
const updateData = date => {
|
||||
setPageViewResults([])
|
||||
updateQueryDate(date)
|
||||
setPage('first')
|
||||
}
|
||||
|
||||
const loadMore = () => {
|
||||
setPage(nextPage)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{overflowx: 'hidden', overflowy: 'hidden'}}>
|
||||
<Flex
|
||||
key="pageviews_datefilter"
|
||||
direction="column"
|
||||
margin="none none small"
|
||||
align="start"
|
||||
overflowY="hidden"
|
||||
overflowX="hidden"
|
||||
height="570px"
|
||||
>
|
||||
<Flex.Item
|
||||
textAlign="start"
|
||||
padding="xx-small"
|
||||
height="50px"
|
||||
overflowY="hidden"
|
||||
overflowX="hidden"
|
||||
>
|
||||
<Text weight="bold">{I18n.t('Filter by date')}</Text>
|
||||
</Flex.Item>
|
||||
<Flex.Item
|
||||
padding="x-small"
|
||||
height="50px"
|
||||
shouldShrink
|
||||
overflowY="hidden"
|
||||
overflowX="hidden"
|
||||
>
|
||||
<Flex overflowY="hidden" overflowX="hidden">
|
||||
<Flex.Item>
|
||||
<CanvasDateInput
|
||||
dataTestid="inputQueryDate"
|
||||
renderLabel={
|
||||
<ScreenReaderContent>{I18n.t('Filter by date')}</ScreenReaderContent>
|
||||
}
|
||||
onSelectedDateChange={updateData}
|
||||
formatDate={formatDate}
|
||||
withRunningValue
|
||||
/>
|
||||
</Flex.Item>
|
||||
<Flex.Item padding="x-small">
|
||||
<Link id="page_views_csv_link" data-testid="page_views_csv_link" href={csvURLLink}>
|
||||
{I18n.t('Download Page Views CSV')}
|
||||
</Link>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</Flex.Item>
|
||||
<Flex.Item height="20px" />
|
||||
<Flex.Item
|
||||
key="page_views_table"
|
||||
padding="x-small"
|
||||
height="450px"
|
||||
overflowY="auto"
|
||||
id="scrollContainer"
|
||||
data-testid="scrollContainer"
|
||||
elementRef={setScrollContainer}
|
||||
>
|
||||
<InfiniteScroll
|
||||
pageStart={0}
|
||||
loadMore={loadMore}
|
||||
hasMore={hasMore}
|
||||
scrollContainer={scrollContainer}
|
||||
loader={
|
||||
<Spinner
|
||||
id="paginatedView-loading"
|
||||
renderTitle={I18n.t('Loading')}
|
||||
size="small"
|
||||
margin="0 0 0 medium"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Table layout="fixed" caption="Page View Results" user-id={userID}>
|
||||
<Table.Head>
|
||||
<Table.Row key="0">
|
||||
<Table.ColHeader id="URL">
|
||||
{I18n.t('#page_views.table.headers.url', 'URL')}
|
||||
</Table.ColHeader>
|
||||
<Table.ColHeader id="Date" width="175px">
|
||||
{(I18n.t('#page_views.table.headers.date'), 'Date')}
|
||||
</Table.ColHeader>
|
||||
<Table.ColHeader id="Paticipated" width="115px" textAlign="start">
|
||||
{(I18n.t('#page_views.table.headers.participated'), 'Participated')}
|
||||
</Table.ColHeader>
|
||||
<Table.ColHeader id="Time" width="60px" textAlign="start">
|
||||
{(I18n.t('#page_views.table.headers.time'), 'Time')}
|
||||
</Table.ColHeader>
|
||||
<Table.ColHeader id="Agent" width="125px">
|
||||
{(I18n.t('#page_views.table.headers.user_agent'), 'User Agent')}
|
||||
</Table.ColHeader>
|
||||
</Table.Row>
|
||||
</Table.Head>
|
||||
<Table.Body id="page_view_results">
|
||||
{jsonData.map((element, index) => (
|
||||
<PageViewRow displayName="Row" rowData={element} key={index.toString()} />
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</InfiniteScroll>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {mount} from 'enzyme'
|
||||
import PageViewRow from '../PageViewRow'
|
||||
|
||||
const defaultData = (data = {}) => ({
|
||||
action: 'show',
|
||||
app_name: null,
|
||||
asset_type: null,
|
||||
asset_user_access_id: '12345',
|
||||
context_type: 'Course',
|
||||
contributed: false,
|
||||
controller: 'assignments',
|
||||
created_at: '2021-12-03T07:51:27Z',
|
||||
developer_key_id: null,
|
||||
http_method: 'get',
|
||||
id: 'b45b74de-19fd-4178-b677-9957ec76b476',
|
||||
interaction_seconds: null,
|
||||
links: {user: 1, context: 1234, asset: null, real_user: null, account: 1},
|
||||
participated: false,
|
||||
remote_ip: '127.0.0.1',
|
||||
render_time: 0.78121,
|
||||
session_id: '58cb2c11111cd5a1b8a999fbe999ea',
|
||||
summarized: null,
|
||||
updated_at: '2021-12-03T07:51:27Z',
|
||||
url: 'https://canvas.local/',
|
||||
user_agent:
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36',
|
||||
user_request: null,
|
||||
...data
|
||||
})
|
||||
|
||||
const EDGE_USER_AGENT =
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/12.246'
|
||||
const CHROME_USER_AGENT =
|
||||
'Mozilla/5.0 (X11; CrOS x86_64 8172.45.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.64'
|
||||
const SAFARI_USER_AGENT =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9'
|
||||
const SPEEDGRADER_USER_AGENT =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Speedgrader/1.3'
|
||||
|
||||
describe('user agents', () => {
|
||||
it('edge', () => {
|
||||
const wrapper = mount(<PageViewRow data={defaultData()} />)
|
||||
expect(wrapper.instance().parseUserAgentString(EDGE_USER_AGENT)).toEqual('Edge 12')
|
||||
})
|
||||
|
||||
it('chrome', () => {
|
||||
const wrapper = mount(<PageViewRow data={defaultData()} />)
|
||||
expect(wrapper.instance().parseUserAgentString(CHROME_USER_AGENT)).toEqual('Chrome 51')
|
||||
})
|
||||
|
||||
it('safari', () => {
|
||||
const wrapper = mount(<PageViewRow data={defaultData()} />)
|
||||
expect(wrapper.instance().parseUserAgentString(SAFARI_USER_AGENT)).toEqual('Safari 9')
|
||||
})
|
||||
|
||||
it('speedgrader', () => {
|
||||
const wrapper = mount(<PageViewRow data={defaultData()} />)
|
||||
expect(wrapper.instance().parseUserAgentString(SPEEDGRADER_USER_AGENT)).toEqual(
|
||||
'SpeedGrader for iPad'
|
||||
)
|
||||
})
|
||||
})
|
|
@ -1,183 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {shallow} from 'enzyme'
|
||||
import {render, fireEvent} from '@testing-library/react'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import {PageViews} from '../PageViews'
|
||||
import {act} from 'react-dom/test-utils'
|
||||
|
||||
import moment from 'moment'
|
||||
import {unfudgeDateForProfileTimezone} from '@canvas/datetime/date-functions'
|
||||
|
||||
const processStartDate = date => {
|
||||
const dateStr = unfudgeDateForProfileTimezone(date).toISOString()
|
||||
return encodeURIComponent(dateStr)
|
||||
}
|
||||
const processEndDate = date => {
|
||||
const dateStr = moment(unfudgeDateForProfileTimezone(date)).add(1, 'days').toISOString()
|
||||
return encodeURIComponent(dateStr)
|
||||
}
|
||||
|
||||
const defaultProps = (props = {}) => ({
|
||||
userID: '718',
|
||||
...props
|
||||
})
|
||||
const PATH_START = `/api/v1/users/718/page_views`
|
||||
const DEFAULT_PATH = `${PATH_START}?start_time=&end_time=&per_page=50`
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.restore()
|
||||
})
|
||||
|
||||
it('renders correctly', () => {
|
||||
const wrapper = shallow(<PageViews {...defaultProps()} />)
|
||||
expect(wrapper).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('Call api with error', () => {
|
||||
const expectedDate = 'Jan 24, 2022'
|
||||
const datePath = `${PATH_START}?start_time=${processStartDate(
|
||||
expectedDate
|
||||
)}&end_time=${processEndDate(expectedDate)}&per_page=50`
|
||||
|
||||
fetchMock.mock(DEFAULT_PATH, 500)
|
||||
fetchMock.mock(datePath, {})
|
||||
|
||||
const {getByTestId} = render(<PageViews {...defaultProps()} />)
|
||||
const inputDate = getByTestId('inputQueryDate')
|
||||
fireEvent.change(inputDate, {target: {value: expectedDate}})
|
||||
fireEvent.blur(inputDate)
|
||||
expect(inputDate.value).toBe(expectedDate)
|
||||
})
|
||||
|
||||
// it('Show table without data', () => {
|
||||
|
||||
// fetchMock.mock(DEFAULT_PATH, 200)
|
||||
// const {queryAllByRole} = render(<PageViews {...defaultProps()} />)
|
||||
// expect(queryAllByRole('row')).toHaveLength(1)
|
||||
// })
|
||||
|
||||
it('Show data and load more data with scroll', async () => {
|
||||
const defaultData = {
|
||||
action: 'new',
|
||||
app_name: null,
|
||||
asset_type: null,
|
||||
asset_user_access_id: null,
|
||||
context_type: 'Course',
|
||||
contributed: false,
|
||||
controller: 'discussion_topics',
|
||||
created_at: '2022-01-26T17:48:34Z',
|
||||
developer_key_id: null,
|
||||
http_method: 'get',
|
||||
id: '32ea0daf-70ac-434e-9111-cf7c71d23df0',
|
||||
interaction_seconds: 6,
|
||||
links: {user: 893, context: 11, asset: null, real_user: 1, account: 2},
|
||||
participated: true,
|
||||
remote_ip: '::1',
|
||||
render_time: 0.498169,
|
||||
session_id: 'd268f88b12896612544921c836302c8e',
|
||||
summarized: null,
|
||||
updated_at: '2022-01-26T17:48:34Z',
|
||||
url: null,
|
||||
user_agent:
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36',
|
||||
user_request: true
|
||||
}
|
||||
const page1 = Array(50)
|
||||
.fill()
|
||||
.map(index => {
|
||||
const copy = {...defaultData}
|
||||
copy.url = `url ${index}`
|
||||
return copy
|
||||
})
|
||||
const page2 = Array(22)
|
||||
.fill()
|
||||
.map(index => {
|
||||
const copy = {...defaultData}
|
||||
copy.url = `url ${index}`
|
||||
return copy
|
||||
})
|
||||
|
||||
fetchMock.mock(DEFAULT_PATH, {
|
||||
body: page1,
|
||||
status: 200,
|
||||
headers: {
|
||||
Link: '<http://canvas.docker/api/v1/users/37/page_views?page=2>; rel="next"'
|
||||
}
|
||||
})
|
||||
fetchMock.mock(`${DEFAULT_PATH}&page=2`, {
|
||||
body: page2,
|
||||
status: 200,
|
||||
headers: {
|
||||
Link: '<http://canvas.docker/api/v1/users/37/page_views?page=3>; rel="current"'
|
||||
}
|
||||
})
|
||||
let wrapper
|
||||
await act(async () => {
|
||||
wrapper = render(<PageViews {...defaultProps()} />)
|
||||
})
|
||||
const {getByTestId, queryAllByRole} = wrapper
|
||||
const scrollContainer = getByTestId('scrollContainer')
|
||||
jest.spyOn(scrollContainer, 'scrollTop', 'get').mockImplementation(() => 450)
|
||||
fireEvent.scroll(scrollContainer)
|
||||
|
||||
expect(queryAllByRole('row')).toHaveLength(73)
|
||||
})
|
||||
|
||||
it('Select the input date and reset the input date', () => {
|
||||
const expectedDate = 'Jan 27, 2021'
|
||||
|
||||
const datePath = `${PATH_START}?start_time=${processStartDate(
|
||||
expectedDate
|
||||
)}&end_time=${processEndDate(expectedDate)}&per_page=50`
|
||||
fetchMock.mock(datePath, {})
|
||||
fetchMock.mock(DEFAULT_PATH, {})
|
||||
|
||||
const {getByTestId} = render(<PageViews {...defaultProps()} />)
|
||||
const inputDate = getByTestId('inputQueryDate')
|
||||
fireEvent.change(inputDate, {target: {value: expectedDate}})
|
||||
fireEvent.blur(inputDate)
|
||||
expect(inputDate.value).toBe(expectedDate)
|
||||
fireEvent.change(inputDate, {target: {value: ''}})
|
||||
fireEvent.blur(inputDate)
|
||||
expect(inputDate.value).toBe('')
|
||||
})
|
||||
|
||||
it('Select date and download file', () => {
|
||||
const expectedDate = 'Jan 27, 2022'
|
||||
const {getByTestId} = render(<PageViews {...defaultProps()} />)
|
||||
const inputDate = getByTestId('inputQueryDate')
|
||||
fireEvent.change(inputDate, {target: {value: expectedDate}})
|
||||
fireEvent.blur(inputDate)
|
||||
expect(inputDate.value).toBe(expectedDate)
|
||||
const pageViewLink = getByTestId('page_views_csv_link')
|
||||
fireEvent.click(pageViewLink)
|
||||
expect(pageViewLink).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('Date with not result', () => {
|
||||
const expectedDate = 'Jan 27, 2022'
|
||||
const {getByTestId, queryAllByRole} = render(<PageViews {...defaultProps()} />)
|
||||
const inputDate = getByTestId('inputQueryDate')
|
||||
fireEvent.change(inputDate, {target: {value: expectedDate}})
|
||||
fireEvent.blur(inputDate)
|
||||
expect(inputDate.value).toBe(expectedDate)
|
||||
expect(queryAllByRole('row')).toHaveLength(1)
|
||||
})
|
|
@ -1,211 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders correctly 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"overflowx": "hidden",
|
||||
"overflowy": "hidden",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Flex
|
||||
align="start"
|
||||
as="span"
|
||||
direction="column"
|
||||
display="flex"
|
||||
elementRef={[Function]}
|
||||
height="570px"
|
||||
justifyItems="start"
|
||||
key="pageviews_datefilter"
|
||||
margin="none none small"
|
||||
overflowX="hidden"
|
||||
overflowY="hidden"
|
||||
withVisualDebug={false}
|
||||
wrap="no-wrap"
|
||||
>
|
||||
<Item
|
||||
as="span"
|
||||
elementRef={[Function]}
|
||||
height="50px"
|
||||
overflowX="hidden"
|
||||
overflowY="hidden"
|
||||
padding="xx-small"
|
||||
shouldGrow={false}
|
||||
shouldShrink={false}
|
||||
textAlign="start"
|
||||
>
|
||||
<Text
|
||||
as="span"
|
||||
letterSpacing="normal"
|
||||
size="medium"
|
||||
weight="bold"
|
||||
wrap="normal"
|
||||
>
|
||||
Filter by date
|
||||
</Text>
|
||||
</Item>
|
||||
<Item
|
||||
as="span"
|
||||
elementRef={[Function]}
|
||||
height="50px"
|
||||
overflowX="hidden"
|
||||
overflowY="hidden"
|
||||
padding="x-small"
|
||||
shouldGrow={false}
|
||||
shouldShrink={true}
|
||||
>
|
||||
<Flex
|
||||
as="span"
|
||||
direction="row"
|
||||
display="flex"
|
||||
elementRef={[Function]}
|
||||
justifyItems="start"
|
||||
overflowX="hidden"
|
||||
overflowY="hidden"
|
||||
withVisualDebug={false}
|
||||
wrap="no-wrap"
|
||||
>
|
||||
<Item
|
||||
as="span"
|
||||
elementRef={[Function]}
|
||||
shouldGrow={false}
|
||||
shouldShrink={false}
|
||||
>
|
||||
<CanvasDateInput
|
||||
dataTestid="inputQueryDate"
|
||||
formatDate={[Function]}
|
||||
onSelectedDateChange={[Function]}
|
||||
renderLabel={
|
||||
<ScreenReaderContent
|
||||
as="span"
|
||||
>
|
||||
Filter by date
|
||||
</ScreenReaderContent>
|
||||
}
|
||||
withRunningValue={true}
|
||||
/>
|
||||
</Item>
|
||||
<Item
|
||||
as="span"
|
||||
elementRef={[Function]}
|
||||
padding="x-small"
|
||||
shouldGrow={false}
|
||||
shouldShrink={false}
|
||||
>
|
||||
<Link
|
||||
color="link"
|
||||
data-testid="page_views_csv_link"
|
||||
href="/users/718/page_views.csv"
|
||||
iconPlacement="start"
|
||||
id="page_views_csv_link"
|
||||
isWithinText={true}
|
||||
>
|
||||
Download Page Views CSV
|
||||
</Link>
|
||||
</Item>
|
||||
</Flex>
|
||||
</Item>
|
||||
<Item
|
||||
as="span"
|
||||
elementRef={[Function]}
|
||||
height="20px"
|
||||
shouldGrow={false}
|
||||
shouldShrink={false}
|
||||
/>
|
||||
<Item
|
||||
as="span"
|
||||
data-testid="scrollContainer"
|
||||
elementRef={[Function]}
|
||||
height="450px"
|
||||
id="scrollContainer"
|
||||
key="page_views_table"
|
||||
overflowY="auto"
|
||||
padding="x-small"
|
||||
shouldGrow={false}
|
||||
shouldShrink={false}
|
||||
>
|
||||
<InfiniteScroll
|
||||
hasMore={false}
|
||||
loadMore={[Function]}
|
||||
loader={
|
||||
<Spinner
|
||||
as="div"
|
||||
id="paginatedView-loading"
|
||||
margin="0 0 0 medium"
|
||||
renderTitle="Loading"
|
||||
size="small"
|
||||
variant="default"
|
||||
/>
|
||||
}
|
||||
pageStart={0}
|
||||
scrollContainer={null}
|
||||
threshold={250}
|
||||
>
|
||||
<Table
|
||||
caption="Page View Results"
|
||||
hover={false}
|
||||
layout="fixed"
|
||||
user-id="718"
|
||||
>
|
||||
<Head>
|
||||
<Row
|
||||
key="0"
|
||||
>
|
||||
<ColHeader
|
||||
id="URL"
|
||||
scope="col"
|
||||
sortDirection="none"
|
||||
textAlign="start"
|
||||
>
|
||||
URL
|
||||
</ColHeader>
|
||||
<ColHeader
|
||||
id="Date"
|
||||
scope="col"
|
||||
sortDirection="none"
|
||||
textAlign="start"
|
||||
width="175px"
|
||||
>
|
||||
Date
|
||||
</ColHeader>
|
||||
<ColHeader
|
||||
id="Paticipated"
|
||||
scope="col"
|
||||
sortDirection="none"
|
||||
textAlign="start"
|
||||
width="115px"
|
||||
>
|
||||
Participated
|
||||
</ColHeader>
|
||||
<ColHeader
|
||||
id="Time"
|
||||
scope="col"
|
||||
sortDirection="none"
|
||||
textAlign="start"
|
||||
width="60px"
|
||||
>
|
||||
Time
|
||||
</ColHeader>
|
||||
<ColHeader
|
||||
id="Agent"
|
||||
scope="col"
|
||||
sortDirection="none"
|
||||
textAlign="start"
|
||||
width="125px"
|
||||
>
|
||||
User Agent
|
||||
</ColHeader>
|
||||
</Row>
|
||||
</Head>
|
||||
<Body
|
||||
id="page_view_results"
|
||||
/>
|
||||
</Table>
|
||||
</InfiniteScroll>
|
||||
</Item>
|
||||
</Flex>
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
Loading…
Reference in New Issue