Skip to content
Snippets Groups Projects
Commit 08706f68 authored by Douwe Maan's avatar Douwe Maan
Browse files

Implement auxiliary blob viewers

parent 62c93ab9
No related branches found
No related tags found
No related merge requests found
Showing
with 259 additions and 67 deletions
/* global Flash */
export default class BlobViewer {
constructor() {
BlobViewer.initAuxiliaryViewer();
this.initMainViewers();
}
static initAuxiliaryViewer() {
const auxiliaryViewer = document.querySelector('.blob-viewer[data-type="auxiliary"]');
if (!auxiliaryViewer) return;
BlobViewer.loadViewer(auxiliaryViewer);
}
initMainViewers() {
this.$fileHolder = $('.file-holder');
if (!this.$fileHolder.length) return;
this.switcher = document.querySelector('.js-blob-viewer-switcher');
this.switcherBtns = document.querySelectorAll('.js-blob-viewer-switch-btn');
this.copySourceBtn = document.querySelector('.js-copy-blob-source-btn');
this.simpleViewer = document.querySelector('.blob-viewer[data-type="simple"]');
this.richViewer = document.querySelector('.blob-viewer[data-type="rich"]');
this.$fileHolder = $('.file-holder');
 
const initialViewer = document.querySelector('.blob-viewer:not(.hidden)');
if (!initialViewer) return;
let initialViewerName = initialViewer.getAttribute('data-type');
this.simpleViewer = this.$fileHolder[0].querySelector('.blob-viewer[data-type="simple"]');
this.richViewer = this.$fileHolder[0].querySelector('.blob-viewer[data-type="rich"]');
 
this.initBindings();
 
this.switchToInitialViewer();
}
switchToInitialViewer() {
const initialViewer = this.$fileHolder[0].querySelector('.blob-viewer:not(.hidden)');
let initialViewerName = initialViewer.getAttribute('data-type');
if (this.switcher && location.hash.indexOf('#L') === 0) {
initialViewerName = 'simple';
}
Loading
Loading
@@ -64,40 +82,13 @@ export default class BlobViewer {
$(this.copySourceBtn).tooltip('fixTitle');
}
 
loadViewer(viewerParam) {
const viewer = viewerParam;
const url = viewer.getAttribute('data-url');
if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) {
return;
}
viewer.setAttribute('data-loading', 'true');
$.ajax({
url,
dataType: 'JSON',
})
.fail(() => new Flash('Error loading source view'))
.done((data) => {
viewer.innerHTML = data.html;
$(viewer).syntaxHighlight();
viewer.setAttribute('data-loaded', 'true');
this.$fileHolder.trigger('highlight:line');
this.toggleCopyButtonState();
});
}
switchToViewer(name) {
const newViewer = document.querySelector(`.blob-viewer[data-type='${name}']`);
const newViewer = this.$fileHolder[0].querySelector(`.blob-viewer[data-type='${name}']`);
if (this.activeViewer === newViewer) return;
 
const oldButton = document.querySelector('.js-blob-viewer-switch-btn.active');
const newButton = document.querySelector(`.js-blob-viewer-switch-btn[data-viewer='${name}']`);
const oldViewer = document.querySelector(`.blob-viewer:not([data-type='${name}'])`);
const oldViewer = this.$fileHolder[0].querySelector(`.blob-viewer:not([data-type='${name}'])`);
 
if (oldButton) {
oldButton.classList.remove('active');
Loading
Loading
@@ -118,6 +109,40 @@ export default class BlobViewer {
 
this.toggleCopyButtonState();
 
this.loadViewer(newViewer);
BlobViewer.loadViewer(newViewer)
.then((viewer) => {
$(viewer).syntaxHighlight();
this.$fileHolder.trigger('highlight:line');
this.toggleCopyButtonState();
})
.catch(() => new Flash('Error loading viewer'));
}
static loadViewer(viewerParam) {
const viewer = viewerParam;
const url = viewer.getAttribute('data-url');
return new Promise((resolve, reject) => {
if (!url || viewer.getAttribute('data-loaded') || viewer.getAttribute('data-loading')) {
resolve(viewer);
return;
}
viewer.setAttribute('data-loading', 'true');
$.ajax({
url,
dataType: 'JSON',
})
.fail(reject)
.done((data) => {
viewer.innerHTML = data.html;
viewer.setAttribute('data-loaded', 'true');
resolve(viewer);
});
});
}
}
Loading
Loading
@@ -3,8 +3,11 @@ module RendersBlob
 
def render_blob_json(blob)
viewer =
if params[:viewer] == 'rich'
case params[:viewer]
when 'rich'
blob.rich_viewer
when 'auxiliary'
blob.auxiliary_viewer
else
blob.simple_viewer
end
Loading
Loading
Loading
Loading
@@ -110,11 +110,8 @@ module ProjectsHelper
end
 
def license_short_name(project)
return 'LICENSE' if project.repository.license_key.nil?
license = Licensee::License.new(project.repository.license_key)
license.nickname || license.name
license = project.repository.license
license&.nickname || license&.name || 'LICENSE'
end
 
def last_push_event
Loading
Loading
Loading
Loading
@@ -34,10 +34,13 @@ class Blob < SimpleDelegator
 
BlobViewer::BinarySTL,
BlobViewer::TextSTL
].freeze
].sort_by { |v| v.binary? ? 0 : 1 }.freeze
 
BINARY_VIEWERS = RICH_VIEWERS.select(&:binary?).freeze
TEXT_VIEWERS = RICH_VIEWERS.select(&:text?).freeze
AUXILIARY_VIEWERS = [
BlobViewer::GitlabCiYml,
BlobViewer::RouteMap,
BlobViewer::License
].freeze
 
attr_reader :project
 
Loading
Loading
@@ -154,6 +157,12 @@ class Blob < SimpleDelegator
@rich_viewer = rich_viewer_class&.new(self)
end
 
def auxiliary_viewer
return @auxiliary_viewer if defined?(@auxiliary_viewer)
@auxiliary_viewer = auxiliary_viewer_class&.new(self)
end
def rendered_as_text?(ignore_errors: true)
simple_viewer.text? && (ignore_errors || simple_viewer.render_error.nil?)
end
Loading
Loading
@@ -180,17 +189,18 @@ class Blob < SimpleDelegator
end
 
def rich_viewer_class
viewer_class_from(RICH_VIEWERS)
end
def auxiliary_viewer_class
viewer_class_from(AUXILIARY_VIEWERS)
end
def viewer_class_from(classes)
return if empty? || external_storage_error?
 
classes =
if stored_externally?
BINARY_VIEWERS + TEXT_VIEWERS
elsif binary?
BINARY_VIEWERS
else # text
TEXT_VIEWERS
end
verify_binary = !stored_externally?
 
classes.find { |viewer_class| viewer_class.can_render?(self) }
classes.find { |viewer_class| viewer_class.can_render?(self, verify_binary: verify_binary) }
end
end
module BlobViewer
module Auxiliary
extend ActiveSupport::Concern
included do
self.loading_partial_name = 'loading_auxiliary'
self.type = :auxiliary
self.max_size = 100.kilobytes
self.absolute_max_size = 100.kilobytes
end
end
end
module BlobViewer
class Base
class_attribute :partial_name, :type, :extensions, :client_side, :binary, :switcher_icon, :switcher_title, :max_size, :absolute_max_size
PARTIAL_PATH_PREFIX = 'projects/blob/viewers'.freeze
 
delegate :partial_path, :rich?, :simple?, :client_side?, :server_side?, :text?, :binary?, to: :class
class_attribute :partial_name, :loading_partial_name, :type, :extensions, :file_type, :client_side, :binary, :switcher_icon, :switcher_title, :max_size, :absolute_max_size
self.loading_partial_name = 'loading'
delegate :partial_path, :loading_partial_path, :rich?, :simple?, :client_side?, :server_side?, :text?, :binary?, to: :class
 
attr_reader :blob
attr_accessor :override_max_size
Loading
Loading
@@ -12,7 +16,11 @@ module BlobViewer
end
 
def self.partial_path
"projects/blob/viewers/#{partial_name}"
File.join(PARTIAL_PATH_PREFIX, partial_name)
end
def self.loading_partial_path
File.join(PARTIAL_PATH_PREFIX, loading_partial_name)
end
 
def self.rich?
Loading
Loading
@@ -23,6 +31,10 @@ module BlobViewer
type == :simple
end
 
def self.auxiliary?
type == :auxiliary
end
def self.client_side?
client_side
end
Loading
Loading
@@ -39,8 +51,12 @@ module BlobViewer
!binary?
end
 
def self.can_render?(blob)
!extensions || extensions.include?(blob.extension)
def self.can_render?(blob, verify_binary: true)
return false if verify_binary && binary? != blob.binary?
return true if extensions&.include?(blob.extension)
return true if file_type && Gitlab::FileDetector.type_of(blob.path) == file_type
false
end
 
def too_large?
Loading
Loading
@@ -83,9 +99,7 @@ module BlobViewer
end
 
