Commit 08706f68 authored by Douwe Maan's avatar Douwe Maan
Browse files

Implement auxiliary blob viewers

parent 4b18ba863f27
/* 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';
}
......@@ -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');
......@@ -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);
});
});
}
}
......@@ -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
......
......@@ -110,11 +110,8 @@ def can_change_visibility_level?(project, current_user)
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
......
......@@ -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
......@@ -154,6 +157,12 @@ def rich_viewer
@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
......@@ -180,17 +189,18 @@ def simple_viewer_class
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
......@@ -12,7 +16,11 @@ def initialize(blob)
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?
......@@ -23,6 +31,10 @@ def self.simple?
type == :simple
end
def self.auxiliary?
type == :auxiliary
end
def self.client_side?
client_side
end
......@@ -39,8 +51,12 @@ def self.text?
!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?
......@@ -83,9 +99,7 @@ def render_error
end
def prepare!
if server_side? && blob.project
blob.load_all_data!(blob.project.repository)
end
# To be overridden by subclasses
end
private
......
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
......@@ -7,5 +7,11 @@ module ServerSide
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
......@@ -549,6 +549,13 @@ def license_key
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
......@@ -1083,8 +1090,8 @@ def cache_instance_variable_name(key)
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
......
......@@ -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
......
......@@ -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
......
- 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…')
- 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')
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment