Skip to content

Commit

Permalink
Merge pull request #5483 from pulibrary/viewer_refactor
Browse files Browse the repository at this point in the history
Refactor Viewer JS to use GraphQL
  • Loading branch information
tpendragon authored Oct 25, 2022
2 parents 6d63b2f + b124a5e commit 461e66b
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 43 deletions.
1 change: 1 addition & 0 deletions app/controllers/graphql_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true
class GraphqlController < ApplicationController
include TokenAuth
class_attribute :change_set_persister
self.change_set_persister = ::ChangeSetPersister.new(
metadata_adapter: Valkyrie::MetadataAdapter.find(:indexing_persister),
Expand Down
4 changes: 3 additions & 1 deletion app/graphql/types/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ def thumbnail_resource
nil
end

def embed; end
def embed
Embed.for(resource: object, ability: ability).to_graphql
end

def query_service
Valkyrie::MetadataAdapter.find(:indexing_persister).query_service
Expand Down
4 changes: 0 additions & 4 deletions app/graphql/types/scanned_resource_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,4 @@ def start_page
def source_metadata_identifier
Array.wrap(object.source_metadata_identifier).first
end

def embed
Embed.for(resource: object, ability: ability).to_graphql
end
end
96 changes: 90 additions & 6 deletions app/javascript/test/viewer/uv_manager.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,29 +42,39 @@ describe('UVManager', () => {
delete global.createUV
}
})
function buildMocks (status, externalManifest = false) {
function mockJquery () {
// Mock jQuery
const clickable = { click: () => clickable, on: () => clickable, is: () => clickable, outerHeight: () => clickable, width: () => clickable, height: () => clickable, hide: () => clickable, show: () => clickable, children: () => clickable }
global.$ = jest.fn().mockImplementation(() => clickable)
}

function mockManifests (status) {
// Mock $.ajax
const data = { status: status }
const jqxhr = { getResponseHeader: () => null }
global.$.ajax = jest.fn().mockImplementation(() => {
if (status !== 200) { return jQ.Deferred().reject(data, status, jqxhr) } else { return jQ.Deferred().resolve(data, status, jqxhr) }
})
}

function mockUvProvider (externalManifest = false, authToken = null) {
const getResult = jest.fn().mockImplementation(function (k) {
if (k === 'manifest') {
if (externalManifest === true) {
return 'https://example.org/other/iiif/manifest'
} else {
return 'https://localhost/concern/scanned_resources/12345/manifest'
if (authToken === null) {
return 'https://localhost/concern/scanned_resources/12345/manifest'
} else {
return `https://localhost/concern/scanned_resources/12345/manifest?auth_token=${authToken}`
}
}
} else if (k === 'config') {
return 'https://figgy.princeton.edu/uv/uv_config.json'
} else { return null }
})
// Mock UV Provider

// This makes it so global.UV.URLDataProvider.get returns our mock data
const provider = jest.fn().mockImplementation(() => {
return { get: getResult }
})
Expand All @@ -74,20 +84,94 @@ describe('UVManager', () => {
jest.spyOn(window.location, 'assign').mockImplementation(() => true)
}

let figgy_id = "12345"
function stubQuery(embedHash, label='Test', type='ScannedResource') {
global.fetch = jest.fn(() =>
Promise.resolve({
status: 200,
json: () => Promise.resolve(
{
"data": {
"resource":
{
"id": figgy_id,
"__typename": type,
"embed": embedHash,
"label": label
}
}
}
)
})
)
}

beforeEach(() => {
jest.clearAllMocks()
})

describe('initialize', () => {
it('redirects to viewer auth if the manifest 401s', async () => {
it('loads a viewer and title for a playlist', async () => {
document.body.innerHTML = initialHTML
buildMocks(401)
mockJquery()
mockUvProvider()
stubQuery({
"type": "html",
"content": "<iframe src='https://figgy.princeton.edu/viewer#?manifest=https://figgy.princeton.edu/concern/scanned_resources/78e15d09-3a79-4057-b358-4fde3d884bbb/manifest'></iframe>",
"status": "authorized"
},
"Test Playlist",
"Playlist"
)

// Initialize
const uvManager = new UVManager()
await uvManager.initialize()
expect(document.getElementById('title').innerHTML).toBe('Test Playlist')
})

it('passes on an auth token to graphql', async () => {
document.body.innerHTML = initialHTML
mockJquery()
mockUvProvider(false, '12')
stubQuery({
'type': 'html',
'content': '<iframe src="https://figgy.princeton.edu/viewer#?manifest=https://figgy.princeton.edu/concern/scanned_resources/78e15d09-3a79-4057-b358-4fde3d884bbb/manifest"></iframe>',
'status': 'authorized'
},
'Test Playlist',
'Playlist'
)

// Initialize
const uvManager = new UVManager()
await uvManager.initialize()
expect(document.getElementById('title').innerHTML).toBe('Test Playlist')
expect(global.fetch.mock.calls[0][0]).toBe('/graphql?auth_token=12')
expect(JSON.parse(global.fetch.mock.calls[0][1].body).query).toMatch('resource(id: "12345")')
})

it('redirects to viewer auth if graph says unauthenticated', async () => {
document.body.innerHTML = initialHTML
mockJquery()
mockUvProvider()
stubQuery({
"type": null,
"content": null,
"status": "unauthenticated"
})

// Initialize
const uvManager = new UVManager()
await uvManager.initialize()
expect(window.location.assign).toHaveBeenCalledWith('/viewer/12345/auth')
expect(LeafletViewer).not.toHaveBeenCalled()
})

it('falls back to a default viewer URI if not using a figgy manifest', async () => {
document.body.innerHTML = initialHTML
buildMocks(401, true)
mockJquery()
mockUvProvider(true)

// Initialize
const uvManager = new UVManager()
Expand Down
91 changes: 60 additions & 31 deletions app/javascript/viewer/uv_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,60 @@ export default class UVManager {
}

async loadUV () {
return this.checkManifest()
.then(this.createUV.bind(this))
// If creating the UV fails, don't build leaflet.
.then(() => { return this.buildLeafletViewer() })
.catch(this.requestAuth.bind(this))
.promise()
if (this.isFiggyManifest) {
const result = await this.checkFiggyStatus()
if (result.embed.status === 'unauthenticated') {
return window.location.assign('/viewer/' + this.figgyId + '/auth')
} else if (result.embed.status === 'authorized') {
this.createUV(null, null, result)
this.buildLeafletViewer()
}
} else {
return this.createUV()
}
}

async checkFiggyStatus() {
let url = "/graphql";
if (this.authToken) {
url = `${url}?auth_token=${this.authToken}`
}
var data = JSON.stringify({ query:`{
resource(id: "` + this.figgyId + `"){
id,
__typename,
label,
embed {
type,
content,
status
}
}
}`
})
return fetch(url,
{
method: "POST",
credentials: 'include',
body: data,
headers: {
'Content-Type': 'application/json',
}
}
)
.then((response) => response.json())
.then((response) => response.data.resource)
}

// Adds a tabbed viewer for Leaflet to show rasters, especially for mosaics.
buildLeafletViewer () {
this.leafletViewer = new LeafletViewer(this.figgyId, this.tabManager)
return this.leafletViewer.loadLeaflet()
}

checkManifest () {
return $.ajax(this.manifest, { type: 'HEAD' })
}

createUV (data, status, jqXHR) {
createUV (data, status, graphql_data) {
this.tabManager.onTabSelect(() => setTimeout(() => this.resize(), 100))
this.processTitle(jqXHR)
this.processTitle(graphql_data)
this.uvElement.show()
this.uv = createUV('#uv', {
root: 'uv',
Expand Down Expand Up @@ -113,16 +147,14 @@ export default class UVManager {
}
}

requestAuth (data, status) {
if (data.status === 401) {
if (this.manifest.includes(window.location.host)) {
window.location.assign('/viewer/' + this.figgyId + '/auth')
}
}
get figgyId () {
return this.manifest.replace('/manifest', '').replace(/.*\//, '').replace(/\?.*/, '')
}

get figgyId () {
return this.manifest.replace('/manifest', '').replace(/.*\//, '')
get authToken () {
const url = new URL(this.manifest)
const authToken = url.searchParams.get('auth_token')
return authToken
}

get isFiggyManifest () {
Expand All @@ -137,18 +169,15 @@ export default class UVManager {
}
}

processTitle (jqXHR) {
var linkHeader = jqXHR.getResponseHeader('Link')
if (linkHeader) {
var titleMatch = /title="(.+?)"/.exec(linkHeader)
if (titleMatch[1]) {
var title = titleMatch[1]
var titleElement = document.getElementById('title')
titleElement.textContent = title
titleElement.style.display = 'block'
this.resize()
}
processTitle (graphql_data) {
if (graphql_data === undefined || graphql_data.__typename !== 'Playlist') {
return
}
var title = graphql_data.label
var titleElement = document.getElementById('title')
titleElement.textContent = title
titleElement.style.display = 'block'
this.resize()
}

resize () {
Expand Down
1 change: 0 additions & 1 deletion config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ module Figgy
class Application < Rails::Application
config.load_defaults "6.0"
config.action_controller.forgery_protection_origin_check = false
config.action_dispatch.cookies_same_site_protection = :none
config.assets.quiet = true
config.generators do |generate|
generate.helper false
Expand Down
1 change: 1 addition & 0 deletions config/environments/production.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true
Rails.application.configure do
config.action_dispatch.cookies_same_site_protection = :none
config.cache_classes = true
config.eager_load = true
config.consider_all_requests_local = false
Expand Down
15 changes: 15 additions & 0 deletions spec/controllers/graphql_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@
before do
sign_in user if user
end
context "when logged in via an auth token" do
let(:user) { nil }
let(:scanned_resource) { FactoryBot.create_for_repository(:complete_private_scanned_resource, viewing_hint: "individuals") }
it "can run a graphql query" do
token = AuthToken.create(label: "Test", group: ["admin"]).token

post :execute, params: { query: query_string, auth_token: token, format: :json }

expect(response).to be_successful
json_response = JSON.parse(response.body)
expect(json_response["data"]).to eq(
"resource" => { "viewingHint" => "individuals" }
)
end
end
context "when logged in as a staff user" do
let(:user) { FactoryBot.create(:staff) }
it "can run a graphql query" do
Expand Down
11 changes: 11 additions & 0 deletions spec/features/scanned_resource_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,15 @@
expect(page).to have_content "Embargo Date"
expect(page).to have_content "Senior Thesis"
end

scenario "show page has a viewer", js: true do
file = fixture_file_upload("files/example.tif", "image/tiff")
resource = FactoryBot.create_for_repository(:scanned_resource, files: [file])

visit solr_document_path(id: resource.id)

within_frame(find(".uv-container > iframe")) do
expect(page).to have_selector(".uv.en-gb")
end
end
end

0 comments on commit 461e66b

Please sign in to comment.