def prepare!
if server_side? && blob.project
blob.load_all_data!(blob.project.repository)
end
# To be overridden by subclasses
end
 
private
Loading
Loading
module BlobViewer
class GitlabCiYml < Base
include ServerSide
include Auxiliary
self.partial_name = 'gitlab_ci_yml'
self.loading_partial_name = 'gitlab_ci_yml_loading'
self.file_type = :gitlab_ci
self.binary = false
def validation_message
return @validation_message if defined?(@validation_message)
prepare!
@validation_message = Ci::GitlabCiYamlProcessor.validation_message(blob.data)
end
def valid?
validation_message.blank?
end
end
end
module BlobViewer
class License < Base
# We treat the License viewer as if it renders the content client-side,
# so that it doesn't attempt to load the entire blob contents and is
# rendered synchronously instead of loaded asynchronously.
include ClientSide
include Auxiliary
self.partial_name = 'license'
self.file_type = :license
self.binary = false
def license
blob.project.repository.license
end
def render_error
return if license
:unknown_license
end
end
end
module BlobViewer
class RouteMap < Base
include ServerSide
include Auxiliary
self.partial_name = 'route_map'
self.loading_partial_name = 'route_map_loading'
self.file_type = :route_map
self.binary = false
def validation_message
return @validation_message if defined?(@validation_message)
prepare!
@validation_message =
begin
Gitlab::RouteMap.new(blob.data)
nil
rescue Gitlab::RouteMap::FormatError => e
e.message
end
end
def valid?
validation_message.blank?
end
end
end
Loading
Loading
@@ -7,5 +7,11 @@ module BlobViewer
self.max_size = 2.megabytes
self.absolute_max_size = 5.megabytes
end
def prepare!
if blob.project
blob.load_all_data!(blob.project.repository)
end
end
end
end
Loading
Loading
@@ -549,6 +549,13 @@ class Repository
end
cache_method :license_key
 
def license
return @license if defined?(@license)
return unless license_key
@license = Licensee::License.new(license_key)
end
def gitignore
file_on_head(:gitignore)
end
Loading
Loading
@@ -1083,8 +1090,8 @@ class Repository
 
def file_on_head(type)
if head = tree(:head)
head.blobs.find do |file|
Gitlab::FileDetector.type_of(file.name) == type
head.blobs.find do |blob|
Gitlab::FileDetector.type_of(blob.path) == type
end
end
end
Loading
Loading
Loading
Loading
@@ -6,6 +6,11 @@
- blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
= render blob_commit, project: @project, ref: @ref
 
- auxiliary_viewer = blob.auxiliary_viewer
- if auxiliary_viewer && !auxiliary_viewer.render_error
.well-segment.blob-auxiliary-viewer
= render 'projects/blob/viewer', viewer: auxiliary_viewer
#blob-content-holder.blob-content-holder
%article.file-holder
= render "projects/blob/header", blob: blob
Loading
Loading
Loading
Loading
@@ -5,8 +5,7 @@
- viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_asynchronously
.blob-viewer{ data: { type: viewer.type, url: viewer_url }, class: ('hidden' if hidden) }
- if load_asynchronously
.text-center.prepend-top-default.append-bottom-default
= icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content')
= render viewer.loading_partial_path, viewer: viewer
- elsif render_error
= render 'projects/blob/render_error', viewer: viewer
- else
Loading
Loading
- if viewer.valid?
= icon('check fw')
This GitLab CI configuration is valid.
- else
= icon('warning fw')
This GitLab CI configuration is invalid:
= viewer.validation_message
= link_to 'Learn more', help_page_path('ci/yaml/README')
= icon('spinner spin fw')
Validating GitLab CI configuration…
= link_to 'Learn more', help_page_path('ci/yaml/README')
- license = viewer.license
= icon('balance-scale fw')
This project is licensed under the
= succeed '.' do
%strong= license.name
= link_to 'Learn more about this license', license.url, target: '_blank', rel: 'noopener noreferrer'
.text-center.prepend-top-default.append-bottom-default
= icon('spinner spin 2x', 'aria-hidden' => 'true', 'aria-label' => 'Loading content…')
= icon('spinner spin fw')
Loading…
- if viewer.valid?
= icon('check fw')
This Route Map is valid.
- else
= icon('warning fw')
This Route Map is invalid:
= viewer.validation_message
= link_to 'Learn more', help_page_path('ci/environments', anchor: 'route-map')
= icon('spinner spin fw')
Validating Route Map…
= link_to 'Learn more', help_page_path('ci/environments', anchor: 'route-map')
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment