mirror of
https://github.com/discourse/discourse.git
synced 2025-10-03 17:21:20 +08:00
DEV: Move discourse-data-explorer to core (#33570)
https://meta.discourse.org/t/373574 Internal `/t/-/156778`
This commit is contained in:
parent
079dd05376
commit
e65677f217
208 changed files with 14870 additions and 0 deletions
4
.github/labeler.yml
vendored
4
.github/labeler.yml
vendored
|
@ -78,6 +78,10 @@ discourse-chat-integration:
|
|||
- changed-files:
|
||||
- any-glob-to-any-file: plugins/discourse-chat-integration/**/*
|
||||
|
||||
discourse-data-explorer:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: plugins/discourse-data-explorer/**/*
|
||||
|
||||
footnote:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: plugins/footnote/**/*
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -62,6 +62,7 @@
|
|||
!/plugins/discourse-rss-polling
|
||||
!/plugins/discourse-math
|
||||
!/plugins/discourse-chat-integration
|
||||
!/plugins/discourse-data-explorer
|
||||
/plugins/*/auto_generated
|
||||
|
||||
/spec/fixtures/plugins/my_plugin/auto_generated
|
||||
|
|
6
plugins/discourse-data-explorer/README.md
Normal file
6
plugins/discourse-data-explorer/README.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Data Explorer Plugin
|
||||
|
||||
This plugin allows admins to run SQL queries against the live Discourse database,
|
||||
including parameterized queries and formatting for several common column types.
|
||||
|
||||
For more information, please see: https://meta.discourse.org/t/data-explorer-plugin/32566
|
|
@ -0,0 +1,236 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ::DiscourseDataExplorer
|
||||
class QueryController < ApplicationController
|
||||
requires_plugin PLUGIN_NAME
|
||||
|
||||
before_action :set_group, only: %i[group_reports_index group_reports_show group_reports_run]
|
||||
before_action :set_query, only: %i[group_reports_show group_reports_run show update]
|
||||
before_action :ensure_admin
|
||||
|
||||
skip_before_action :check_xhr, only: %i[show group_reports_run run]
|
||||
skip_before_action :ensure_admin,
|
||||
only: %i[group_reports_index group_reports_show group_reports_run]
|
||||
|
||||
def index
|
||||
queries = Query.where(hidden: false).order(:last_run_at, :name).includes(:groups).to_a
|
||||
|
||||
database_queries_ids = Query.pluck(:id)
|
||||
Queries.default.each do |params|
|
||||
attributes = params.last
|
||||
next if database_queries_ids.include?(attributes["id"])
|
||||
query = Query.new
|
||||
query.id = attributes["id"]
|
||||
query.sql = attributes["sql"]
|
||||
query.name = attributes["name"]
|
||||
query.description = attributes["description"]
|
||||
query.user_id = Discourse::SYSTEM_USER_ID.to_s
|
||||
queries << query
|
||||
end
|
||||
|
||||
render_serialized queries, QuerySerializer, root: "queries"
|
||||
end
|
||||
|
||||
def show
|
||||
check_xhr unless params[:export]
|
||||
|
||||
if params[:export]
|
||||
response.headers["Content-Disposition"] = "attachment; filename=#{@query.slug}.dcquery.json"
|
||||
response.sending_file = true
|
||||
end
|
||||
|
||||
return raise Discourse::NotFound if !guardian.user_can_access_query?(@query) || @query.hidden
|
||||
render_serialized @query, QueryDetailsSerializer, root: "query"
|
||||
end
|
||||
|
||||
def groups
|
||||
render json: Group.all.select(:id, :name).as_json(only: %i[id name]), root: false
|
||||
end
|
||||
|
||||
def group_reports_index
|
||||
return raise Discourse::NotFound unless guardian.user_is_a_member_of_group?(@group)
|
||||
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
queries = Query.for_group(@group)
|
||||
render_serialized(queries, QuerySerializer, root: "queries")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def group_reports_show
|
||||
if !guardian.group_and_user_can_access_query?(@group, @query) || @query.hidden
|
||||
return raise Discourse::NotFound
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
query_group = QueryGroup.find_by(query_id: @query.id, group_id: @group.id)
|
||||
|
||||
render json: {
|
||||
query: serialize_data(@query, QueryDetailsSerializer, root: nil),
|
||||
query_group: serialize_data(query_group, QueryGroupSerializer, root: nil),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def group_reports_run
|
||||
if !guardian.group_and_user_can_access_query?(@group, @query) || @query.hidden
|
||||
return raise Discourse::NotFound
|
||||
end
|
||||
|
||||
run
|
||||
end
|
||||
|
||||
def create
|
||||
query =
|
||||
Query.create!(
|
||||
params
|
||||
.require(:query)
|
||||
.permit(:name, :description, :sql)
|
||||
.merge(user_id: current_user.id, last_run_at: Time.now),
|
||||
)
|
||||
group_ids = params.require(:query)[:group_ids]
|
||||
group_ids&.each { |group_id| query.query_groups.find_or_create_by!(group_id: group_id) }
|
||||
render_serialized query, QueryDetailsSerializer, root: "query"
|
||||
end
|
||||
|
||||
def update
|
||||
ActiveRecord::Base.transaction do
|
||||
@query.update!(
|
||||
params.require(:query).permit(:name, :sql, :description).merge(hidden: false),
|
||||
)
|
||||
|
||||
group_ids = params.require(:query)[:group_ids]
|
||||
QueryGroup.where.not(group_id: group_ids).where(query_id: @query.id).delete_all
|
||||
group_ids&.each { |group_id| @query.query_groups.find_or_create_by!(group_id: group_id) }
|
||||
end
|
||||
|
||||
render_serialized @query, QueryDetailsSerializer, root: "query"
|
||||
rescue ValidationError => e
|
||||
render_json_error e.message
|
||||
end
|
||||
|
||||
def destroy
|
||||
query = Query.where(id: params[:id]).first_or_initialize
|
||||
query.update!(hidden: true)
|
||||
|
||||
render json: { success: true, errors: [] }
|
||||
end
|
||||
|
||||
def schema
|
||||
schema_version = DB.query_single("SELECT max(version) AS tag FROM schema_migrations").first
|
||||
if stale?(public: true, etag: schema_version, template: false)
|
||||
render json: DataExplorer.schema
|
||||
end
|
||||
end
|
||||
|
||||
# Return value:
|
||||
# success - true/false. if false, inspect the errors value.
|
||||
# errors - array of strings.
|
||||
# params - hash. Echo of the query parameters as executed.
|
||||
# duration - float. Time to execute the query, in milliseconds, to 1 decimal place.
|
||||
# columns - array of strings. Titles of the returned columns, in order.
|
||||
# explain - string. (Optional - pass explain=true in the request) Postgres query plan, UNIX newlines.
|
||||
# rows - array of array of strings. Results of the query. In the same order as 'columns'.
|
||||
def run
|
||||
rate_limit_query_runs!
|
||||
|
||||
check_xhr unless params[:download]
|
||||
|
||||
query = Query.find(params[:id].to_i)
|
||||
query.update!(last_run_at: Time.now)
|
||||
|
||||
response.sending_file = true if params[:download]
|
||||
|
||||
query_params = {}
|
||||
query_params = MultiJson.load(params[:params]) if params[:params]
|
||||
|
||||
opts = { current_user: current_user&.username }
|
||||
opts[:explain] = true if params[:explain] == "true"
|
||||
|
||||
opts[:limit] = if params[:format] == "csv"
|
||||
if params[:limit].present?
|
||||
limit = params[:limit].to_i
|
||||
limit = QUERY_RESULT_MAX_LIMIT if limit > QUERY_RESULT_MAX_LIMIT
|
||||
limit
|
||||
else
|
||||
QUERY_RESULT_MAX_LIMIT
|
||||
end
|
||||
elsif params[:limit].present?
|
||||
params[:limit] == "ALL" ? "ALL" : params[:limit].to_i
|
||||
end
|
||||
|
||||
result = DataExplorer.run_query(query, query_params, opts)
|
||||
|
||||
if result[:error]
|
||||
err = result[:error]
|
||||
|
||||
# Pretty printing logic
|
||||
err_class = err.class
|
||||
err_msg = err.message
|
||||
if err.is_a? ActiveRecord::StatementInvalid
|
||||
err_class = err.original_exception.class
|
||||
err_msg.gsub!("#{err_class}:", "")
|
||||
else
|
||||
err_msg = "#{err_class}: #{err_msg}"
|
||||
end
|
||||
|
||||
render json: { success: false, errors: [err_msg] }, status: 422
|
||||
else
|
||||
content_disposition =
|
||||
"attachment; filename=#{query.slug}@#{Slug.for(Discourse.current_hostname, "discourse")}-#{Date.today}.dcqresult"
|
||||
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
response.headers["Content-Disposition"] = "#{content_disposition}.json" if params[
|
||||
:download
|
||||
]
|
||||
|
||||
render json:
|
||||
ResultFormatConverter.convert(
|
||||
:json,
|
||||
result,
|
||||
query_params:,
|
||||
download: params[:download],
|
||||
explain: params[:explain] == "true",
|
||||
)
|
||||
end
|
||||
format.csv do
|
||||
response.headers["Content-Disposition"] = "#{content_disposition}.csv"
|
||||
|
||||
render plain: ResultFormatConverter.convert(:csv, result)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def rate_limit_query_runs!
|
||||
return if !is_api? && !is_user_api?
|
||||
|
||||
RateLimiter.new(
|
||||
nil,
|
||||
"api-query-run-10-sec",
|
||||
GlobalSetting.max_data_explorer_api_reqs_per_10_seconds,
|
||||
10.seconds,
|
||||
).performed!
|
||||
rescue RateLimiter::LimitExceeded => e
|
||||
if GlobalSetting.max_data_explorer_api_req_mode.include?("warn")
|
||||
Discourse.warn("Query run 10 second rate limit exceeded", query_id: params[:id])
|
||||
end
|
||||
raise e if GlobalSetting.max_data_explorer_api_req_mode.include?("block")
|
||||
end
|
||||
|
||||
def set_group
|
||||
@group = Group.find_by(name: params["group_name"])
|
||||
end
|
||||
|
||||
def set_query
|
||||
@query = Query.find(params[:id])
|
||||
raise Discourse::NotFound unless @query
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class DeleteHiddenQueries < ::Jobs::Scheduled
|
||||
every 7.days
|
||||
|
||||
def execute(args)
|
||||
return unless SiteSetting.data_explorer_enabled
|
||||
|
||||
DiscourseDataExplorer::Query
|
||||
.where("id > 0")
|
||||
.where(hidden: true)
|
||||
.where(
|
||||
"(last_run_at IS NULL OR last_run_at < :days_ago) AND updated_at < :days_ago",
|
||||
days_ago: 7.days.ago,
|
||||
)
|
||||
.delete_all
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,79 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ::DiscourseDataExplorer
|
||||
class QueryFinder
|
||||
def self.find(id)
|
||||
default_query = Queries.default[id.to_s]
|
||||
return raise ActiveRecord::RecordNotFound unless default_query
|
||||
|
||||
query = Query.find_by(id: id) || Query.new
|
||||
query.attributes = default_query
|
||||
query.user_id = Discourse::SYSTEM_USER_ID.to_s
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
class Query < ActiveRecord::Base
|
||||
self.table_name = "data_explorer_queries"
|
||||
|
||||
has_many :query_groups
|
||||
has_many :groups, through: :query_groups
|
||||
belongs_to :user
|
||||
validates :name, presence: true
|
||||
|
||||
scope :for_group,
|
||||
->(group) do
|
||||
where(hidden: false).joins(
|
||||
"INNER JOIN data_explorer_query_groups
|
||||
ON data_explorer_query_groups.query_id = data_explorer_queries.id
|
||||
AND data_explorer_query_groups.group_id = #{group.id}",
|
||||
)
|
||||
end
|
||||
|
||||
def params
|
||||
@params ||= Parameter.create_from_sql(sql)
|
||||
end
|
||||
|
||||
def cast_params(input_params)
|
||||
result = {}.with_indifferent_access
|
||||
self.params.each do |pobj|
|
||||
result[pobj.identifier] = pobj.cast_to_ruby input_params[pobj.identifier]
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
def slug
|
||||
Slug.for(name).presence || "query-#{id}"
|
||||
end
|
||||
|
||||
def self.find(id)
|
||||
return super if id.to_i >= 0
|
||||
QueryFinder.find(id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# for `Query.unscoped.find`
|
||||
class ActiveRecord_Relation
|
||||
def find(id)
|
||||
return super if id.to_i >= 0
|
||||
QueryFinder.find(id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: data_explorer_queries
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# name :string
|
||||
# description :text
|
||||
# sql :text default("SELECT 1"), not null
|
||||
# user_id :integer
|
||||
# last_run_at :datetime
|
||||
# hidden :boolean default(FALSE), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ::DiscourseDataExplorer
|
||||
class QueryGroup < ActiveRecord::Base
|
||||
self.table_name = "data_explorer_query_groups"
|
||||
|
||||
belongs_to :query
|
||||
belongs_to :group
|
||||
|
||||
has_many :bookmarks, as: :bookmarkable
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: data_explorer_query_groups
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# query_id :bigint
|
||||
# group_id :integer
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_data_explorer_query_groups_on_group_id (group_id)
|
||||
# index_data_explorer_query_groups_on_query_id (query_id)
|
||||
# index_data_explorer_query_groups_on_query_id_and_group_id (query_id,group_id) UNIQUE
|
||||
#
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ::DiscourseDataExplorer
|
||||
class QueryDetailsSerializer < QuerySerializer
|
||||
attributes :sql, :param_info, :created_at, :hidden
|
||||
|
||||
def param_info
|
||||
object&.params&.uniq { |p| p.identifier }&.map(&:to_hash)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ::DiscourseDataExplorer
|
||||
class QueryGroupBookmarkSerializer < UserBookmarkBaseSerializer
|
||||
def title
|
||||
fancy_title
|
||||
end
|
||||
|
||||
def fancy_title
|
||||
data_explorer_query.name
|
||||
end
|
||||
|
||||
def cooked
|
||||
data_explorer_query.description
|
||||
end
|
||||
|
||||
def bookmarkable_user
|
||||
@bookmarkable_user ||= data_explorer_query.user
|
||||
end
|
||||
|
||||
def bookmarkable_url
|
||||
"/g/#{data_explorer_query_group.group.name}/reports/#{data_explorer_query_group.query_id}"
|
||||
end
|
||||
|
||||
def excerpt
|
||||
return nil unless cooked
|
||||
@excerpt ||= PrettyText.excerpt(cooked, 300, keep_emoji_images: true)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def data_explorer_query
|
||||
data_explorer_query_group.query
|
||||
end
|
||||
|
||||
def data_explorer_query_group
|
||||
object.bookmarkable
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ::DiscourseDataExplorer
|
||||
class QueryGroupSerializer < ActiveModel::Serializer
|
||||
attributes :id, :group_id, :query_id, :bookmark
|
||||
|
||||
def query_group_bookmark
|
||||
@query_group_bookmark ||= Bookmark.find_by(user: scope.user, bookmarkable: object)
|
||||
end
|
||||
|
||||
def include_bookmark?
|
||||
query_group_bookmark.present?
|
||||
end
|
||||
|
||||
def bookmark
|
||||
{
|
||||
id: query_group_bookmark.id,
|
||||
reminder_at: query_group_bookmark.reminder_at,
|
||||
name: query_group_bookmark.name,
|
||||
auto_delete_preference: query_group_bookmark.auto_delete_preference,
|
||||
bookmarkable_id: query_group_bookmark.bookmarkable_id,
|
||||
bookmarkable_type: query_group_bookmark.bookmarkable_type,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ::DiscourseDataExplorer
|
||||
class QuerySerializer < ActiveModel::Serializer
|
||||
attributes :id, :name, :description, :username, :group_ids, :last_run_at, :user_id
|
||||
|
||||
def username
|
||||
object&.user&.username
|
||||
end
|
||||
|
||||
def group_ids
|
||||
object.groups.map(&:id)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DiscourseDataExplorer::SmallBadgeSerializer < ApplicationSerializer
|
||||
attributes :id, :name, :display_name, :badge_type, :description, :icon
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ::DiscourseDataExplorer
|
||||
class SmallPostWithExcerptSerializer < ApplicationSerializer
|
||||
attributes :id, :topic_id, :post_number, :excerpt, :username, :avatar_template
|
||||
|
||||
def excerpt
|
||||
Post.excerpt(object.cooked, 70)
|
||||
end
|
||||
|
||||
def username
|
||||
object.user && object.user.username
|
||||
end
|
||||
|
||||
def avatar_template
|
||||
object.user && object.user.avatar_template
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
import buildPluginAdapter from "admin/adapters/build-plugin";
|
||||
|
||||
export default buildPluginAdapter("explorer").extend({});
|
|
@ -0,0 +1,5 @@
|
|||
const CodeView = <template>
|
||||
<pre><code class={{@codeClass}}>{{@value}}</code></pre>
|
||||
</template>;
|
||||
|
||||
export default CodeView;
|
|
@ -0,0 +1,101 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
import themeColor from "../lib/themeColor";
|
||||
|
||||
export default class DataExplorerBarChart extends Component {
|
||||
chart;
|
||||
barsColor = themeColor("--tertiary");
|
||||
barsHoverColor = themeColor("--tertiary-high");
|
||||
gridColor = themeColor("--primary-low");
|
||||
labelsColor = themeColor("--primary-medium");
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
this.chart.destroy();
|
||||
}
|
||||
|
||||
get config() {
|
||||
const data = this.data;
|
||||
const options = this.options;
|
||||
return {
|
||||
type: "bar",
|
||||
data,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
get data() {
|
||||
const labels = this.args.labels;
|
||||
return {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: this.args.datasetName,
|
||||
data: this.args.values,
|
||||
backgroundColor: this.barsColor,
|
||||
hoverBackgroundColor: this.barsHoverColor,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
get options() {
|
||||
return {
|
||||
scales: {
|
||||
legend: {
|
||||
labels: {
|
||||
fontColor: this.labelsColor,
|
||||
},
|
||||
},
|
||||
xAxes: [
|
||||
{
|
||||
gridLines: {
|
||||
color: this.gridColor,
|
||||
zeroLineColor: this.gridColor,
|
||||
},
|
||||
ticks: {
|
||||
fontColor: this.labelsColor,
|
||||
},
|
||||
},
|
||||
],
|
||||
yAxes: [
|
||||
{
|
||||
gridLines: {
|
||||
color: this.gridColor,
|
||||
zeroLineColor: this.gridColor,
|
||||
},
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
fontColor: this.labelsColor,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@bind
|
||||
async initChart(canvas) {
|
||||
await loadScript("/javascripts/Chart.min.js");
|
||||
const context = canvas.getContext("2d");
|
||||
// eslint-disable-next-line
|
||||
this.chart = new Chart(context, this.config);
|
||||
}
|
||||
|
||||
@action
|
||||
updateChartData() {
|
||||
this.chart.data = this.data;
|
||||
this.chart.update();
|
||||
}
|
||||
|
||||
<template>
|
||||
<canvas
|
||||
{{didInsert this.initChart}}
|
||||
{{on "change" this.updateChartData}}
|
||||
></canvas>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { isBlank, isEmpty } from "@ember/utils";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import { debounce } from "discourse/lib/decorators";
|
||||
import OneTable from "./explorer-schema/one-table";
|
||||
|
||||
export default class ExplorerSchema extends Component {
|
||||
@tracked filter;
|
||||
@tracked loading;
|
||||
@tracked hideSchema = this.args.hideSchema;
|
||||
|
||||
get transformedSchema() {
|
||||
const schema = this.args.schema;
|
||||
for (const key in schema) {
|
||||
if (!schema.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
schema[key].forEach((col) => {
|
||||
const notes_components = [];
|
||||
if (col.primary) {
|
||||
notes_components.push("primary key");
|
||||
}
|
||||
if (col.is_nullable) {
|
||||
notes_components.push("null");
|
||||
}
|
||||
if (col.column_default) {
|
||||
notes_components.push("default " + col.column_default);
|
||||
}
|
||||
if (col.fkey_info) {
|
||||
notes_components.push("fkey " + col.fkey_info);
|
||||
}
|
||||
if (col.denormal) {
|
||||
notes_components.push("denormal " + col.denormal);
|
||||
}
|
||||
const notes = notes_components.join(", ");
|
||||
|
||||
if (notes) {
|
||||
col.notes = notes;
|
||||
}
|
||||
|
||||
if (col.enum || col.column_desc) {
|
||||
col.havepopup = true;
|
||||
}
|
||||
|
||||
col.havetypeinfo = !!(col.notes || col.enum || col.column_desc);
|
||||
});
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
get filteredTables() {
|
||||
let tables = [];
|
||||
let filter = this.filter;
|
||||
|
||||
try {
|
||||
if (!isBlank(this.filter)) {
|
||||
filter = new RegExp(this.filter);
|
||||
}
|
||||
} catch {
|
||||
filter = null;
|
||||
}
|
||||
|
||||
const haveFilter = !!filter;
|
||||
|
||||
for (const key in this.transformedSchema) {
|
||||
if (!this.transformedSchema.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
if (!haveFilter) {
|
||||
tables.push({
|
||||
name: key,
|
||||
columns: this.transformedSchema[key],
|
||||
open: false,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check the table name vs the filter
|
||||
if (filter.source === key || filter.source + "s" === key) {
|
||||
tables.unshift({
|
||||
name: key,
|
||||
columns: this.transformedSchema[key],
|
||||
open: haveFilter,
|
||||
});
|
||||
} else if (filter.test(key)) {
|
||||
// whole table matches
|
||||
tables.push({
|
||||
name: key,
|
||||
columns: this.transformedSchema[key],
|
||||
open: haveFilter,
|
||||
});
|
||||
} else {
|
||||
// filter the columns
|
||||
let filterCols = [];
|
||||
this.transformedSchema[key].forEach((col) => {
|
||||
if (filter.source === col.column_name) {
|
||||
filterCols.unshift(col);
|
||||
} else if (filter.test(col.column_name)) {
|
||||
filterCols.push(col);
|
||||
}
|
||||
});
|
||||
if (!isEmpty(filterCols)) {
|
||||
tables.push({
|
||||
name: key,
|
||||
columns: filterCols,
|
||||
open: haveFilter,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return tables;
|
||||
}
|
||||
|
||||
@debounce(500)
|
||||
updateFilter(value) {
|
||||
this.filter = value.toLowerCase();
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
@action
|
||||
filterChanged(value) {
|
||||
this.loading = true;
|
||||
this.updateFilter(value);
|
||||
}
|
||||
|
||||
@action
|
||||
collapseSchema() {
|
||||
this.hideSchema = true;
|
||||
this.args.updateHideSchema(true);
|
||||
}
|
||||
|
||||
@action
|
||||
expandSchema() {
|
||||
this.hideSchema = false;
|
||||
this.args.updateHideSchema(false);
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if this.hideSchema}}
|
||||
<DButton
|
||||
@action={{this.expandSchema}}
|
||||
@icon="chevron-left"
|
||||
class="no-text unhide"
|
||||
/>
|
||||
{{else}}
|
||||
<div class="schema">
|
||||
<div class="schema-search inline-form full-width">
|
||||
<input
|
||||
type="text"
|
||||
{{! template-lint-disable no-action }}
|
||||
{{on "input" (action "filterChanged" value="target.value")}}
|
||||
/>
|
||||
<DButton
|
||||
@action={{this.collapseSchema}}
|
||||
@icon="chevron-right"
|
||||
class="no-text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="schema-container">
|
||||
<ConditionalLoadingSpinner @condition={{this.loading}}>
|
||||
<ul>
|
||||
{{#each this.filteredTables as |table|}}
|
||||
<OneTable @table={{table}} />
|
||||
{{/each}}
|
||||
</ul>
|
||||
</ConditionalLoadingSpinner>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import Component from "@glimmer/component";
|
||||
|
||||
export default class EnumInfo extends Component {
|
||||
get enuminfo() {
|
||||
return Object.entries(this.args.col.enum).map(([value, name]) => ({
|
||||
value,
|
||||
name,
|
||||
}));
|
||||
}
|
||||
|
||||
<template>
|
||||
<ol>
|
||||
{{#each this.enuminfo as |enum|}}
|
||||
<li value={{enum.value}}>
|
||||
{{enum.name}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { on } from "@ember/modifier";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import EnumInfo from "./enum-info";
|
||||
|
||||
export default class OneTable extends Component {
|
||||
@tracked open = this.args.table.open;
|
||||
|
||||
get styles() {
|
||||
return this.open ? "open" : "";
|
||||
}
|
||||
|
||||
@bind
|
||||
toggleOpen() {
|
||||
this.open = !this.open;
|
||||
}
|
||||
|
||||
<template>
|
||||
<li class="schema-table {{this.styles}}">
|
||||
{{! template-lint-enable no-invalid-interactive }}
|
||||
<div
|
||||
class="schema-table-name"
|
||||
role="button"
|
||||
{{on "click" this.toggleOpen}}
|
||||
>
|
||||
{{#if this.open}}
|
||||
{{icon "caret-down"}}
|
||||
{{else}}
|
||||
{{icon "caret-right"}}
|
||||
{{/if}}
|
||||
{{@table.name}}
|
||||
</div>
|
||||
|
||||
<div class="schema-table-cols">
|
||||
{{#if this.open}}
|
||||
<dl>
|
||||
{{#each @table.columns as |col|}}
|
||||
<div>
|
||||
<dt
|
||||
class={{if col.sensitive "sensitive"}}
|
||||
title={{if col.sensitive (i18n "explorer.schema.sensitive")}}
|
||||
>
|
||||
{{#if col.sensitive}}
|
||||
{{icon "triangle-exclamation"}}
|
||||
{{/if}}
|
||||
{{col.column_name}}
|
||||
</dt>
|
||||
<dd>
|
||||
{{col.data_type}}
|
||||
{{#if col.havetypeinfo}}
|
||||
<br />
|
||||
{{#if col.havepopup}}
|
||||
<div class="popup-info">
|
||||
{{icon "info"}}
|
||||
<div class="popup">
|
||||
{{col.column_desc}}
|
||||
{{#if col.enum}}
|
||||
<EnumInfo @col={{col}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<span class="schema-typenotes">
|
||||
{{col.notes}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</dd>
|
||||
</div>
|
||||
{{/each}}
|
||||
</dl>
|
||||
{{/if}}
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { htmlSafe } from "@ember/template";
|
||||
import DModal from "discourse/components/d-modal";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
const QueryHelp = <template>
|
||||
<DModal
|
||||
@title={{i18n "explorer.help.modal_title"}}
|
||||
@closeModal={{@closeModal}}
|
||||
>
|
||||
<:body>
|
||||
{{htmlSafe (i18n "explorer.help.auto_resolution")}}
|
||||
{{htmlSafe (i18n "explorer.help.custom_params")}}
|
||||
{{htmlSafe (i18n "explorer.help.default_values")}}
|
||||
{{htmlSafe (i18n "explorer.help.data_types")}}
|
||||
</:body>
|
||||
</DModal>
|
||||
</template>;
|
||||
|
||||
export default QueryHelp;
|
|
@ -0,0 +1,453 @@
|
|||
import Component from "@glimmer/component";
|
||||
import EmberObject, { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { dasherize } from "@ember/string";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import Form from "discourse/components/form";
|
||||
import Category from "discourse/models/category";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import BooleanThree from "./param-input/boolean-three";
|
||||
import CategoryIdInput from "./param-input/category-id-input";
|
||||
import GroupInput from "./param-input/group-input";
|
||||
import UserIdInput from "./param-input/user-id-input";
|
||||
import UserListInput from "./param-input/user-list-input";
|
||||
|
||||
export class ParamValidationError extends Error {}
|
||||
|
||||
const layoutMap = {
|
||||
int: "int",
|
||||
bigint: "string",
|
||||
boolean: "boolean",
|
||||
string: "string",
|
||||
time: "time",
|
||||
date: "date",
|
||||
datetime: "datetime",
|
||||
double: "string",
|
||||
user_id: "user_id",
|
||||
post_id: "string",
|
||||
topic_id: "generic",
|
||||
category_id: "category_id",
|
||||
group_id: "group_list",
|
||||
badge_id: "generic",
|
||||
int_list: "generic",
|
||||
string_list: "generic",
|
||||
user_list: "user_list",
|
||||
group_list: "group_list",
|
||||
};
|
||||
|
||||
export const ERRORS = {
|
||||
REQUIRED: i18n("form_kit.errors.required"),
|
||||
NOT_AN_INTEGER: i18n("form_kit.errors.not_an_integer"),
|
||||
NOT_A_NUMBER: i18n("form_kit.errors.not_a_number"),
|
||||
OVERFLOW_HIGH: i18n("form_kit.errors.too_high", { count: 2147484647 }),
|
||||
OVERFLOW_LOW: i18n("form_kit.errors.too_low", { count: -2147484648 }),
|
||||
INVALID: i18n("explorer.form.errors.invalid"),
|
||||
NO_SUCH_CATEGORY: i18n("explorer.form.errors.no_such_category"),
|
||||
NO_SUCH_GROUP: i18n("explorer.form.errors.no_such_group"),
|
||||
INVALID_DATE: (date) => i18n("explorer.form.errors.invalid_date", { date }),
|
||||
INVALID_TIME: (time) => i18n("explorer.form.errors.invalid_time", { time }),
|
||||
};
|
||||
|
||||
function digitalizeCategoryId(value) {
|
||||
value = String(value || "");
|
||||
const isPositiveInt = /^\d+$/.test(value);
|
||||
if (!isPositiveInt && value.trim()) {
|
||||
return Category.asyncFindBySlugPath(dasherize(value))
|
||||
.then((res) => res.id)
|
||||
.catch((err) => {
|
||||
if (err.jqXHR?.status === 404) {
|
||||
throw new ParamValidationError(
|
||||
`${ERRORS.NO_SUCH_CATEGORY}: ${value}`
|
||||
);
|
||||
} else {
|
||||
throw new Error(err.errorThrow || err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function serializeValue(type, value) {
|
||||
switch (type) {
|
||||
case "string":
|
||||
case "int":
|
||||
return value != null ? String(value) : "";
|
||||
case "boolean":
|
||||
return String(value);
|
||||
case "group_list":
|
||||
case "user_list":
|
||||
return value?.join(",");
|
||||
case "group_id":
|
||||
return value[0];
|
||||
case "datetime":
|
||||
return value?.replaceAll("T", " ");
|
||||
default:
|
||||
return value?.toString();
|
||||
}
|
||||
}
|
||||
|
||||
function validationOf(info) {
|
||||
switch (layoutMap[info.type]) {
|
||||
case "boolean":
|
||||
return info.nullable ? "required" : "";
|
||||
case "string":
|
||||
case "string_list":
|
||||
case "generic":
|
||||
return info.nullable ? "" : "required:trim";
|
||||
default:
|
||||
return info.nullable ? "" : "required";
|
||||
}
|
||||
}
|
||||
|
||||
const components = {
|
||||
int: <template>
|
||||
<@field.Input @type="number" name={{@info.identifier}} />
|
||||
</template>,
|
||||
boolean: <template><@field.Checkbox name={{@info.identifier}} /></template>,
|
||||
boolean_three: BooleanThree,
|
||||
category_id: CategoryIdInput, // TODO
|
||||
user_id: UserIdInput,
|
||||
user_list: UserListInput,
|
||||
group_list: GroupInput,
|
||||
date: <template>
|
||||
<@field.Input @type="date" name={{@info.identifier}} />
|
||||
</template>,
|
||||
time: <template>
|
||||
<@field.Input @type="time" name={{@info.identifier}} />
|
||||
</template>,
|
||||
datetime: <template>
|
||||
<@field.Input @type="datetime-local" name={{@info.identifier}} />
|
||||
</template>,
|
||||
default: <template><@field.Input name={{@info.identifier}} /></template>,
|
||||
};
|
||||
|
||||
function componentOf(info) {
|
||||
let type = layoutMap[info.type] || "generic";
|
||||
if (info.nullable && type === "boolean") {
|
||||
type = "boolean_three";
|
||||
}
|
||||
return components[type] || components.default;
|
||||
}
|
||||
|
||||
export default class ParamInputForm extends Component {
|
||||
@service site;
|
||||
|
||||
data = {};
|
||||
paramInfo = [];
|
||||
infoOf = {};
|
||||
form = null;
|
||||
|
||||
promiseNormalizations = [];
|
||||
formLoaded = new Promise((res) => {
|
||||
this.__form_load_callback = res;
|
||||
});
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.initializeParams();
|
||||
|
||||
this.args.onRegisterApi?.({
|
||||
submit: this.submit,
|
||||
allNormalized: Promise.allSettled(this.promiseNormalizations),
|
||||
});
|
||||
}
|
||||
|
||||
initializeParams() {
|
||||
this.args.paramInfo.forEach((info) => {
|
||||
const identifier = info.identifier;
|
||||
const pinfo = this.createParamInfo(info);
|
||||
|
||||
this.paramInfo.push(pinfo);
|
||||
this.infoOf[identifier] = info;
|
||||
|
||||
const normalized = this.getNormalizedValue(info);
|
||||
|
||||
if (normalized instanceof Promise) {
|
||||
this.handlePromiseNormalization(normalized, pinfo);
|
||||
} else {
|
||||
this.data[identifier] = normalized;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createParamInfo(info) {
|
||||
return EmberObject.create({
|
||||
...info,
|
||||
validation: validationOf(info),
|
||||
validate: this.validatorOf(info),
|
||||
component: componentOf(info),
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async addError(identifier, message) {
|
||||
await this.formLoaded;
|
||||
this.form.addError(identifier, {
|
||||
title: identifier,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
normalizeValue(info, value) {
|
||||
switch (info.type) {
|
||||
case "category_id":
|
||||
return digitalizeCategoryId(value);
|
||||
case "boolean":
|
||||
if (value == null || value === "#null") {
|
||||
return info.nullable ? "#null" : false;
|
||||
}
|
||||
return value === "true";
|
||||
case "group_id":
|
||||
case "group_list":
|
||||
const normalized = this.normalizeGroups(value);
|
||||
if (normalized.errorMsg) {
|
||||
this.addError(info.identifier, normalized.errorMsg);
|
||||
}
|
||||
return info.type === "group_id"
|
||||
? normalized.value.slice(0, 1)
|
||||
: normalized.value;
|
||||
case "user_list":
|
||||
if (Array.isArray(value)) {
|
||||
return value || null;
|
||||
}
|
||||
return value?.split(",") || null;
|
||||
case "user_id":
|
||||
if (Array.isArray(value)) {
|
||||
return value[0];
|
||||
}
|
||||
return value;
|
||||
case "date":
|
||||
try {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return moment(value).format("YYYY-MM-DD");
|
||||
} catch {
|
||||
this.addError(info.identifier, ERRORS.INVALID_DATE(String(value)));
|
||||
return null;
|
||||
}
|
||||
case "time":
|
||||
try {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return moment(new Date(`1970/01/01 ${value}`).toISOString()).format(
|
||||
"HH:mm"
|
||||
);
|
||||
} catch {
|
||||
this.addError(info.identifier, ERRORS.INVALID_TIME(String(value)));
|
||||
return null;
|
||||
}
|
||||
case "datetime":
|
||||
try {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return moment(new Date(value).toISOString()).format(
|
||||
"YYYY-MM-DD HH:mm"
|
||||
);
|
||||
} catch {
|
||||
this.addError(info.identifier, ERRORS.INVALID_TIME(String(value)));
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
getNormalizedValue(info) {
|
||||
const initialValues = this.args.initialValues;
|
||||
const identifier = info.identifier;
|
||||
return this.normalizeValue(
|
||||
info,
|
||||
initialValues && identifier in initialValues
|
||||
? initialValues[identifier]
|
||||
: info.default
|
||||
);
|
||||
}
|
||||
|
||||
handlePromiseNormalization(promise, pinfo) {
|
||||
this.promiseNormalizations.push(promise);
|
||||
pinfo.set("loading", true);
|
||||
this.data[pinfo.identifier] = null;
|
||||
|
||||
promise
|
||||
.then((res) => this.form.set(pinfo.identifier, res))
|
||||
.catch((err) => this.addError(pinfo.identifier, err.message))
|
||||
.finally(() => pinfo.set("loading", false));
|
||||
}
|
||||
|
||||
@action
|
||||
normalizeGroups(values) {
|
||||
values ||= [];
|
||||
if (typeof values === "string") {
|
||||
values = values.split(",");
|
||||
}
|
||||
|
||||
const GroupNames = new Set(this.site.get("groups").map((g) => g.name));
|
||||
const GroupNameOf = Object.fromEntries(
|
||||
this.site.get("groups").map((g) => [g.id, g.name])
|
||||
);
|
||||
|
||||
const valid_groups = [];
|
||||
const invalid_groups = [];
|
||||
|
||||
for (const val of values) {
|
||||
if (GroupNames.has(val)) {
|
||||
valid_groups.push(val);
|
||||
} else if (GroupNameOf[Number(val)]) {
|
||||
valid_groups.push(GroupNameOf[Number(val)]);
|
||||
} else {
|
||||
invalid_groups.push(String(val));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
value: valid_groups,
|
||||
errorMsg:
|
||||
invalid_groups.length !== 0
|
||||
? `${ERRORS.NO_SUCH_GROUP}: ${invalid_groups.join(", ")}`
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
getErrorFn(info) {
|
||||
const isPositiveInt = (value) => /^\d+$/.test(value);
|
||||
const VALIDATORS = {
|
||||
int: (value) => {
|
||||
if (value >= 2147483648) {
|
||||
return ERRORS.OVERFLOW_HIGH;
|
||||
}
|
||||
if (value <= -2147483649) {
|
||||
return ERRORS.OVERFLOW_LOW;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
bigint: (value) => {
|
||||
if (isNaN(parseInt(value, 10))) {
|
||||
return ERRORS.NOT_A_NUMBER;
|
||||
}
|
||||
return /^-?\d+$/.test(value) ? null : ERRORS.NOT_AN_INTEGER;
|
||||
},
|
||||
boolean: (value) => {
|
||||
return /^Y|N|#null|true|false/.test(String(value))
|
||||
? null
|
||||
: ERRORS.INVALID;
|
||||
},
|
||||
double: (value) => {
|
||||
if (isNaN(parseFloat(value))) {
|
||||
if (/^(-?)Inf(inity)?$/i.test(value) || /^(-?)NaN$/i.test(value)) {
|
||||
return null;
|
||||
}
|
||||
return ERRORS.NOT_A_NUMBER;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
int_list: (value) => {
|
||||
return value.split(",").every((i) => /^(-?\d+|null)$/.test(i.trim()))
|
||||
? null
|
||||
: ERRORS.INVALID;
|
||||
},
|
||||
post_id: (value) => {
|
||||
return isPositiveInt(value) ||
|
||||
/\d+\/\d+(\?u=.*)?$/.test(value) ||
|
||||
/\/t\/[^/]+\/(\d+)(\?u=.*)?/.test(value)
|
||||
? null
|
||||
: ERRORS.INVALID;
|
||||
},
|
||||
topic_id: (value) => {
|
||||
return isPositiveInt(value) || /\/t\/[^/]+\/(\d+)/.test(value)
|
||||
? null
|
||||
: ERRORS.INVALID;
|
||||
},
|
||||
category_id: (value) => {
|
||||
return this.site.categoriesById.get(Number(value))
|
||||
? null
|
||||
: ERRORS.NO_SUCH_CATEGORY;
|
||||
},
|
||||
group_list: (value) => {
|
||||
return this.normalizeGroups(value).errorMsg;
|
||||
},
|
||||
group_id: (value) => {
|
||||
return this.normalizeGroups(value).errorMsg;
|
||||
},
|
||||
};
|
||||
return VALIDATORS[info.type] ?? (() => null);
|
||||
}
|
||||
|
||||
validatorOf(info) {
|
||||
const getError = this.getErrorFn(info);
|
||||
return (name, value, { addError }) => {
|
||||
// skip require validation for we have used them in @validation
|
||||
if (isEmpty(value) || value == null) {
|
||||
return;
|
||||
}
|
||||
const message = getError(value);
|
||||
if (message != null) {
|
||||
addError(name, { title: info.identifier, message });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@action
|
||||
async submit() {
|
||||
if (this.form == null) {
|
||||
throw "No form";
|
||||
}
|
||||
this.serializedData = null;
|
||||
await this.form.submit();
|
||||
if (this.serializedData == null) {
|
||||
throw new ParamValidationError("validation_failed");
|
||||
} else {
|
||||
return this.serializedData;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
onRegisterApi(form) {
|
||||
this.__form_load_callback();
|
||||
this.form = form;
|
||||
}
|
||||
|
||||
@action
|
||||
onSubmit(data) {
|
||||
const serializedData = {};
|
||||
for (const [id, val] of Object.entries(data)) {
|
||||
serializedData[id] =
|
||||
serializeValue(this.infoOf[id].type, val) ?? undefined;
|
||||
}
|
||||
this.serializedData = serializedData;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="query-params">
|
||||
<Form
|
||||
@data={{this.data}}
|
||||
@onRegisterApi={{this.onRegisterApi}}
|
||||
@onSubmit={{this.onSubmit}}
|
||||
class="params-form"
|
||||
as |form|
|
||||
>
|
||||
{{#each this.paramInfo as |info|}}
|
||||
<div class="param">
|
||||
<form.Field
|
||||
@name={{info.identifier}}
|
||||
@title={{info.identifier}}
|
||||
@validation={{info.validation}}
|
||||
@validate={{info.validate}}
|
||||
as |field|
|
||||
>
|
||||
<info.component @field={{field}} @info={{info}} />
|
||||
<ConditionalLoadingSpinner
|
||||
@condition={{info.loading}}
|
||||
@size="small"
|
||||
/>
|
||||
</form.Field>
|
||||
</div>
|
||||
{{/each}}
|
||||
</Form>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { i18n } from "discourse-i18n";
|
||||
|
||||
const BooleanThree = <template>
|
||||
<@field.Select name={{@info.identifier}} as |select|>
|
||||
<select.Option @value="Y">
|
||||
{{i18n "explorer.types.bool.true"}}
|
||||
</select.Option>
|
||||
<select.Option @value="N">
|
||||
{{i18n "explorer.types.bool.false"}}
|
||||
</select.Option>
|
||||
<select.Option @value="#null">
|
||||
{{i18n "explorer.types.bool.null_"}}
|
||||
</select.Option>
|
||||
</@field.Select>
|
||||
</template>;
|
||||
|
||||
export default BooleanThree;
|
|
@ -0,0 +1,28 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { hash } from "@ember/helper";
|
||||
import CategoryChooser from "select-kit/components/category-chooser";
|
||||
|
||||
export default class CategoryIdInput extends Component {
|
||||
// CategoryChooser will try to modify the value of value,
|
||||
// triggering a setting-on-hash error. So we have to do the dirty work.
|
||||
get data() {
|
||||
return {
|
||||
value: this.args.field.value,
|
||||
};
|
||||
}
|
||||
|
||||
<template>
|
||||
<@field.Custom id={{@field.id}}>
|
||||
<CategoryChooser
|
||||
@value={{this.data.value}}
|
||||
@onChange={{@field.set}}
|
||||
@options={{hash
|
||||
allowUncategorized=null
|
||||
autoInsertNoneItem=true
|
||||
none=true
|
||||
}}
|
||||
name={{@info.identifier}}
|
||||
/>
|
||||
</@field.Custom>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { service } from "@ember/service";
|
||||
import GroupChooser from "select-kit/components/group-chooser";
|
||||
|
||||
export default class GroupInput extends Component {
|
||||
@service site;
|
||||
|
||||
get allGroups() {
|
||||
return this.site.get("groups");
|
||||
}
|
||||
|
||||
get groupChooserOption() {
|
||||
return this.args.info.type === "group_id"
|
||||
? {
|
||||
maximum: 1,
|
||||
}
|
||||
: {};
|
||||
}
|
||||
|
||||
<template>
|
||||
<@field.Custom id={{@field.id}}>
|
||||
<GroupChooser
|
||||
@content={{this.allGroups}}
|
||||
@value={{@field.value}}
|
||||
@labelProperty="name"
|
||||
@valueProperty="name"
|
||||
@onChange={{@field.set}}
|
||||
@options={{this.groupChooserOption}}
|
||||
name={{@info.identifier}}
|
||||
/>
|
||||
</@field.Custom>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { hash } from "@ember/helper";
|
||||
import EmailGroupUserChooser from "select-kit/components/email-group-user-chooser";
|
||||
|
||||
const UserIdInput = <template>
|
||||
<@field.Custom id={{@field.id}}>
|
||||
<EmailGroupUserChooser
|
||||
@value={{@field.value}}
|
||||
@options={{hash maximum=1}}
|
||||
@onChange={{@field.set}}
|
||||
name={{@info.identifier}}
|
||||
/>
|
||||
</@field.Custom>
|
||||
</template>;
|
||||
|
||||
export default UserIdInput;
|
|
@ -0,0 +1,13 @@
|
|||
import EmailGroupUserChooser from "select-kit/components/email-group-user-chooser";
|
||||
|
||||
const UserListInput = <template>
|
||||
<@field.Custom id={{@field.id}}>
|
||||
<EmailGroupUserChooser
|
||||
@value={{@field.value}}
|
||||
@onChange={{@field.set}}
|
||||
name={{@info.identifier}}
|
||||
/>
|
||||
</@field.Custom>
|
||||
</template>;
|
||||
|
||||
export default UserListInput;
|
|
@ -0,0 +1,419 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import { service } from "@ember/service";
|
||||
import { capitalize } from "@ember/string";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import getURL from "discourse/lib/get-url";
|
||||
import Badge from "discourse/models/badge";
|
||||
import Category from "discourse/models/category";
|
||||
import I18n, { i18n } from "discourse-i18n";
|
||||
import DataExplorerBarChart from "./data-explorer-bar-chart";
|
||||
import QueryRowContent from "./query-row-content";
|
||||
import BadgeViewComponent from "./result-types/badge";
|
||||
import CategoryViewComponent from "./result-types/category";
|
||||
import GroupViewComponent from "./result-types/group";
|
||||
import HtmlViewComponent from "./result-types/html";
|
||||
import JsonViewComponent from "./result-types/json";
|
||||
import PostViewComponent from "./result-types/post";
|
||||
import ReltimeViewComponent from "./result-types/reltime";
|
||||
import TagGroupViewComponent from "./result-types/tag-group";
|
||||
import TextViewComponent from "./result-types/text";
|
||||
import TopicViewComponent from "./result-types/topic";
|
||||
import UrlViewComponent from "./result-types/url";
|
||||
import UserViewComponent from "./result-types/user";
|
||||
|
||||
const VIEW_COMPONENTS = {
|
||||
topic: TopicViewComponent,
|
||||
text: TextViewComponent,
|
||||
post: PostViewComponent,
|
||||
reltime: ReltimeViewComponent,
|
||||
badge: BadgeViewComponent,
|
||||
url: UrlViewComponent,
|
||||
user: UserViewComponent,
|
||||
group: GroupViewComponent,
|
||||
html: HtmlViewComponent,
|
||||
json: JsonViewComponent,
|
||||
category: CategoryViewComponent,
|
||||
tag_group: TagGroupViewComponent,
|
||||
};
|
||||
|
||||
export default class QueryResult extends Component {
|
||||
@service site;
|
||||
|
||||
@tracked chartDisplayed = false;
|
||||
|
||||
get colRender() {
|
||||
return this.args.content.colrender || {};
|
||||
}
|
||||
|
||||
get rows() {
|
||||
return this.args.content.rows;
|
||||
}
|
||||
|
||||
get columns() {
|
||||
return this.args.content.columns;
|
||||
}
|
||||
|
||||
get params() {
|
||||
return this.args.content.params;
|
||||
}
|
||||
|
||||
get explainText() {
|
||||
return this.args.content.explain;
|
||||
}
|
||||
|
||||
get chartDatasetName() {
|
||||
return this.columnNames[1];
|
||||
}
|
||||
|
||||
get columnNames() {
|
||||
if (!this.columns) {
|
||||
return [];
|
||||
}
|
||||
return this.columns.map((colName) => {
|
||||
if (colName.endsWith("_id")) {
|
||||
return colName.slice(0, -3);
|
||||
}
|
||||
const dIdx = colName.indexOf("$");
|
||||
if (dIdx >= 0) {
|
||||
return colName.substring(dIdx + 1);
|
||||
}
|
||||
return colName;
|
||||
});
|
||||
}
|
||||
|
||||
get columnComponents() {
|
||||
if (!this.columns) {
|
||||
return [];
|
||||
}
|
||||
return this.columns.map((_, idx) => {
|
||||
let type = "text";
|
||||
if (this.colRender[idx]) {
|
||||
type = this.colRender[idx];
|
||||
}
|
||||
return { name: type, component: VIEW_COMPONENTS[type] };
|
||||
});
|
||||
}
|
||||
|
||||
get chartValues() {
|
||||
// return an array with the second value of this.row
|
||||
return this.rows.mapBy(1);
|
||||
}
|
||||
|
||||
get colCount() {
|
||||
return this.columns.length;
|
||||
}
|
||||
|
||||
get resultCount() {
|
||||
const count = this.args.content.result_count;
|
||||
if (count === this.args.content.default_limit) {
|
||||
return i18n("explorer.max_result_count", { count });
|
||||
} else {
|
||||
return i18n("explorer.result_count", { count });
|
||||
}
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return i18n("explorer.run_time", {
|
||||
value: I18n.toNumber(this.args.content.duration, { precision: 1 }),
|
||||
});
|
||||
}
|
||||
|
||||
get parameterAry() {
|
||||
let arr = [];
|
||||
for (let key in this.params) {
|
||||
if (this.params.hasOwnProperty(key)) {
|
||||
arr.push({ key, value: this.params[key] });
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
get transformedUserTable() {
|
||||
return transformedRelTable(this.args.content.relations.user);
|
||||
}
|
||||
|
||||
get transformedBadgeTable() {
|
||||
return transformedRelTable(this.args.content.relations.badge, Badge);
|
||||
}
|
||||
|
||||
get transformedPostTable() {
|
||||
return transformedRelTable(this.args.content.relations.post);
|
||||
}
|
||||
|
||||
get transformedTopicTable() {
|
||||
return transformedRelTable(this.args.content.relations.topic);
|
||||
}
|
||||
|
||||
get transformedTagGroupTable() {
|
||||
return transformedRelTable(this.args.content.relations.tag_group);
|
||||
}
|
||||
|
||||
get transformedGroupTable() {
|
||||
return transformedRelTable(this.site.groups);
|
||||
}
|
||||
|
||||
get canShowChart() {
|
||||
const hasTwoColumns = this.colCount === 2;
|
||||
const secondColumnContainsNumber =
|
||||
this.resultCount[0] > 0 && typeof this.rows[0][1] === "number";
|
||||
const secondColumnContainsId = this.colRender[1];
|
||||
|
||||
return (
|
||||
hasTwoColumns && secondColumnContainsNumber && !secondColumnContainsId
|
||||
);
|
||||
}
|
||||
|
||||
get chartLabels() {
|
||||
const labelSelectors = {
|
||||
user: (user) => user.username,
|
||||
badge: (badge) => badge.name,
|
||||
topic: (topic) => topic.title,
|
||||
group: (group) => group.name,
|
||||
category: (category) => category.name,
|
||||
};
|
||||
|
||||
const relationName = this.colRender[0];
|
||||
if (relationName) {
|
||||
const lookupFunc = this[`lookup${capitalize(relationName)}`];
|
||||
const labelSelector = labelSelectors[relationName];
|
||||
|
||||
if (lookupFunc && labelSelector) {
|
||||
return this.rows.map((r) => {
|
||||
const relation = lookupFunc.call(this, r[0]);
|
||||
const label = labelSelector(relation);
|
||||
return this._cutChartLabel(label);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return this.rows.map((r) => this._cutChartLabel(r[0]));
|
||||
}
|
||||
|
||||
lookupUser(id) {
|
||||
return this.transformedUserTable[id];
|
||||
}
|
||||
|
||||
lookupBadge(id) {
|
||||
return this.transformedBadgeTable[id];
|
||||
}
|
||||
|
||||
lookupPost(id) {
|
||||
return this.transformedPostTable[id];
|
||||
}
|
||||
|
||||
lookupTopic(id) {
|
||||
return this.transformedTopicTable[id];
|
||||
}
|
||||
|
||||
lookupTagGroup(id) {
|
||||
return this.transformedTagGroupTable[id];
|
||||
}
|
||||
|
||||
lookupGroup(id) {
|
||||
return this.transformedGroupTable[id];
|
||||
}
|
||||
|
||||
lookupCategory(id) {
|
||||
return Category.findById(id);
|
||||
}
|
||||
|
||||
_cutChartLabel(label) {
|
||||
const labelString = label.toString();
|
||||
if (labelString.length > 25) {
|
||||
return `${labelString.substring(0, 25)}...`;
|
||||
} else {
|
||||
return labelString;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
downloadResultJson() {
|
||||
this._downloadResult("json");
|
||||
}
|
||||
|
||||
@action
|
||||
downloadResultCsv() {
|
||||
this._downloadResult("csv");
|
||||
}
|
||||
|
||||
@action
|
||||
showChart() {
|
||||
this.chartDisplayed = true;
|
||||
}
|
||||
|
||||
@action
|
||||
hideChart() {
|
||||
this.chartDisplayed = false;
|
||||
}
|
||||
|
||||
_download_url() {
|
||||
return this.args.group
|
||||
? `/g/${this.args.group.name}/reports/`
|
||||
: "/admin/plugins/explorer/queries/";
|
||||
}
|
||||
|
||||
_downloadResult(format) {
|
||||
// Create a frame to submit the form in (?)
|
||||
// to avoid leaving an about:blank behind
|
||||
let windowName = randomIdShort();
|
||||
const newWindowContents =
|
||||
"<style>body{font-size:36px;display:flex;justify-content:center;align-items:center;}</style><body>Click anywhere to close this window once the download finishes.<script>window.onclick=function(){window.close()};</script>";
|
||||
|
||||
window.open("data:text/html;base64," + btoa(newWindowContents), windowName);
|
||||
|
||||
let form = document.createElement("form");
|
||||
form.setAttribute("id", "query-download-result");
|
||||
form.setAttribute("method", "post");
|
||||
form.setAttribute(
|
||||
"action",
|
||||
getURL(
|
||||
this._download_url() +
|
||||
this.args.query.id +
|
||||
"/run." +
|
||||
format +
|
||||
"?download=1"
|
||||
)
|
||||
);
|
||||
form.setAttribute("target", windowName);
|
||||
form.setAttribute("style", "display:none;");
|
||||
|
||||
function addInput(name, value) {
|
||||
let field;
|
||||
field = document.createElement("input");
|
||||
field.setAttribute("name", name);
|
||||
field.setAttribute("value", value);
|
||||
form.appendChild(field);
|
||||
}
|
||||
|
||||
addInput("params", JSON.stringify(this.params));
|
||||
addInput("explain", this.explainText);
|
||||
addInput("limit", "1000000");
|
||||
|
||||
ajax("/session/csrf.json").then((csrf) => {
|
||||
addInput("authenticity_token", csrf.csrf);
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
schedule("afterRender", () => document.body.removeChild(form));
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<article>
|
||||
<header class="result-header">
|
||||
<div class="result-info">
|
||||
<DButton
|
||||
@action={{this.downloadResultJson}}
|
||||
@icon="download"
|
||||
@label="explorer.download_json"
|
||||
/>
|
||||
|
||||
<DButton
|
||||
@action={{this.downloadResultCsv}}
|
||||
@icon="download"
|
||||
@label="explorer.download_csv"
|
||||
/>
|
||||
|
||||
{{#if this.canShowChart}}
|
||||
{{#if this.chartDisplayed}}
|
||||
<DButton
|
||||
@action={{this.hideChart}}
|
||||
@icon="table"
|
||||
@label="explorer.show_table"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@action={{this.showChart}}
|
||||
@icon="chart-bar"
|
||||
@label="explorer.show_graph"
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="result-about">
|
||||
{{this.resultCount}}
|
||||
{{this.duration}}
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
{{~#if this.explainText}}
|
||||
<pre class="result-explain">
|
||||
<code>
|
||||
{{~this.explainText}}
|
||||
</code>
|
||||
</pre>
|
||||
{{~/if}}
|
||||
|
||||
<br />
|
||||
</header>
|
||||
|
||||
<section>
|
||||
{{#if this.chartDisplayed}}
|
||||
<DataExplorerBarChart
|
||||
@labels={{this.chartLabels}}
|
||||
@values={{this.chartValues}}
|
||||
@datasetName={{this.chartDatasetName}}
|
||||
/>
|
||||
{{else}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="headers">
|
||||
{{#each this.columnNames as |col|}}
|
||||
<th>{{col}}</th>
|
||||
{{/each}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each this.rows as |row|}}
|
||||
<QueryRowContent
|
||||
@row={{row}}
|
||||
@columnComponents={{this.columnComponents}}
|
||||
@lookupUser={{this.lookupUser}}
|
||||
@lookupBadge={{this.lookupBadge}}
|
||||
@lookupPost={{this.lookupPost}}
|
||||
@lookupTopic={{this.lookupTopic}}
|
||||
@lookupTagGroup={{this.lookupTagGroup}}
|
||||
@lookupGroup={{this.lookupGroup}}
|
||||
@lookupCategory={{this.lookupCategory}}
|
||||
@transformedPostTable={{this.transformedPostTable}}
|
||||
@transformedBadgeTable={{this.transformedBadgeTable}}
|
||||
@transformedUserTable={{this.transformedUserTable}}
|
||||
@transformedTagGroupTable={{this.transformedTagGroupTable}}
|
||||
@transformedGroupTable={{this.transformedGroupTable}}
|
||||
@transformedTopicTable={{this.transformedTopicTable}}
|
||||
@site={{this.site}}
|
||||
/>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/if}}
|
||||
</section>
|
||||
</article>
|
||||
</template>
|
||||
}
|
||||
|
||||
function randomIdShort() {
|
||||
return "xxxxxxxx".replace(/[xy]/g, () => {
|
||||
/*eslint-disable*/
|
||||
return ((Math.random() * 16) | 0).toString(16);
|
||||
/*eslint-enable*/
|
||||
});
|
||||
}
|
||||
|
||||
function transformedRelTable(table, modelClass) {
|
||||
const result = {};
|
||||
table?.forEach((item) => {
|
||||
if (modelClass) {
|
||||
result[item.id] = modelClass.create(item);
|
||||
} else {
|
||||
result[item.id] = item;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import QueryResult from "./query-result";
|
||||
|
||||
const QueryResultsWrapper = <template>
|
||||
{{#if @results}}
|
||||
<div class="query-results">
|
||||
{{#if @showResults}}
|
||||
<QueryResult @query={{@query}} @content={{@results}} />
|
||||
{{else}}
|
||||
{{#each @results.errors as |err|}}
|
||||
<pre class="query-error"><code>{{~err}}</code></pre>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>;
|
||||
|
||||
export default QueryResultsWrapper;
|
|
@ -0,0 +1,82 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { cached } from "@glimmer/tracking";
|
||||
import { classify } from "@ember/string";
|
||||
import getURL from "discourse/lib/get-url";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import TextViewComponent from "./result-types/text";
|
||||
|
||||
export default class QueryRowContent extends Component {
|
||||
@cached
|
||||
get results() {
|
||||
return this.args.columnComponents.map((componentDefinition, idx) => {
|
||||
const value = this.args.row[idx],
|
||||
id = parseInt(value, 10);
|
||||
|
||||
const ctx = {
|
||||
value,
|
||||
id,
|
||||
baseuri: getURL(""),
|
||||
};
|
||||
|
||||
if (this.args.row[idx] === null) {
|
||||
return {
|
||||
component: TextViewComponent,
|
||||
textValue: "NULL",
|
||||
};
|
||||
} else if (componentDefinition.name === "text") {
|
||||
return {
|
||||
component: TextViewComponent,
|
||||
textValue: escapeExpression(this.args.row[idx].toString()),
|
||||
};
|
||||
}
|
||||
|
||||
const lookupFunc =
|
||||
this.args[`lookup${classify(componentDefinition.name)}`];
|
||||
if (lookupFunc) {
|
||||
ctx[componentDefinition.name] = lookupFunc.call(this.args, id);
|
||||
}
|
||||
|
||||
if (componentDefinition.name === "url") {
|
||||
let [url, name] = guessUrl(value);
|
||||
ctx["href"] = url;
|
||||
ctx["target"] = name;
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
component: componentDefinition.component || TextViewComponent,
|
||||
ctx,
|
||||
};
|
||||
} catch {
|
||||
return "error";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<tr>
|
||||
{{#each this.results as |result|}}
|
||||
<td>
|
||||
<result.component
|
||||
@ctx={{result.ctx}}
|
||||
@params={{result.params}}
|
||||
@textValue={{result.textValue}}
|
||||
/>
|
||||
</td>
|
||||
{{/each}}
|
||||
</tr>
|
||||
</template>
|
||||
}
|
||||
|
||||
function guessUrl(columnValue) {
|
||||
let [dest, name] = [columnValue, columnValue];
|
||||
|
||||
const split = columnValue.split(/,(.+)/);
|
||||
|
||||
if (split.length > 1) {
|
||||
name = split[0];
|
||||
dest = split[1];
|
||||
}
|
||||
|
||||
return [dest, name];
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { convertIconClass, iconHTML } from "discourse/lib/icon-library";
|
||||
|
||||
export default class Badge extends Component {
|
||||
get iconOrImageReplacement() {
|
||||
if (isEmpty(this.args.ctx.badge.icon)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (this.args.ctx.badge.icon.indexOf("fa-") > -1) {
|
||||
const icon = iconHTML(convertIconClass(this.args.ctx.badge.icon));
|
||||
return htmlSafe(icon);
|
||||
} else {
|
||||
return htmlSafe("<img src='" + this.args.ctx.badge.icon + "'>");
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<a
|
||||
href="{{@ctx.baseuri}}/badges/{{@ctx.badge.id}}/{{@ctx.badge.name}}"
|
||||
class="user-badge {{@ctx.badge.badgeTypeClassName}}"
|
||||
title={{@ctx.badge.display_name}}
|
||||
data-badge-name={{@ctx.badge.name}}
|
||||
>
|
||||
{{this.iconOrImageReplacement}}
|
||||
<span class="badge-display-name">{{@ctx.badge.display_name}}</span>
|
||||
</a>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { categoryLinkHTML } from "discourse/helpers/category-link";
|
||||
|
||||
export default class Category extends Component {
|
||||
get categoryBadgeReplacement() {
|
||||
return categoryLinkHTML(this.args.ctx.category, {
|
||||
allowUncategorized: true,
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if @ctx.category}}
|
||||
{{this.categoryBadgeReplacement}}
|
||||
{{else}}
|
||||
<a href="{{@ctx.baseuri}}/t/{{@ctx.id}}">{{@ctx.id}}</a>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
const Group = <template>
|
||||
{{#if @ctx.group}}
|
||||
<a
|
||||
href="{{@ctx.baseuri}}/groups/{{@ctx.group.name}}"
|
||||
>{{@ctx.group.name}}</a>
|
||||
{{else}}
|
||||
{{@ctx.id}}
|
||||
{{/if}}
|
||||
</template>;
|
||||
|
||||
export default Group;
|
|
@ -0,0 +1,5 @@
|
|||
import htmlSafe from "discourse/helpers/html-safe";
|
||||
|
||||
const Html = <template>{{htmlSafe @ctx.value}}</template>;
|
||||
|
||||
export default Html;
|
|
@ -0,0 +1,45 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { cached } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import FullscreenCodeModal from "discourse/components/modal/fullscreen-code";
|
||||
|
||||
export default class Json extends Component {
|
||||
@service dialog;
|
||||
@service modal;
|
||||
|
||||
@cached
|
||||
get parsedJson() {
|
||||
try {
|
||||
return JSON.parse(this.args.ctx.value);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
viewJson() {
|
||||
this.modal.show(FullscreenCodeModal, {
|
||||
model: {
|
||||
code: this.parsedJson
|
||||
? JSON.stringify(this.parsedJson, null, 2)
|
||||
: this.args.ctx.value,
|
||||
codeClasses: "",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="result-json">
|
||||
<div class="result-json-value">{{@ctx.value}}</div>
|
||||
<DButton
|
||||
class="result-json-button"
|
||||
{{! template-lint-disable no-action }}
|
||||
@action={{action "viewJson"}}
|
||||
@icon="ellipsis"
|
||||
@title="explorer.view_json"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import avatar from "discourse/helpers/avatar";
|
||||
import htmlSafe from "discourse/helpers/html-safe";
|
||||
|
||||
const Post = <template>
|
||||
{{#if @ctx.post}}
|
||||
<aside
|
||||
class="quote"
|
||||
data-post={{@ctx.post.post_number}}
|
||||
data-topic={{@ctx.post.topic_id}}
|
||||
>
|
||||
<div class="title">
|
||||
<div class="quote-controls">
|
||||
{{! template-lint-disable no-invalid-link-text }}
|
||||
<a
|
||||
href="/t/via-quote/{{@ctx.post.topic_id}}/{{@ctx.post.post_number}}"
|
||||
title="go to the quoted post"
|
||||
class="quote-other-topic"
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a
|
||||
class="result-post-link"
|
||||
href="/t/{{@ctx.post.topic_id}}/{{@ctx.post.post_number}}"
|
||||
>
|
||||
{{avatar @ctx.post imageSize="tiny"}}{{@ctx.post.username}}:
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<blockquote>
|
||||
<p>
|
||||
{{htmlSafe @ctx.post.excerpt}}
|
||||
</p>
|
||||
</blockquote>
|
||||
</aside>
|
||||
{{else}}
|
||||
{{@ctx.id}}
|
||||
{{/if}}
|
||||
</template>;
|
||||
|
||||
export default Post;
|
|
@ -0,0 +1,13 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { autoUpdatingRelativeAge } from "discourse/lib/formatter";
|
||||
|
||||
export default class Reltime extends Component {
|
||||
get boundDateReplacement() {
|
||||
return htmlSafe(
|
||||
autoUpdatingRelativeAge(new Date(this.args.ctx.value), { title: true })
|
||||
);
|
||||
}
|
||||
|
||||
<template>{{this.boundDateReplacement}}</template>
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
const Group = <template>
|
||||
{{#if @ctx.tag_group}}
|
||||
<a
|
||||
href="{{@ctx.baseuri}}/tag_groups/{{@ctx.id}}"
|
||||
>{{@ctx.tag_group.name}}</a>
|
||||
{{else}}
|
||||
{{@ctx.id}}
|
||||
{{/if}}
|
||||
</template>;
|
||||
|
||||
export default Group;
|
|
@ -0,0 +1,3 @@
|
|||
const Text = <template>{{@textValue}}</template>;
|
||||
|
||||
export default Text;
|
|
@ -0,0 +1,14 @@
|
|||
import htmlSafe from "discourse/helpers/html-safe";
|
||||
|
||||
const Topic = <template>
|
||||
{{#if @ctx.topic}}
|
||||
<a href="{{@ctx.baseuri}}/t/{{@ctx.topic.slug}}/{{@ctx.topic.id}}">
|
||||
{{htmlSafe @ctx.topic.fancy_title}}
|
||||
</a>
|
||||
({{@ctx.topic.posts_count}})
|
||||
{{else}}
|
||||
<a href="{{@ctx.baseuri}}/t/{{@ctx.id}}">{{@ctx.id}}</a>
|
||||
{{/if}}
|
||||
</template>;
|
||||
|
||||
export default Topic;
|
|
@ -0,0 +1,5 @@
|
|||
const Url = <template>
|
||||
<a href={{@ctx.href}}>{{@ctx.target}}</a>
|
||||
</template>;
|
||||
|
||||
export default Url;
|
|
@ -0,0 +1,17 @@
|
|||
import avatar from "discourse/helpers/avatar";
|
||||
|
||||
const User = <template>
|
||||
{{#if @ctx.user}}
|
||||
<a
|
||||
href="{{@ctx.baseuri}}/u/{{@ctx.user.username}}/activity"
|
||||
data-user-card={{@ctx.user.username}}
|
||||
>
|
||||
{{avatar @ctx.user imageSize="tiny"}}
|
||||
{{@ctx.user.username}}
|
||||
</a>
|
||||
{{else}}
|
||||
{{@ctx.id}}
|
||||
{{/if}}
|
||||
</template>;
|
||||
|
||||
export default User;
|
|
@ -0,0 +1,101 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
import getURL from "discourse/lib/get-url";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class ShareReport extends Component {
|
||||
@tracked visible = false;
|
||||
element;
|
||||
|
||||
get link() {
|
||||
return getURL(`/g/${this.args.group}/reports/${this.args.query.id}`);
|
||||
}
|
||||
|
||||
@bind
|
||||
mouseDownHandler(e) {
|
||||
if (!this.element.contains(e.target)) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
keyDownHandler(e) {
|
||||
if (e.keyCode === 27) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
registerListeners(element) {
|
||||
if (!element || this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.element = element;
|
||||
document.addEventListener("mousedown", this.mouseDownHandler);
|
||||
element.addEventListener("keydown", this.keyDownHandler);
|
||||
}
|
||||
|
||||
@action
|
||||
unregisterListeners(element) {
|
||||
this.element = element;
|
||||
document.removeEventListener("mousedown", this.mouseDownHandler);
|
||||
element.removeEventListener("keydown", this.keyDownHandler);
|
||||
}
|
||||
|
||||
@action
|
||||
focusInput(e) {
|
||||
e.select();
|
||||
e.focus();
|
||||
}
|
||||
|
||||
@action
|
||||
open(e) {
|
||||
e.preventDefault();
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
@action
|
||||
close() {
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="share-report">
|
||||
<a href="#" {{on "click" this.open}} class="share-report-button">
|
||||
{{icon "link"}}
|
||||
{{@group}}
|
||||
</a>
|
||||
|
||||
{{#if this.visible}}
|
||||
<div
|
||||
class="popup"
|
||||
{{didInsert this.registerListeners}}
|
||||
{{willDestroy this.unregisterListeners}}
|
||||
>
|
||||
<label>{{i18n "explorer.link"}} {{@group}}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={{this.link}}
|
||||
{{didInsert this.focusInput}}
|
||||
/>
|
||||
|
||||
<DButton
|
||||
@action={{this.close}}
|
||||
@icon="xmark"
|
||||
@aria-label="share.close"
|
||||
@title="share.close"
|
||||
class="btn-flat close"
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import Component from "@ember/component";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { classNames, tagName } from "@ember-decorators/component";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
@tagName("li")
|
||||
@classNames("group-reports-nav-item-outlet", "nav-item")
|
||||
export default class NavItem extends Component {
|
||||
static shouldRender(args) {
|
||||
return args.group.has_visible_data_explorer_queries;
|
||||
}
|
||||
|
||||
<template>
|
||||
<LinkTo @route="group.reports">
|
||||
{{icon "chart-bar"}}{{i18n "group.reports"}}
|
||||
</LinkTo>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import Controller from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { Promise } from "rsvp";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class PluginsExplorerController extends Controller {
|
||||
@service dialog;
|
||||
@service appEvents;
|
||||
@service router;
|
||||
|
||||
@tracked sortByProperty = "last_run_at";
|
||||
@tracked sortDescending = true;
|
||||
@tracked params;
|
||||
@tracked search;
|
||||
@tracked newQueryName;
|
||||
@tracked showCreate;
|
||||
@tracked loading = false;
|
||||
|
||||
queryParams = ["id"];
|
||||
explain = false;
|
||||
acceptedImportFileTypes = ["application/json"];
|
||||
order = null;
|
||||
form = null;
|
||||
|
||||
get sortedQueries() {
|
||||
const sortedQueries = this.model.sortBy(this.sortByProperty);
|
||||
return this.sortDescending ? sortedQueries.reverse() : sortedQueries;
|
||||
}
|
||||
|
||||
get parsedParams() {
|
||||
return this.params ? JSON.parse(this.params) : null;
|
||||
}
|
||||
|
||||
get filteredContent() {
|
||||
const regexp = new RegExp(this.search, "i");
|
||||
return this.sortedQueries.filter(
|
||||
(result) => regexp.test(result.name) || regexp.test(result.description)
|
||||
);
|
||||
}
|
||||
|
||||
get createDisabled() {
|
||||
return (this.newQueryName || "").trim().length === 0;
|
||||
}
|
||||
|
||||
addCreatedRecord(record) {
|
||||
this.model.pushObject(record);
|
||||
this.router.transitionTo(
|
||||
"adminPlugins.explorer.queries.details",
|
||||
record.id
|
||||
);
|
||||
}
|
||||
|
||||
async _importQuery(file) {
|
||||
const json = await this._readFileAsTextAsync(file);
|
||||
const query = this._parseQuery(json);
|
||||
const record = this.store.createRecord("query", query);
|
||||
const response = await record.save();
|
||||
return response.target;
|
||||
}
|
||||
|
||||
_parseQuery(json) {
|
||||
const parsed = JSON.parse(json);
|
||||
const query = parsed.query;
|
||||
if (!query || !query.sql) {
|
||||
throw new TypeError();
|
||||
}
|
||||
query.id = 0; // 0 means no Id yet
|
||||
return query;
|
||||
}
|
||||
|
||||
_readFileAsTextAsync(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
resolve(reader.result);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
dragMove(e) {
|
||||
if (!e.movementY && !e.movementX) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editPane = document.querySelector(".query-editor");
|
||||
const target = editPane.querySelector(".panels-flex");
|
||||
const grippie = editPane.querySelector(".grippie");
|
||||
|
||||
// we need to get the initial height / width of edit pane
|
||||
// before we manipulate the size
|
||||
if (!this.initialPaneWidth && !this.originalPaneHeight) {
|
||||
this.originalPaneWidth = target.clientWidth;
|
||||
this.originalPaneHeight = target.clientHeight;
|
||||
}
|
||||
|
||||
const newHeight = Math.max(
|
||||
this.originalPaneHeight,
|
||||
target.clientHeight + e.movementY
|
||||
);
|
||||
const newWidth = Math.max(
|
||||
this.originalPaneWidth,
|
||||
target.clientWidth + e.movementX
|
||||
);
|
||||
|
||||
target.style.height = newHeight + "px";
|
||||
target.style.width = newWidth + "px";
|
||||
grippie.style.width = newWidth + "px";
|
||||
this.appEvents.trigger("ace:resize");
|
||||
}
|
||||
|
||||
@bind
|
||||
scrollTop() {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
@action
|
||||
async import(files) {
|
||||
try {
|
||||
this.loading = true;
|
||||
const file = files[0];
|
||||
const record = await this._importQuery(file);
|
||||
this.addCreatedRecord(record);
|
||||
} catch (e) {
|
||||
if (e.jqXHR) {
|
||||
popupAjaxError(e);
|
||||
} else if (e instanceof SyntaxError) {
|
||||
this.dialog.alert(i18n("explorer.import.unparseable_json"));
|
||||
} else if (e instanceof TypeError) {
|
||||
this.dialog.alert(i18n("explorer.import.wrong_json"));
|
||||
} else {
|
||||
this.dialog.alert(i18n("errors.desc.unknown"));
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
displayCreate() {
|
||||
this.showCreate = true;
|
||||
}
|
||||
|
||||
@action
|
||||
updateSortProperty(property) {
|
||||
if (this.sortByProperty === property) {
|
||||
this.sortDescending = !this.sortDescending;
|
||||
} else {
|
||||
this.sortByProperty = property;
|
||||
this.sortDescending = true;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async create() {
|
||||
try {
|
||||
const name = this.newQueryName.trim();
|
||||
this.loading = true;
|
||||
this.showCreate = false;
|
||||
const result = await this.store.createRecord("query", { name }).save();
|
||||
this.addCreatedRecord(result.target);
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
updateSearch(value) {
|
||||
this.search = value;
|
||||
}
|
||||
|
||||
@action
|
||||
updateNewQueryName(value) {
|
||||
this.newQueryName = value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,292 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import Controller from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { Promise } from "rsvp";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
import QueryHelp from "discourse/plugins/discourse-data-explorer/discourse/components/modal/query-help";
|
||||
import { ParamValidationError } from "discourse/plugins/discourse-data-explorer/discourse/components/param-input-form";
|
||||
import Query from "discourse/plugins/discourse-data-explorer/discourse/models/query";
|
||||
|
||||
export default class PluginsExplorerController extends Controller {
|
||||
@service modal;
|
||||
@service appEvents;
|
||||
@service router;
|
||||
|
||||
@tracked params;
|
||||
@tracked editingName = false;
|
||||
@tracked editingQuery = false;
|
||||
@tracked loading = false;
|
||||
@tracked showResults = false;
|
||||
@tracked hideSchema = false;
|
||||
@tracked results = this.model.results;
|
||||
@tracked dirty = false;
|
||||
|
||||
queryParams = ["params"];
|
||||
explain = false;
|
||||
order = null;
|
||||
form = null;
|
||||
|
||||
get saveDisabled() {
|
||||
return !this.dirty;
|
||||
}
|
||||
|
||||
get runDisabled() {
|
||||
return this.dirty;
|
||||
}
|
||||
|
||||
get parsedParams() {
|
||||
return this.params ? JSON.parse(this.params) : null;
|
||||
}
|
||||
|
||||
get editDisabled() {
|
||||
return parseInt(this.model.id, 10) < 0 ? true : false;
|
||||
}
|
||||
|
||||
get groupOptions() {
|
||||
return this.groups
|
||||
.filter((g) => g.id !== 0)
|
||||
.map((g) => {
|
||||
return { id: g.id, name: g.name };
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async save() {
|
||||
try {
|
||||
this.loading = true;
|
||||
await this.model.save();
|
||||
|
||||
this.dirty = false;
|
||||
this.editingName = false;
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
saveAndRun() {
|
||||
this.save().then(() => this.run());
|
||||
}
|
||||
|
||||
async _importQuery(file) {
|
||||
const json = await this._readFileAsTextAsync(file);
|
||||
const query = this._parseQuery(json);
|
||||
const record = this.store.createRecord("query", query);
|
||||
const response = await record.save();
|
||||
return response.target;
|
||||
}
|
||||
|
||||
_parseQuery(json) {
|
||||
const parsed = JSON.parse(json);
|
||||
const query = parsed.query;
|
||||
if (!query || !query.sql) {
|
||||
throw new TypeError();
|
||||
}
|
||||
query.id = 0; // 0 means no Id yet
|
||||
return query;
|
||||
}
|
||||
|
||||
_readFileAsTextAsync(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
resolve(reader.result);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
dragMove(e) {
|
||||
if (!e.movementY && !e.movementX) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editPane = document.querySelector(".query-editor");
|
||||
const target = editPane.querySelector(".panels-flex");
|
||||
const grippie = editPane.querySelector(".grippie");
|
||||
|
||||
// we need to get the initial height / width of edit pane
|
||||
// before we manipulate the size
|
||||
if (!this.initialPaneWidth && !this.originalPaneHeight) {
|
||||
this.originalPaneWidth = target.clientWidth;
|
||||
this.originalPaneHeight = target.clientHeight;
|
||||
}
|
||||
|
||||
const newHeight = Math.max(
|
||||
this.originalPaneHeight,
|
||||
target.clientHeight + e.movementY
|
||||
);
|
||||
const newWidth = Math.max(
|
||||
this.originalPaneWidth,
|
||||
target.clientWidth + e.movementX
|
||||
);
|
||||
|
||||
target.style.height = newHeight + "px";
|
||||
target.style.width = newWidth + "px";
|
||||
grippie.style.width = newWidth + "px";
|
||||
this.appEvents.trigger("ace:resize");
|
||||
}
|
||||
|
||||
@bind
|
||||
didStartDrag() {}
|
||||
|
||||
@bind
|
||||
didEndDrag() {}
|
||||
|
||||
@action
|
||||
updateGroupIds(value) {
|
||||
this.dirty = true;
|
||||
this.model.set("group_ids", value);
|
||||
}
|
||||
|
||||
@action
|
||||
updateHideSchema(value) {
|
||||
this.hideSchema = value;
|
||||
}
|
||||
|
||||
@action
|
||||
editName() {
|
||||
this.editingName = true;
|
||||
}
|
||||
|
||||
@action
|
||||
editQuery() {
|
||||
this.editingQuery = true;
|
||||
}
|
||||
|
||||
@action
|
||||
download() {
|
||||
window.open(this.model.downloadUrl, "_blank");
|
||||
}
|
||||
|
||||
@action
|
||||
goHome() {
|
||||
this.router.transitionTo("adminPlugins.explorer");
|
||||
}
|
||||
|
||||
@action
|
||||
showHelpModal() {
|
||||
this.modal.show(QueryHelp);
|
||||
}
|
||||
|
||||
@action
|
||||
resetParams() {
|
||||
this.model.resetParams();
|
||||
}
|
||||
|
||||
@action
|
||||
async discard() {
|
||||
try {
|
||||
this.loading = true;
|
||||
const result = await this.store.find("query", this.model.id);
|
||||
this.model.setProperties(result.getProperties(Query.updatePropertyNames));
|
||||
if (!this.model.group_ids || !Array.isArray(this.model.group_ids)) {
|
||||
this.model.set("group_ids", []);
|
||||
}
|
||||
this.dirty = false;
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async destroyQuery() {
|
||||
try {
|
||||
this.loading = true;
|
||||
this.showResults = false;
|
||||
await this.store.destroyRecord("query", this.model);
|
||||
this.model.set("destroyed", true);
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async recover() {
|
||||
try {
|
||||
this.loading = true;
|
||||
this.showResults = true;
|
||||
await this.model.save();
|
||||
this.model.set("destroyed", false);
|
||||
} catch (error) {
|
||||
popupAjaxError(error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
onRegisterApi(form) {
|
||||
this.form = form;
|
||||
}
|
||||
|
||||
@action
|
||||
setDirty() {
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
@action
|
||||
exitEdit() {
|
||||
this.editingName = false;
|
||||
}
|
||||
|
||||
@action
|
||||
async run() {
|
||||
let params = null;
|
||||
if (this.model.hasParams) {
|
||||
try {
|
||||
params = await this.form?.submit();
|
||||
} catch (err) {
|
||||
if (err instanceof ParamValidationError) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (params == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.setProperties({
|
||||
loading: true,
|
||||
showResults: false,
|
||||
params: JSON.stringify(params),
|
||||
});
|
||||
|
||||
ajax("/admin/plugins/explorer/queries/" + this.model.id + "/run", {
|
||||
type: "POST",
|
||||
data: {
|
||||
params: JSON.stringify(params),
|
||||
explain: this.explain,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.results = result;
|
||||
if (!result.success) {
|
||||
this.showResults = false;
|
||||
return;
|
||||
}
|
||||
this.showResults = true;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.showResults = false;
|
||||
if (err.jqXHR && err.jqXHR.status === 422 && err.jqXHR.responseJSON) {
|
||||
this.results = err.jqXHR.responseJSON;
|
||||
} else {
|
||||
popupAjaxError(err);
|
||||
}
|
||||
})
|
||||
.finally(() => (this.loading = false));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import Controller from "@ember/controller";
|
||||
|
||||
export default class GroupReportsIndexController extends Controller {}
|
|
@ -0,0 +1,137 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import Controller from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import BookmarkModal from "discourse/components/modal/bookmark";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { BookmarkFormData } from "discourse/lib/bookmark-form-data";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
import {
|
||||
NO_REMINDER_ICON,
|
||||
WITH_REMINDER_ICON,
|
||||
} from "discourse/models/bookmark";
|
||||
import { ParamValidationError } from "discourse/plugins/discourse-data-explorer/discourse/components/param-input-form";
|
||||
|
||||
export default class GroupReportsShowController extends Controller {
|
||||
@service currentUser;
|
||||
@service modal;
|
||||
@service router;
|
||||
|
||||
@tracked showResults = false;
|
||||
@tracked loading = false;
|
||||
@tracked results = this.model.results;
|
||||
@tracked queryGroupBookmark = this.queryGroup?.bookmark;
|
||||
|
||||
queryParams = ["params"];
|
||||
form = null;
|
||||
explain = false;
|
||||
|
||||
get parsedParams() {
|
||||
return this.params ? JSON.parse(this.params) : null;
|
||||
}
|
||||
|
||||
get hasParams() {
|
||||
return this.model.param_info.length > 0;
|
||||
}
|
||||
|
||||
get bookmarkLabel() {
|
||||
return this.queryGroupBookmark
|
||||
? "bookmarked.edit_bookmark"
|
||||
: "bookmarked.title";
|
||||
}
|
||||
|
||||
get bookmarkIcon() {
|
||||
if (this.queryGroupBookmark && this.queryGroupBookmark.reminder_at) {
|
||||
return WITH_REMINDER_ICON;
|
||||
}
|
||||
return NO_REMINDER_ICON;
|
||||
}
|
||||
|
||||
get bookmarkClassName() {
|
||||
return this.queryGroupBookmark
|
||||
? ["query-group-bookmark", "bookmarked"].join(" ")
|
||||
: "query-group-bookmark";
|
||||
}
|
||||
|
||||
@bind
|
||||
async run() {
|
||||
try {
|
||||
let params = null;
|
||||
if (this.hasParams) {
|
||||
params = await this.form.submit();
|
||||
if (params == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.loading = true;
|
||||
this.showResults = false;
|
||||
const stringifiedParams = JSON.stringify(params);
|
||||
this.router.transitionTo({
|
||||
queryParams: {
|
||||
params: params ? stringifiedParams : null,
|
||||
},
|
||||
});
|
||||
const response = await ajax(
|
||||
`/g/${this.get("group.name")}/reports/${this.model.id}/run`,
|
||||
{
|
||||
type: "POST",
|
||||
data: {
|
||||
params: stringifiedParams,
|
||||
explain: this.explain,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.results = response;
|
||||
if (!response.success) {
|
||||
return;
|
||||
}
|
||||
this.showResults = true;
|
||||
} catch (error) {
|
||||
if (error.jqXHR?.status === 422 && error.jqXHR.responseJSON) {
|
||||
this.results = error.jqXHR.responseJSON;
|
||||
} else if (!(error instanceof ParamValidationError)) {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
toggleBookmark() {
|
||||
const modalBookmark =
|
||||
this.queryGroupBookmark ||
|
||||
this.store.createRecord("bookmark", {
|
||||
bookmarkable_type: "DiscourseDataExplorer::QueryGroup",
|
||||
bookmarkable_id: this.queryGroup.id,
|
||||
user_id: this.currentUser.id,
|
||||
});
|
||||
return this.modal.show(BookmarkModal, {
|
||||
model: {
|
||||
bookmark: new BookmarkFormData(modalBookmark),
|
||||
afterSave: (bookmarkFormData) => {
|
||||
const bookmark = this.store.createRecord(
|
||||
"bookmark",
|
||||
bookmarkFormData.saveData
|
||||
);
|
||||
this.queryGroupBookmark = bookmark;
|
||||
this.appEvents.trigger(
|
||||
"bookmarks:changed",
|
||||
bookmarkFormData.saveData,
|
||||
bookmark.attachedTo()
|
||||
);
|
||||
},
|
||||
afterDelete: () => {
|
||||
this.queryGroupBookmark = null;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
onRegisterApi(form) {
|
||||
this.form = form;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
export default {
|
||||
resource: "admin.adminPlugins",
|
||||
path: "/plugins",
|
||||
map() {
|
||||
this.route("explorer", function () {
|
||||
this.route("queries", function () {
|
||||
this.route("details", { path: "/:query_id" });
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
export default {
|
||||
resource: "group",
|
||||
|
||||
map() {
|
||||
this.route("reports", function () {
|
||||
this.route("show", { path: "/:query_id" });
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
name: "initialize-data-explorer",
|
||||
initialize(container) {
|
||||
container.lookup("service:store").addPluralization("query", "queries");
|
||||
},
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
export default function themeColor(name) {
|
||||
const style = getComputedStyle(document.body);
|
||||
return style.getPropertyValue(name);
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import { computed } from "@ember/object";
|
||||
import getURL from "discourse/lib/get-url";
|
||||
import RestModel from "discourse/models/rest";
|
||||
|
||||
export default class Query extends RestModel {
|
||||
static updatePropertyNames = [
|
||||
"name",
|
||||
"description",
|
||||
"sql",
|
||||
"user_id",
|
||||
"created_at",
|
||||
"group_ids",
|
||||
"last_run_at",
|
||||
];
|
||||
|
||||
params = {};
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.param_info?.resetParams();
|
||||
}
|
||||
|
||||
get downloadUrl() {
|
||||
return getURL(`/admin/plugins/explorer/queries/${this.id}.json?export=1`);
|
||||
}
|
||||
|
||||
@computed("param_info", "updating")
|
||||
get hasParams() {
|
||||
// When saving, we need to refresh the param-input component to clean up the old key
|
||||
return this.param_info.length && !this.updating;
|
||||
}
|
||||
|
||||
beforeUpdate() {
|
||||
this.set("updating", true);
|
||||
}
|
||||
|
||||
afterUpdate() {
|
||||
this.set("updating", false);
|
||||
}
|
||||
|
||||
resetParams() {
|
||||
const newParams = {};
|
||||
const oldParams = this.params;
|
||||
this.param_info.forEach((pinfo) => {
|
||||
const name = pinfo.identifier;
|
||||
if (oldParams[pinfo.identifier]) {
|
||||
newParams[name] = oldParams[name];
|
||||
} else if (pinfo["default"] !== null) {
|
||||
newParams[name] = pinfo["default"];
|
||||
} else if (pinfo["type"] === "boolean") {
|
||||
newParams[name] = "false";
|
||||
} else if (pinfo["type"] === "user_id") {
|
||||
newParams[name] = null;
|
||||
} else if (pinfo["type"] === "user_list") {
|
||||
newParams[name] = null;
|
||||
} else if (pinfo["type"] === "group_list") {
|
||||
newParams[name] = null;
|
||||
} else {
|
||||
newParams[name] = "";
|
||||
}
|
||||
});
|
||||
this.params = newParams;
|
||||
}
|
||||
|
||||
updateProperties() {
|
||||
const props = this.getProperties(Query.updatePropertyNames);
|
||||
if (this.destroyed) {
|
||||
props.id = this.id;
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
createProperties() {
|
||||
if (this.sql) {
|
||||
// Importing
|
||||
return this.updateProperties();
|
||||
}
|
||||
return this.getProperties("name");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import { service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class AdminPluginsExplorerIndex extends DiscourseRoute {
|
||||
@service router;
|
||||
|
||||
beforeModel(transition) {
|
||||
// Redirect old /explorer?id=123 route to /explorer/queries/123
|
||||
if (transition.to.queryParams.id) {
|
||||
this.router.transitionTo(
|
||||
"adminPlugins.explorer.queries.details",
|
||||
transition.to.queryParams.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
model() {
|
||||
if (!this.currentUser.admin) {
|
||||
// display "Only available to admins" message
|
||||
return { model: null, schema: null, disallow: true, groups: null };
|
||||
}
|
||||
|
||||
const groupPromise = ajax("/admin/plugins/explorer/groups.json");
|
||||
const queryPromise = this.store.findAll("query");
|
||||
|
||||
return groupPromise.then((groups) => {
|
||||
let groupNames = {};
|
||||
groups.forEach((g) => {
|
||||
groupNames[g.id] = g.name;
|
||||
});
|
||||
return queryPromise.then((model) => {
|
||||
model.forEach((query) => {
|
||||
query.set(
|
||||
"group_names",
|
||||
(query.group_ids || []).map((id) => groupNames[id])
|
||||
);
|
||||
});
|
||||
return { model, groups };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.setProperties(model);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { ajax } from "discourse/lib/ajax";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class AdminPluginsExplorerQueriesDetails extends DiscourseRoute {
|
||||
model(params) {
|
||||
if (!this.currentUser.admin) {
|
||||
// display "Only available to admins" message
|
||||
return { model: null, schema: null, disallow: true, groups: null };
|
||||
}
|
||||
|
||||
const groupPromise = ajax("/admin/plugins/explorer/groups.json");
|
||||
const schemaPromise = ajax("/admin/plugins/explorer/schema.json", {
|
||||
cache: true,
|
||||
});
|
||||
const queryPromise = this.store.find("query", params.query_id);
|
||||
|
||||
return groupPromise.then((groups) => {
|
||||
let groupNames = {};
|
||||
groups.forEach((g) => {
|
||||
groupNames[g.id] = g.name;
|
||||
});
|
||||
return schemaPromise.then((schema) => {
|
||||
return queryPromise.then((model) => {
|
||||
model.set(
|
||||
"group_names",
|
||||
(model.group_ids || []).map((id) => groupNames[id])
|
||||
);
|
||||
return { model, schema, groups };
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.setProperties(model);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import { service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class GroupReportsIndexRoute extends DiscourseRoute {
|
||||
@service router;
|
||||
|
||||
model() {
|
||||
const group = this.modelFor("group");
|
||||
return ajax(`/g/${group.name}/reports`)
|
||||
.then((queries) => {
|
||||
return {
|
||||
model: queries,
|
||||
group,
|
||||
};
|
||||
})
|
||||
.catch(() => this.router.transitionTo("group.members", group));
|
||||
}
|
||||
|
||||
afterModel(model) {
|
||||
if (
|
||||
!model.group.get("is_group_user") &&
|
||||
!(this.currentUser && this.currentUser.admin)
|
||||
) {
|
||||
this.router.transitionTo("group.members", model.group);
|
||||
}
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.setProperties(model);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { service } from "@ember/service";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class GroupReportsShowRoute extends DiscourseRoute {
|
||||
@service router;
|
||||
|
||||
model(params) {
|
||||
const group = this.modelFor("group");
|
||||
return ajax(`/g/${group.name}/reports/${params.query_id}`)
|
||||
.then((response) => {
|
||||
const query = response.query;
|
||||
const queryGroup = response.query_group;
|
||||
|
||||
const queryParamInfo = query.param_info;
|
||||
const queryParams = queryParamInfo.reduce((acc, param) => {
|
||||
acc[param.identifier] = param.default;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
model: Object.assign({ params: queryParams }, query),
|
||||
group,
|
||||
queryGroup,
|
||||
results: null,
|
||||
showResults: false,
|
||||
};
|
||||
})
|
||||
.catch(() => {
|
||||
this.router.transitionTo("group.members", group);
|
||||
});
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.setProperties(model);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
import { fn } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import { not } from "truth-helpers";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import PickFilesButton from "discourse/components/pick-files-button";
|
||||
import TableHeaderToggle from "discourse/components/table-header-toggle";
|
||||
import TextField from "discourse/components/text-field";
|
||||
import boundDate from "discourse/helpers/bound-date";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import ShareReport from "../../components/share-report";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
{{#if @controller.disallow}}
|
||||
<h1>{{i18n "explorer.admins_only"}}</h1>
|
||||
{{else}}
|
||||
<div class="query-list">
|
||||
<TextField
|
||||
@value={{@controller.search}}
|
||||
@placeholderKey="explorer.search_placeholder"
|
||||
@onChange={{@controller.updateSearch}}
|
||||
/>
|
||||
<DButton
|
||||
@action={{@controller.displayCreate}}
|
||||
@icon="plus"
|
||||
class="no-text btn-right"
|
||||
/>
|
||||
<PickFilesButton
|
||||
@label="explorer.import.label"
|
||||
@icon="upload"
|
||||
@acceptedFormatsOverride={{@controller.acceptedImportFileTypes}}
|
||||
@showButton="true"
|
||||
@onFilesPicked={{@controller.import}}
|
||||
class="import-btn"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{#if @controller.showCreate}}
|
||||
<div class="query-create">
|
||||
<TextField
|
||||
@value={{@controller.newQueryName}}
|
||||
@placeholderKey="explorer.create_placeholder"
|
||||
@onChange={{@controller.updateNewQueryName}}
|
||||
/>
|
||||
<DButton
|
||||
@action={{@controller.create}}
|
||||
@disabled={{@controller.createDisabled}}
|
||||
@label="explorer.create"
|
||||
@icon="plus"
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if @controller.othersDirty}}
|
||||
<div class="warning">
|
||||
{{icon "triangle-exclamation"}}
|
||||
{{i18n "explorer.others_dirty"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if @controller.model.length}}
|
||||
<ConditionalLoadingSpinner @condition={{@controller.loading}} />
|
||||
|
||||
<div class="container">
|
||||
<table class="d-admin-table recent-queries">
|
||||
<thead class="heading-container">
|
||||
<th class="col heading name">
|
||||
<div
|
||||
role="button"
|
||||
class="heading-toggle"
|
||||
{{on "click" (fn @controller.updateSortProperty "name")}}
|
||||
>
|
||||
<TableHeaderToggle
|
||||
@field="name"
|
||||
@labelKey="explorer.query_name"
|
||||
@order={{@controller.order}}
|
||||
@asc={{not @controller.sortDescending}}
|
||||
@automatic="true"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th class="col heading created-by">
|
||||
<div
|
||||
role="button"
|
||||
class="heading-toggle"
|
||||
{{on "click" (fn @controller.updateSortProperty "username")}}
|
||||
>
|
||||
<TableHeaderToggle
|
||||
@field="username"
|
||||
@labelKey="explorer.query_user"
|
||||
@order={{@controller.order}}
|
||||
@asc={{not @controller.sortDescending}}
|
||||
@automatic="true"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
<th class="col heading group-names">
|
||||
<div class="group-names-header">
|
||||
{{i18n "explorer.query_groups"}}
|
||||
</div>
|
||||
</th>
|
||||
<th class="col heading created-at">
|
||||
<div
|
||||
role="button"
|
||||
class="heading-toggle"
|
||||
{{on
|
||||
"click"
|
||||
(fn @controller.updateSortProperty "last_run_at")
|
||||
}}
|
||||
>
|
||||
<TableHeaderToggle
|
||||
@field="last_run_at"
|
||||
@labelKey="explorer.query_time"
|
||||
@order={{@controller.order}}
|
||||
@asc={{not @controller.sortDescending}}
|
||||
@automatic="true"
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each @controller.filteredContent as |query|}}
|
||||
<tr class="d-admin-row__content query-row">
|
||||
<td class="d-admin-row__overview">
|
||||
<a
|
||||
{{on "click" @controller.scrollTop}}
|
||||
href="/admin/plugins/explorer/queries/{{query.id}}"
|
||||
>
|
||||
<b class="query-name">{{query.name}}</b>
|
||||
<span class="query-desc">{{query.description}}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="d-admin-row__detail query-created-by">
|
||||
<div class="d-admin-row__mobile-label">
|
||||
{{i18n "explorer.query_user"}}
|
||||
</div>
|
||||
{{#if query.username}}
|
||||
<div>
|
||||
<a href="/u/{{query.username}}/activity">
|
||||
<span>{{query.username}}</span>
|
||||
</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="d-admin-row__detail query-group-names">
|
||||
<div class="d-admin-row__mobile-label">
|
||||
{{i18n "explorer.query_groups"}}
|
||||
</div>
|
||||
<div class="group-names">
|
||||
{{#each query.group_names as |group|}}
|
||||
<ShareReport @group={{group}} @query={{query}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
</td>
|
||||
<td class="d-admin-row__detail query-created-at">
|
||||
<div class="d-admin-row__mobile-label">
|
||||
{{i18n "explorer.query_time"}}
|
||||
</div>
|
||||
{{#if query.last_run_at}}
|
||||
<span>
|
||||
{{boundDate query.last_run_at}}
|
||||
</span>
|
||||
{{else if query.created_at}}
|
||||
<span>
|
||||
{{boundDate query.created_at}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<br />
|
||||
<em class="no-search-results">
|
||||
{{i18n "explorer.no_search_results"}}
|
||||
</em>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="explorer-pad-bottom"></div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</template>
|
||||
);
|
|
@ -0,0 +1,262 @@
|
|||
import { Input } from "@ember/component";
|
||||
import { fn, hash } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import AceEditor from "discourse/components/ace-editor";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import DTextarea from "discourse/components/d-textarea";
|
||||
import TextField from "discourse/components/text-field";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import draggable from "discourse/modifiers/draggable";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import MultiSelect from "select-kit/components/multi-select";
|
||||
import CodeView from "../../components/code-view";
|
||||
import ExplorerSchema from "../../components/explorer-schema";
|
||||
import ParamInputForm from "../../components/param-input-form";
|
||||
import QueryResultsWrapper from "../../components/query-results-wrapper";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
{{#if @controller.disallow}}
|
||||
<h1>{{i18n "explorer.admins_only"}}</h1>
|
||||
{{else}}
|
||||
|
||||
<div class="query-edit {{if @controller.editingName 'editing'}}">
|
||||
{{#if @controller.editingName}}
|
||||
<div class="name">
|
||||
<DButton
|
||||
@action={{@controller.goHome}}
|
||||
@icon="chevron-left"
|
||||
class="previous"
|
||||
/>
|
||||
<DButton
|
||||
@action={{@controller.exitEdit}}
|
||||
@icon="xmark"
|
||||
class="previous"
|
||||
/>
|
||||
<div class="name-text-field">
|
||||
<TextField
|
||||
@value={{@controller.model.name}}
|
||||
@onChange={{@controller.setDirty}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="desc">
|
||||
<DTextarea
|
||||
@value={{@controller.model.description}}
|
||||
@placeholder={{i18n "explorer.description_placeholder"}}
|
||||
@input={{@controller.setDirty}}
|
||||
/>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="name">
|
||||
<DButton
|
||||
@action={{@controller.goHome}}
|
||||
@icon="chevron-left"
|
||||
class="previous"
|
||||
/>
|
||||
|
||||
<h1>
|
||||
<span>{{@controller.model.name}}</span>
|
||||
{{#unless @controller.editDisabled}}
|
||||
<DButton
|
||||
@action={{@controller.editName}}
|
||||
@icon="pencil"
|
||||
class="edit-query-name btn-transparent"
|
||||
/>
|
||||
{{/unless}}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="desc">
|
||||
{{@controller.model.description}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#unless @controller.model.destroyed}}
|
||||
<div class="groups">
|
||||
<span class="label">{{i18n "explorer.allow_groups"}}</span>
|
||||
<span>
|
||||
<MultiSelect
|
||||
@value={{@controller.model.group_ids}}
|
||||
@content={{@controller.groupOptions}}
|
||||
@options={{hash allowAny=false}}
|
||||
@onChange={{@controller.updateGroupIds}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
<div class="clear"></div>
|
||||
|
||||
{{#if @controller.editingQuery}}
|
||||
<div class="query-editor {{if @controller.hideSchema 'no-schema'}}">
|
||||
<div class="panels-flex">
|
||||
<div class="editor-panel">
|
||||
<AceEditor
|
||||
{{on "click" @controller.setDirty}}
|
||||
@content={{@controller.model.sql}}
|
||||
@onChange={{fn (mut @controller.model.sql)}}
|
||||
@mode="sql"
|
||||
@disabled={{@controller.model.destroyed}}
|
||||
@save={{@controller.save}}
|
||||
@submit={{@controller.saveAndRun}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="right-panel">
|
||||
<ExplorerSchema
|
||||
@schema={{@controller.schema}}
|
||||
@hideSchema={{@controller.hideSchema}}
|
||||
@updateHideSchema={{@controller.updateHideSchema}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grippie"
|
||||
{{draggable
|
||||
didStartDrag=@controller.didStartDrag
|
||||
didEndDrag=@controller.didEndDrag
|
||||
dragMove=@controller.dragMove
|
||||
}}
|
||||
>
|
||||
{{icon "discourse-expand"}}
|
||||
</div>
|
||||
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="sql">
|
||||
<CodeView
|
||||
@value={{@controller.model.sql}}
|
||||
@codeClass="sql"
|
||||
@setDirty={{@controller.setDirty}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="clear"></div>
|
||||
|
||||
<div class="pull-left left-buttons">
|
||||
{{#if @controller.editingQuery}}
|
||||
<DButton
|
||||
class="btn-save-query"
|
||||
@action={{@controller.save}}
|
||||
@label="explorer.save"
|
||||
@disabled={{@controller.saveDisabled}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#unless @controller.editDisabled}}
|
||||
<DButton
|
||||
class="btn-edit-query"
|
||||
@action={{@controller.editQuery}}
|
||||
@label="explorer.edit"
|
||||
@icon="pencil"
|
||||
/>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
<DButton
|
||||
@action={{@controller.download}}
|
||||
@label="explorer.export"
|
||||
@disabled={{@controller.runDisabled}}
|
||||
@icon="download"
|
||||
/>
|
||||
|
||||
{{#if @controller.editingQuery}}
|
||||
<DButton
|
||||
@action={{@controller.showHelpModal}}
|
||||
@label="explorer.help.label"
|
||||
@icon="circle-question"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="pull-right right-buttons">
|
||||
{{#if @controller.model.destroyed}}
|
||||
<DButton
|
||||
@action={{@controller.recover}}
|
||||
@icon="arrow-rotate-left"
|
||||
@label="explorer.recover"
|
||||
/>
|
||||
{{else}}
|
||||
{{#if @controller.editingQuery}}
|
||||
<DButton
|
||||
@action={{@controller.discard}}
|
||||
@icon="arrow-rotate-left"
|
||||
@label="explorer.undo"
|
||||
@disabled={{@controller.saveDisabled}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<DButton
|
||||
@action={{@controller.destroyQuery}}
|
||||
@icon="trash-can"
|
||||
@label="explorer.delete"
|
||||
class="btn-danger"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
|
||||
<form class="query-run" {{on "submit" @controller.run}}>
|
||||
{{#if @controller.model.hasParams}}
|
||||
<ParamInputForm
|
||||
@initialValues={{@controller.parsedParams}}
|
||||
@paramInfo={{@controller.model.param_info}}
|
||||
@onRegisterApi={{@controller.onRegisterApi}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if @controller.runDisabled}}
|
||||
{{#if @controller.saveDisabled}}
|
||||
<DButton
|
||||
@label="explorer.run"
|
||||
@disabled="true"
|
||||
class="btn-primary"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@action={{@controller.saveAndRun}}
|
||||
@icon="play"
|
||||
@label="explorer.saverun"
|
||||
class="btn-primary"
|
||||
/>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<DButton
|
||||
@action={{@controller.run}}
|
||||
@icon="play"
|
||||
@label="explorer.run"
|
||||
@disabled={{@controller.runDisabled}}
|
||||
@type="submit"
|
||||
class="btn-primary"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<label class="query-plan">
|
||||
<Input
|
||||
@type="checkbox"
|
||||
@checked={{@controller.explain}}
|
||||
name="explain"
|
||||
/>
|
||||
{{i18n "explorer.explain_label"}}
|
||||
</label>
|
||||
</form>
|
||||
<hr />
|
||||
|
||||
<ConditionalLoadingSpinner @condition={{@controller.loading}} />
|
||||
|
||||
<QueryResultsWrapper
|
||||
@results={{@controller.results}}
|
||||
@showResults={{@controller.showResults}}
|
||||
@query={{@controller.model}}
|
||||
@content={{@controller.results}}
|
||||
/>
|
||||
{{/if}}
|
||||
</template>
|
||||
);
|
|
@ -0,0 +1,45 @@
|
|||
import { array } from "@ember/helper";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import boundDate from "discourse/helpers/bound-date";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
<section class="user-content">
|
||||
<table class="group-reports">
|
||||
<thead>
|
||||
<th>
|
||||
{{i18n "explorer.report_name"}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n "explorer.query_description"}}
|
||||
</th>
|
||||
<th>
|
||||
{{i18n "explorer.query_time"}}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each @controller.model.queries as |query|}}
|
||||
<tr>
|
||||
<td>
|
||||
<LinkTo
|
||||
@route="group.reports.show"
|
||||
@models={{array @controller.group.name query.id}}
|
||||
>
|
||||
{{query.name}}
|
||||
</LinkTo>
|
||||
</td>
|
||||
<td>{{query.description}}</td>
|
||||
<td>
|
||||
{{#if query.last_run_at}}
|
||||
{{boundDate query.last_run_at}}
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</template>
|
||||
);
|
|
@ -0,0 +1,58 @@
|
|||
import { on } from "@ember/modifier";
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import ParamInputForm from "../components/param-input-form";
|
||||
import QueryResult from "../components/query-result";
|
||||
|
||||
export default RouteTemplate(
|
||||
<template>
|
||||
<section class="user-content">
|
||||
<h1>{{@controller.model.name}}</h1>
|
||||
<p>{{@controller.model.description}}</p>
|
||||
|
||||
<form class="query-run" {{on "submit" @controller.run}}>
|
||||
{{#if @controller.hasParams}}
|
||||
<ParamInputForm
|
||||
@initialValues={{@controller.parsedParams}}
|
||||
@paramInfo={{@controller.model.param_info}}
|
||||
@onRegisterApi={{@controller.onRegisterApi}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<DButton
|
||||
@action={{@controller.run}}
|
||||
@icon="play"
|
||||
@label="explorer.run"
|
||||
@type="submit"
|
||||
class="btn-primary"
|
||||
/>
|
||||
|
||||
<DButton
|
||||
@action={{@controller.toggleBookmark}}
|
||||
@label={{@controller.bookmarkLabel}}
|
||||
@icon={{@controller.bookmarkIcon}}
|
||||
class={{@controller.bookmarkClassName}}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<ConditionalLoadingSpinner @condition={{@controller.loading}} />
|
||||
|
||||
{{#if @controller.results}}
|
||||
<div class="query-results">
|
||||
{{#if @controller.showResults}}
|
||||
<QueryResult
|
||||
@query={{@controller.model}}
|
||||
@content={{@controller.results}}
|
||||
@group={{@controller.group}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#each @controller.results.errors as |err|}}
|
||||
<pre class="query-error"><code>{{~err}}</code></pre>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</section>
|
||||
</template>
|
||||
);
|
568
plugins/discourse-data-explorer/assets/stylesheets/explorer.scss
Normal file
568
plugins/discourse-data-explorer/assets/stylesheets/explorer.scss
Normal file
|
@ -0,0 +1,568 @@
|
|||
@use "lib/viewport";
|
||||
|
||||
table.group-reports {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
|
||||
th:first-child {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
th:nth-child(2) {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
th:last-child {
|
||||
width: 20%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
tbody tr td {
|
||||
padding: 0.5em;
|
||||
|
||||
&:first-child {
|
||||
font-size: $font-up-1;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.https-warning {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.query-editor {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.panels-flex {
|
||||
display: flex;
|
||||
height: 400px;
|
||||
border: 1px solid var(--primary-very-low);
|
||||
}
|
||||
|
||||
&.no-schema {
|
||||
.editor-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
width: 0;
|
||||
|
||||
button.unhide {
|
||||
position: absolute;
|
||||
margin-left: -53px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editor-panel {
|
||||
flex-grow: 1;
|
||||
|
||||
.ace-wrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ace_editor {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
width: 345px;
|
||||
|
||||
.schema {
|
||||
border-left: 1px solid var(--primary-low);
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
color: var(--primary-high);
|
||||
font-size: var(--font-down-1);
|
||||
position: relative;
|
||||
|
||||
.schema-search {
|
||||
padding: 0.5em;
|
||||
position: sticky;
|
||||
background-color: var(--secondary);
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.schema-table-name {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid var(--primary-low);
|
||||
padding-left: 5px;
|
||||
|
||||
.fa {
|
||||
width: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dl > div > * {
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
dl > div {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
dt {
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
overflow-wrap: break-word;
|
||||
width: 110px;
|
||||
margin-left: 5px;
|
||||
|
||||
&.sensitive {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
dd {
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
width: 110px;
|
||||
color: var(--tertiary);
|
||||
margin: 0;
|
||||
padding-left: 7px;
|
||||
border-left: 1px dotted var(--primary-low-mid);
|
||||
|
||||
.schema-typenotes {
|
||||
color: var(--primary-medium);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.popup-info {
|
||||
color: var(--primary-medium);
|
||||
|
||||
.popup {
|
||||
display: none;
|
||||
width: 180px;
|
||||
padding: 4px;
|
||||
position: relative;
|
||||
border: 1px solid;
|
||||
background: var(--secondary);
|
||||
padding-right: calc(5px + 0.5em);
|
||||
}
|
||||
|
||||
&:hover .popup {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:focus .popup {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.popup ol {
|
||||
padding: 0;
|
||||
position: relative;
|
||||
list-style: none;
|
||||
|
||||
> li::before {
|
||||
content: attr(value) ": ";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grippie {
|
||||
cursor: nwse-resize;
|
||||
clear: both;
|
||||
font-size: $font-down-2;
|
||||
user-select: none;
|
||||
color: var(--primary);
|
||||
text-align: right;
|
||||
background: var(--primary-very-low);
|
||||
border: 1px solid var(--primary-very-low);
|
||||
|
||||
.d-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-edit {
|
||||
> .name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 1em;
|
||||
|
||||
h1 {
|
||||
display: inline-block;
|
||||
margin: 0 0.5em 0 0;
|
||||
color: var(--primary);
|
||||
|
||||
button .d-icon {
|
||||
color: currentcolor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button.previous {
|
||||
margin-right: 0.5em;
|
||||
|
||||
.d-icon {
|
||||
margin-left: -0.15em; // fixing fontawesome horizontal alignment
|
||||
}
|
||||
}
|
||||
|
||||
.name-text-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.name input,
|
||||
.desc textarea {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:not(.editing) .desc {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.groups {
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
@include viewport.until(md) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-right: 10px;
|
||||
color: var(--primary-high);
|
||||
}
|
||||
|
||||
.name {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.select-kit.multi-select {
|
||||
@include viewport.until(md) {
|
||||
width: 360px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-run {
|
||||
margin-top: 25px;
|
||||
|
||||
.query-plan {
|
||||
display: inline-block;
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.schema-title {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.query-params {
|
||||
border: 1px solid var(--header_primary-medium);
|
||||
|
||||
.params-form {
|
||||
margin: 5px;
|
||||
|
||||
html.desktop-view & {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.param > input,
|
||||
.param > .select-kit {
|
||||
margin: 9px;
|
||||
}
|
||||
|
||||
.invalid input {
|
||||
background-color: var(--danger-low);
|
||||
}
|
||||
|
||||
.invalid .ac-wrap {
|
||||
background-color: var(--danger-low);
|
||||
}
|
||||
|
||||
.param {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0;
|
||||
|
||||
.ac-wrap {
|
||||
display: inline-block;
|
||||
|
||||
input {
|
||||
width: 100px !important; // override an inline style
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
.select-kit {
|
||||
width: auto;
|
||||
max-width: 250px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-list,
|
||||
.query-create,
|
||||
.query-edit,
|
||||
.query-results,
|
||||
.query-params,
|
||||
.https-warning {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.query-create {
|
||||
display: flex;
|
||||
|
||||
input {
|
||||
margin-right: 0.5em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.query-results {
|
||||
section {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
max-height: 1000px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
|
||||
td {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
thead {
|
||||
th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
color: var(--primary);
|
||||
background: var(--primary-low);
|
||||
z-index: z("base");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-list {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
.btn-left {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.btn-right {
|
||||
margin-left: auto;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
li.none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.import-btn {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.recent-queries {
|
||||
thead {
|
||||
.created-by {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.group-names {
|
||||
width: 15%;
|
||||
|
||||
.group-names-header {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.created-at {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.heading {
|
||||
position: relative;
|
||||
color: var(--primary-medium);
|
||||
padding: 50px 0 0 0;
|
||||
|
||||
th.sortable {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.heading-toggle {
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-row {
|
||||
a {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.query-name {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.query-desc {
|
||||
display: block;
|
||||
color: var(--primary-high);
|
||||
}
|
||||
|
||||
.query-created-by {
|
||||
color: var(--primary-high);
|
||||
}
|
||||
|
||||
.query-group-names {
|
||||
color: var(--tertiary);
|
||||
|
||||
a {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.group-names {
|
||||
@include viewport.until(md) {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.query-created-at {
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
}
|
||||
|
||||
.query-row:hover {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
tr a {
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.no-search-results {
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
}
|
||||
|
||||
.result-info {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.result-about {
|
||||
color: var(--primary-high);
|
||||
float: right;
|
||||
}
|
||||
|
||||
.result-explain {
|
||||
padding-top: 1em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.result-post-link {
|
||||
display: block;
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.result-json {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.result-json-value {
|
||||
flex: 1;
|
||||
margin-right: 0.5em;
|
||||
max-width: 250px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.explorer-pad-bottom {
|
||||
margin-bottom: 200px;
|
||||
}
|
||||
|
||||
.share-report {
|
||||
cursor: pointer;
|
||||
|
||||
label {
|
||||
color: var(--primary-high);
|
||||
}
|
||||
|
||||
input {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.popup {
|
||||
background-color: var(--secondary);
|
||||
position: absolute;
|
||||
box-shadow: var(--shadow-card);
|
||||
padding: 12px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.left-buttons,
|
||||
.right-buttons {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.left-buttons .btn {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.right-buttons .btn {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.query-group-bookmark {
|
||||
&.bookmarked .d-icon {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
}
|
124
plugins/discourse-data-explorer/config/locales/client.ar.yml
Normal file
124
plugins/discourse-data-explorer/config/locales/client.ar.yml
Normal file
|
@ -0,0 +1,124 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
ar:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "أزِل الفواصل المنقوطة من الاستعلام."
|
||||
dirty: "يجب عليك حفظ الاستعلام قبل التشغيل."
|
||||
explorer:
|
||||
or: "أو"
|
||||
admins_only: "مستكشف البيانات متاح للمسؤولين فقط."
|
||||
allow_groups: "السماح للمجموعات بالوصول إلى هذا الاستعلام"
|
||||
title: "مستكشف البيانات"
|
||||
create: "إنشاء جديد"
|
||||
create_placeholder: "اسم الاستعلام..."
|
||||
description_placeholder: "أدخِل وصفًا هنا"
|
||||
import:
|
||||
label: "استيراد"
|
||||
modal: "استيراد استعلام"
|
||||
unparseable_json: "ملف JSON غير قابل للتحليل"
|
||||
wrong_json: "ملف JSON خاطئ. يجب أن يحتوي ملف JSON على كائن \"استعلام\"، والذي يجب أن يحتوي على الأقل على خاصية \"sql\"."
|
||||
help:
|
||||
label: "المساعدة"
|
||||
modal_title: "مساعدة مستكشف البيانات"
|
||||
auto_resolution: "<h2>ربط السجلات التلقائي</h2> <p>عندما يؤدي الاستعلام إلى إرجاع مُعرِّف الكيان، فقد يستبدله مستكشف البيانات تلقائيًا باسم الكيان والمعلومات المفيدة الأخرى في نتائج الاستعلام. يتوفَّر الربط التلقائي لـ <i><b>user_id</b></i> و<i><b>group_id</b></i> و<i><b>topic_id</b></i> و<i><b>category_id</b></i> و<i><b>badge_id</b></i>. لتجربة ذلك، شغِّل هذا الاستعلام:</p> <pre><code>SELECT user_id\nFROM posts</code></pre>"
|
||||
custom_params: "<h2>إنشاء معلمات مخصَّصة</h2> <p>لإنشاء معلمات مخصَّصة لاستعلاماتك، ضع هذا في الجزء العلوي من استعلامك واتبع التنسيق:</p> <pre><code>-- [params]\n-- int :num = 1\n\nSELECT :num</code></pre> <p><i>ملاحظة: السطر الأول الذي يحتوي على [params] مطلوب، بالإضافة إلى شرطتين قبله وكل معلمة مخصَّصة تريد الإعلان عنها.</i></p>"
|
||||
default_values: "<h3>القيم الافتراضية</h3> <p>يمكنك إعلان المعلمات بقيم افتراضية أو من دونها. ستظهر القيم الافتراضية في حقلٍ نصي أسفل محرِّر الاستعلام، والذي يمكنك تعديله حسب احتياجاتك. ستظل المعلمات المعلنة دون قيم افتراضية تنشئ حقلًا نصيًا، لكنها ستكون فارغة ومظللة باللون الأحمر.</p> <pre><code>-- [params]\n-- text :username = my_username\n-- int :age</code></pre>"
|
||||
data_types: "<h3>أنواع البيانات</h3> <p>فيما يلي أنواع البيانات الشائعة التي يمكنك استخدامها:</p> <ul> <li><b>عدد صحيح</b> - عدد صحيح رباعي البايت مع مراعاة إشارته</li> <li><b>نص</b> - سلسلة أحرف متغيرة الطول</li> <li><b>قيمة منطقية</b> - صواب/خطأ</li> <li><b>تاريخ</b> - تاريخ تقويمي (سنة، شهر، يوم)</li> </ul> <p>لمزيد من المعلومات بشأن أنواع البيانات، انتقل إلى <a href='http://www.postgresql.org/docs/9.3/static/datatype.html#DATATYPE-TABLE' target='_blank'>هذا الموقع</a>.</p>"
|
||||
schema:
|
||||
title: "مخطط قاعدة البيانات"
|
||||
filter: "بحث..."
|
||||
sensitive: "قد تتضمَّن محتويات هذا العمود معلومات حساسة أو سرية بشكلٍ خاص. يُرجى توخي الحذر عند استخدام محتويات هذا العمود."
|
||||
types:
|
||||
bool:
|
||||
yes: "نعم"
|
||||
no: "لا"
|
||||
null_: "فارغ"
|
||||
export: "تصدير"
|
||||
view_json: "عرض JSON"
|
||||
save: "حفظ التغييرات"
|
||||
saverun: "حفظ التغييرات والتشغيل"
|
||||
run: "تشغيل"
|
||||
undo: "تجاهل التغييرات"
|
||||
edit: "تعديل"
|
||||
delete: "حذف"
|
||||
recover: "إلغاء حذف الاستعلام"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
show_table: "جدول"
|
||||
show_graph: "رسم بياني"
|
||||
others_dirty: "يحتوي الاستعلام على تغييرات غير محفوظة سيتم فقدها إذا انتقلت بعيدًا."
|
||||
run_time: "اكتمل الاستعلام في %{value} مللي ثانية."
|
||||
result_count:
|
||||
zero: "%{count} نتيجة."
|
||||
one: "نتيجة واحدة (%{count})."
|
||||
two: "نتيجتان (%{count})"
|
||||
few: "%{count} نتائج."
|
||||
many: "%{count} نتيجةً."
|
||||
other: "%{count} نتيجة."
|
||||
max_result_count:
|
||||
zero: "جارٍ عرض أبرز %{count} نتيجة."
|
||||
one: "جارٍ عرض أبرز نتيجة (%{count})."
|
||||
two: "جارٍ عرض أبرز نتيجتين (%{count})."
|
||||
few: "جارٍ عرض أبرز %{count} نتائج."
|
||||
many: "جارٍ عرض أبرز %{count} نتيجةً."
|
||||
other: "جارٍ عرض أبرز %{count} نتيجة."
|
||||
query_name: "استعلام"
|
||||
query_groups: "المجموعات"
|
||||
link: "رابط لـ"
|
||||
report_name: "تقرير"
|
||||
query_description: "الوصف"
|
||||
query_time: "آخر تشغيل"
|
||||
query_user: "تم الإنشاء من قِبل"
|
||||
column: "العمود %{number}"
|
||||
explain_label: "هل تريد تضمين خطة الاستعلام؟"
|
||||
save_params: "الضبط على الإعدادات الافتراضية"
|
||||
reset_params: "إعادة التعيين"
|
||||
search_placeholder: "بحث..."
|
||||
no_search_results: "عذرًا، لم نتمكن من العثور على أي نتائج مطابقة لنصك."
|
||||
form:
|
||||
errors:
|
||||
invalid: "غير صالح"
|
||||
no_such_category: "لا توجد فئة بهذا الاسم"
|
||||
no_such_group: "لا توجد مجموعة بهذا الاسم"
|
||||
invalid_date: "%{date} هو تاريخ غير صالح"
|
||||
invalid_time: "%{time} هو وقت غير صالح"
|
||||
group:
|
||||
reports: "التقارير"
|
||||
admin:
|
||||
api:
|
||||
scopes:
|
||||
descriptions:
|
||||
discourse_data_explorer:
|
||||
run_queries: "تشغيل استعلامات مستكشف البيانات. يمكنك تقييد مفتاح واجهة برمجة التطبيقات (API) إلى مجموعة من الاستعلامات عن طريق تحديد معرِّفات الاستعلامات."
|
||||
discourse_automation:
|
||||
scriptables:
|
||||
recurring_data_explorer_result_pm:
|
||||
fields:
|
||||
recipients:
|
||||
label: الإرسال إلى مستخدم أو مجموعة أو بريد إلكتروني
|
||||
query_id:
|
||||
label: استعلام مستكشف البيانات
|
||||
query_params:
|
||||
label: معلمات استعلام مستكشف البيانات
|
||||
skip_empty:
|
||||
label: تخطي إرسال رسالة شخصية إذا لم تكن هناك نتائج
|
||||
attach_csv:
|
||||
label: إرفاق ملف CSV بالرسالة الشخصية
|
||||
recurring_data_explorer_result_topic:
|
||||
fields:
|
||||
topic_id:
|
||||
label: الموضوع الذي سيتم نشر نتائج الاستعلام فيه
|
||||
query_id:
|
||||
label: استعلام مستكشف البيانات
|
||||
query_params:
|
||||
label: معلمات استعلام مستكشف البيانات
|
||||
skip_empty:
|
||||
label: تخطي النشر إذا لم تكن هناك نتائج
|
||||
attach_csv:
|
||||
label: إرفاق ملف CSV بالمنشور
|
23
plugins/discourse-data-explorer/config/locales/client.be.yml
Normal file
23
plugins/discourse-data-explorer/config/locales/client.be.yml
Normal file
|
@ -0,0 +1,23 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
be:
|
||||
js:
|
||||
explorer:
|
||||
or: "або"
|
||||
types:
|
||||
bool:
|
||||
yes: "Так"
|
||||
no: "Не"
|
||||
export: "экспарт"
|
||||
save: "Захаваць"
|
||||
edit: "Рэдагаваць"
|
||||
delete: "Выдаліць"
|
||||
query_groups: "групы"
|
||||
query_description: "апісанне"
|
||||
reset_params: "скінуць"
|
||||
group:
|
||||
reports: "Справаздачы"
|
31
plugins/discourse-data-explorer/config/locales/client.bg.yml
Normal file
31
plugins/discourse-data-explorer/config/locales/client.bg.yml
Normal file
|
@ -0,0 +1,31 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
bg:
|
||||
js:
|
||||
explorer:
|
||||
or: "или"
|
||||
import:
|
||||
label: "Импорт"
|
||||
help:
|
||||
label: "Помощ"
|
||||
schema:
|
||||
filter: "Търсене ... "
|
||||
types:
|
||||
bool:
|
||||
yes: "Да"
|
||||
no: "Не"
|
||||
export: "Експорт "
|
||||
save: "Запази промените"
|
||||
edit: "Редактирай"
|
||||
delete: "Изтрий"
|
||||
result_count:
|
||||
one: "%{count} резултат."
|
||||
other: "%{count} резултата."
|
||||
query_groups: "Групи"
|
||||
query_description: "Описание"
|
||||
reset_params: "Нулиране"
|
||||
search_placeholder: "Търсене ... "
|
|
@ -0,0 +1,28 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
bs_BA:
|
||||
js:
|
||||
explorer:
|
||||
or: "ili"
|
||||
help:
|
||||
label: "Pomoć"
|
||||
schema:
|
||||
filter: "Pretraga..."
|
||||
types:
|
||||
bool:
|
||||
yes: "Da"
|
||||
no: "Ne"
|
||||
export: "Izvoz"
|
||||
save: "Spremiti promjene"
|
||||
edit: "Izmijeni"
|
||||
delete: "Delete"
|
||||
query_groups: "Grupa"
|
||||
query_description: "Opis"
|
||||
reset_params: "Resetovati"
|
||||
search_placeholder: "Pretraga..."
|
||||
group:
|
||||
reports: "Izvještaji"
|
63
plugins/discourse-data-explorer/config/locales/client.ca.yml
Normal file
63
plugins/discourse-data-explorer/config/locales/client.ca.yml
Normal file
|
@ -0,0 +1,63 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
ca:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "Elimina els punts i coma de la consulta."
|
||||
dirty: "Cal desar la consulta abans d'executar-la"
|
||||
explorer:
|
||||
or: "o"
|
||||
admins_only: "L'explorador de dades sols és disponible per a administradors."
|
||||
title: "Explorador de dades"
|
||||
create: "Crea'n un de nou"
|
||||
create_placeholder: "Nom de consulta..."
|
||||
description_placeholder: "Introduïu una descripció aquí"
|
||||
import:
|
||||
label: "Importa"
|
||||
modal: "Importa una consulta"
|
||||
help:
|
||||
label: "Ajuda"
|
||||
schema:
|
||||
title: "Esquema de base de dades"
|
||||
filter: "Cerca..."
|
||||
sensitive: "El contingut d'aquesta columna pot contenir informació particularment sensible o privada. Tingueu precaució quan utilitzeu el contingut d'aquesta columna."
|
||||
types:
|
||||
bool:
|
||||
yes: "Sí"
|
||||
no: "No"
|
||||
null_: "Nul"
|
||||
export: "Exporta"
|
||||
save: "Desa els canvis"
|
||||
saverun: "Desa els canvis i executa"
|
||||
run: "Executa"
|
||||
undo: "Descarta els canvis"
|
||||
edit: "Edita"
|
||||
delete: "Suprimeix"
|
||||
recover: "Restaura la consulta"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
others_dirty: "Una consulta té canvis no desats que es perdran si continueu navegant."
|
||||
run_time: "La consulta s'ha completat en %{value} ms."
|
||||
result_count:
|
||||
one: "%{count} resultat."
|
||||
other: "%{count} resultats."
|
||||
query_name: "Consulta"
|
||||
query_groups: "Grups"
|
||||
link: "Enllaç de"
|
||||
report_name: "Report"
|
||||
query_description: "Descripció"
|
||||
query_time: "Darrera execució"
|
||||
query_user: "Creat per"
|
||||
column: "Columna %{number}"
|
||||
explain_label: "Hi incloem pla de consulta?"
|
||||
save_params: "Estableix valors per defecte"
|
||||
reset_params: "Reinicia"
|
||||
search_placeholder: "Cerca..."
|
||||
no_search_results: "Ho sentim, no hem trobat cap resultat que coincideixi amb el vostre text."
|
||||
group:
|
||||
reports: "Reports"
|
39
plugins/discourse-data-explorer/config/locales/client.cs.yml
Normal file
39
plugins/discourse-data-explorer/config/locales/client.cs.yml
Normal file
|
@ -0,0 +1,39 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
cs:
|
||||
js:
|
||||
explorer:
|
||||
or: "nebo"
|
||||
create: "Vytvořit nový"
|
||||
import:
|
||||
label: "Import"
|
||||
help:
|
||||
label: "Nápověda"
|
||||
schema:
|
||||
filter: "Hledat..."
|
||||
types:
|
||||
bool:
|
||||
yes: "Ano"
|
||||
no: "Ne"
|
||||
null_: "Null"
|
||||
export: "Export"
|
||||
save: "Uložit změny"
|
||||
edit: "Upravit"
|
||||
delete: "Smazat"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
result_count:
|
||||
one: "%{count} výsledek."
|
||||
few: "%{count} výsledky."
|
||||
many: "%{count} výsledky."
|
||||
other: "%{count} výsledků."
|
||||
query_groups: "Skupiny"
|
||||
query_description: "Popis"
|
||||
reset_params: "obnovit výchozí"
|
||||
search_placeholder: "Hledat..."
|
||||
group:
|
||||
reports: "Přehledy"
|
54
plugins/discourse-data-explorer/config/locales/client.da.yml
Normal file
54
plugins/discourse-data-explorer/config/locales/client.da.yml
Normal file
|
@ -0,0 +1,54 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
da:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "Fjern semikoloner fra forespørgslen."
|
||||
dirty: "Du skal gemme forespørgslen, før du kører den."
|
||||
explorer:
|
||||
or: "eller"
|
||||
admins_only: "Data explorer er kun tilgængelige for administratorer."
|
||||
allow_groups: "Tillad grupper at få adgang til denne forespørgsel"
|
||||
title: "Data Udforsker"
|
||||
create: "Opret Ny"
|
||||
create_placeholder: "Navn på forespørgsel..."
|
||||
description_placeholder: "Indtast en beskrivelse her"
|
||||
import:
|
||||
label: "Importer"
|
||||
modal: "Importer en forespørgsel"
|
||||
help:
|
||||
label: "Hjælp"
|
||||
modal_title: "Hjælp til Data Udforsker"
|
||||
schema:
|
||||
title: "Database Skema"
|
||||
filter: "Søg..."
|
||||
types:
|
||||
bool:
|
||||
yes: "Ja"
|
||||
no: "Nej"
|
||||
null_: "Null"
|
||||
export: "Eksporter"
|
||||
save: "Gem ændringer"
|
||||
saverun: "Gem ændringer og kør"
|
||||
run: "Kør"
|
||||
undo: "Kassér Ændringer"
|
||||
edit: "Rediger"
|
||||
delete: "Slet"
|
||||
recover: "Fortryd sletning af forespørgsel"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
show_table: "Tabel"
|
||||
show_graph: "Graf"
|
||||
query_name: "Forespørgsel"
|
||||
query_groups: "grupper"
|
||||
query_description: "Beskrivelse"
|
||||
query_user: "Oprettet af"
|
||||
reset_params: "Nulstil"
|
||||
search_placeholder: "Søg..."
|
||||
group:
|
||||
reports: "Rapporter"
|
118
plugins/discourse-data-explorer/config/locales/client.de.yml
Normal file
118
plugins/discourse-data-explorer/config/locales/client.de.yml
Normal file
|
@ -0,0 +1,118 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
de:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "Entferne die Semikolons aus der Anfrage."
|
||||
dirty: "Du musst die Anfrage vor der Ausführung speichern."
|
||||
explorer:
|
||||
or: "oder"
|
||||
admins_only: "Der Daten-Explorer ist nur für Administratoren verfügbar."
|
||||
allow_groups: "Gruppen erlauben, auf diese Anfrage zuzugreifen"
|
||||
title: "Daten-Explorer"
|
||||
create: "Neu erstellen"
|
||||
create_placeholder: "Name der Anfrage …"
|
||||
description_placeholder: "Hier bitte eine Beschreibung eingeben"
|
||||
import:
|
||||
label: "Importieren"
|
||||
modal: "Anfrage importieren"
|
||||
unparseable_json: "JSON-Datei kann nicht verarbeitet werden."
|
||||
wrong_json: "Falsche JSON-Datei. Eine JSON-Datei sollte ein „query“-Objekt enthalten, das zumindest eine „sql“-Eigenschaft haben sollte."
|
||||
help:
|
||||
label: "Hilfe"
|
||||
modal_title: "Daten-Explorer-Hilfe"
|
||||
auto_resolution: "<h2>Automatische Auflösung von Entitäten</h2> <p>Wenn deine Anfrage eine Entitäts-ID zurückgibt, kann der Daten-Explorer diese automatisch durch den Entitätsnamen und andere nützliche Informationen in den Anfrageergebnissen ersetzen. Die automatische Auflösung ist verfügbar für <i><b>user_id</b></i>, <i><b>group_id</b></i>, <i><b>topic_id</b></i>, <i><b>category_id</b></i> und <i><b>badge_id</b></i>. Um dies auszuprobieren, führe diese Anfrage aus:</p> <pre><code>SELECT user_id\nFROM posts</code></pre>"
|
||||
custom_params: "<h2>Benutzerdefinierte Parameter erstellen</h2> <p>Um benutzerdefinierte Parameter für deine Anfragen zu erstellen, füge Folgendes am Anfang deiner Anfrage ein und befolge das Format:</p> <pre><code>-- [params]\n-- int :num = 1\n\nSELECT :num</code></pre> <p><i>Hinweis: Die erste Zeile mit [params] ist erforderlich, zusammen mit zwei Bindestrichen davor und jedem benutzerdefinierten Parameter, den du deklarieren möchtest.</i></p>"
|
||||
default_values: "<h3>Standardwerte</h3> <p>Du kannst Parameter mit oder ohne Standardwerte deklarieren. Die Standardwerte werden in einem Textfeld unter dem Anfrage-Editor angezeigt, das du beliebig bearbeiten kannst. Bei Parametern, die ohne Standardwerte angegeben werden, erscheint zwar immer noch ein Textfeld, aber es ist leer und rot markiert.</p> <pre><code>-- [params]\n-- text :username = my_username\n-- int :age</code></pre>"
|
||||
data_types: "<h3>Datentypen</h3> <p>Hier sind gängige Datentypen, die du verwenden kannst:</p> <ul> <li><b>Ganzzahl</b> - vorzeichenbehaftete Vier-Byte-Ganzzahl</li> <li><b>Text</b> - Zeichenkette mit variabler Länge</li> <li><b>Boolean</b> – wahr/falsch</li> <li><b>Datum</b> - Kalenderdatum (Jahr, Monat, Tag)</li> </ul> <p>Weitere Informationen über Datentypen findest du auf <a href='http://www.postgresql.org/docs/9.3/static/datatype.html#DATATYPE-TABLE' target='_blank'>dieser Website</a>.</p>"
|
||||
schema:
|
||||
title: "Datenbank-Schema"
|
||||
filter: "Suchen …"
|
||||
sensitive: "Die Inhalte dieser Spalte enthalten möglicherweise besonders sensible oder persönliche Informationen. Bitte gehe behutsam mit den Inhalten dieser Spalte um."
|
||||
types:
|
||||
bool:
|
||||
yes: "Ja"
|
||||
no: "Nein"
|
||||
null_: "Null"
|
||||
export: "Exportieren"
|
||||
view_json: "JSON anzeigen"
|
||||
save: "Änderungen speichern"
|
||||
saverun: "Änderungen speichern und ausführen"
|
||||
run: "Ausführen"
|
||||
undo: "Änderungen verwerfen"
|
||||
edit: "Bearbeiten"
|
||||
delete: "Löschen"
|
||||
recover: "Anfrage wiederherstellen"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
show_table: "Tabelle"
|
||||
show_graph: "Diagramm"
|
||||
others_dirty: "Eine Anfrage hat nicht gespeicherte Änderungen, die verloren gehen, wenn du woanders hin navigierst."
|
||||
run_time: "Anfrage in %{value} ms abgeschlossen."
|
||||
result_count:
|
||||
one: "%{count} Ergebnis."
|
||||
other: "%{count} Ergebnisse."
|
||||
max_result_count:
|
||||
one: "Zeige das Top-%{count} Ergebnis."
|
||||
other: "Zeige die Top-%{count} Ergebnisse."
|
||||
query_name: "Anfrage"
|
||||
query_groups: "Gruppen"
|
||||
link: "Link für"
|
||||
report_name: "Bericht"
|
||||
query_description: "Beschreibung"
|
||||
query_time: "Letzter Lauf"
|
||||
query_user: "Erstellt von"
|
||||
column: "Spalte %{number}"
|
||||
explain_label: "Anfrageplan einbeziehen?"
|
||||
save_params: "Standards festlegen"
|
||||
reset_params: "Zurücksetzen"
|
||||
search_placeholder: "Suchen …"
|
||||
no_search_results: "Leider konnten wir keine Ergebnisse finden, die deinem Text entsprechen."
|
||||
form:
|
||||
errors:
|
||||
invalid: "Ungültig"
|
||||
no_such_category: "Keine solche Kategorie"
|
||||
no_such_group: "Keine solche Gruppe"
|
||||
invalid_date: "%{date} ist ein ungültiges Datum"
|
||||
invalid_time: "%{time} ist eine ungültige Zeit"
|
||||
group:
|
||||
reports: "Berichte"
|
||||
admin:
|
||||
api:
|
||||
scopes:
|
||||
descriptions:
|
||||
discourse_data_explorer:
|
||||
run_queries: "Daten-Explorer-Anfragen ausführen. Schränke den API-Schlüssel auf eine Reihe von Anfragen ein, indem du Anfrage-IDs angibst."
|
||||
discourse_automation:
|
||||
scriptables:
|
||||
recurring_data_explorer_result_pm:
|
||||
fields:
|
||||
recipients:
|
||||
label: An Benutzer, Gruppe oder E-Mail senden
|
||||
query_id:
|
||||
label: Daten-Explorer-Anfrage
|
||||
query_params:
|
||||
label: Daten-Explorer-Anfrageparameter
|
||||
skip_empty:
|
||||
label: Überspringe das Senden einer PN, wenn es keine Ergebnisse gibt
|
||||
attach_csv:
|
||||
label: Hänge die CSV-Datei an die PN an
|
||||
users_from_group:
|
||||
label: Senden jedem Gruppenmitglied eine individuelle PN
|
||||
recurring_data_explorer_result_topic:
|
||||
fields:
|
||||
topic_id:
|
||||
label: Das Thema, in dem die Anfrageergebnisse veröffentlicht werden
|
||||
query_id:
|
||||
label: Daten-Explorer-Anfrage
|
||||
query_params:
|
||||
label: Daten-Explorer-Anfrageparameter
|
||||
skip_empty:
|
||||
label: Überspringe die Veröffentlichung, wenn keine Ergebnisse vorliegen
|
||||
attach_csv:
|
||||
label: Hänge die CSV-Datei an den Beitrag an
|
31
plugins/discourse-data-explorer/config/locales/client.el.yml
Normal file
31
plugins/discourse-data-explorer/config/locales/client.el.yml
Normal file
|
@ -0,0 +1,31 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
el:
|
||||
js:
|
||||
explorer:
|
||||
or: "ή"
|
||||
import:
|
||||
label: "Εισαγωγή"
|
||||
help:
|
||||
label: "Βοήθεια"
|
||||
schema:
|
||||
filter: "Αναζήτηση..."
|
||||
types:
|
||||
bool:
|
||||
yes: "Ναι "
|
||||
no: "Όχι"
|
||||
export: "Εξαγωγή"
|
||||
save: "Αποθήκευση Αλλαγών"
|
||||
edit: "Επεξεργασία"
|
||||
delete: "Σβήσιμο"
|
||||
query_groups: "Ομάδες"
|
||||
query_description: "Περιγραφή"
|
||||
query_user: "Δημιουργήθηκε από"
|
||||
reset_params: "Επαναφορά"
|
||||
search_placeholder: "Αναζήτηση..."
|
||||
group:
|
||||
reports: "Αναφορές"
|
134
plugins/discourse-data-explorer/config/locales/client.en.yml
Normal file
134
plugins/discourse-data-explorer/config/locales/client.en.yml
Normal file
|
@ -0,0 +1,134 @@
|
|||
en:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "Remove the semicolons from the query."
|
||||
dirty: "You must save the query before running."
|
||||
explorer:
|
||||
or: "or"
|
||||
admins_only: "The data explorer is only available to admins."
|
||||
allow_groups: "Allow groups to access this query"
|
||||
title: "Data Explorer"
|
||||
create: "Create New"
|
||||
create_placeholder: "Query name..."
|
||||
description_placeholder: "Enter a description here"
|
||||
import:
|
||||
label: "Import"
|
||||
modal: "Import A Query"
|
||||
unparseable_json: "Unparseable JSON file."
|
||||
wrong_json: "Wrong JSON file. A JSON file should contain a 'query' object, which should at least have an 'sql' property."
|
||||
help:
|
||||
label: "Help"
|
||||
modal_title: "Data Explorer Help"
|
||||
auto_resolution: "<h2>Automatic Entity Resolution</h2>
|
||||
<p>When your query returns an entity id, Data Explorer may automatically substitute it with
|
||||
the entity name and other useful information in query results. Automatic resolution is available for
|
||||
<i><b>user_id</b></i>, <i><b>group_id</b></i>, <i><b>topic_id</b></i>, <i><b>category_id</b></i>
|
||||
and <i><b>badge_id</b></i>. To try this out run this query:</p>
|
||||
<pre><code>SELECT user_id\nFROM posts</code></pre>"
|
||||
custom_params: "<h2>Creating Custom Parameters</h2>
|
||||
<p>To create custom parameters for your queries, put this at the top of your query and follow the format:</p>
|
||||
<pre><code>-- [params]\n-- int :num = 1\n\nSELECT :num</code></pre>
|
||||
<p><i>Note: the first line with [params] is required, along with two dashes preceding it and every
|
||||
custom parameter you want to declare.</i></p>"
|
||||
default_values: "<h3>Default Values</h3>
|
||||
<p>You can declare parameters with or without default values. Default values will show up in a text field
|
||||
below the query editor, which you can edit to your needs. Parameters declared without default values will
|
||||
still generate a text field, but will be empty and highlighted red.</p>
|
||||
<pre><code>-- [params]\n-- text :username = my_username\n-- int :age</code></pre>"
|
||||
data_types: "<h3>Data Types</h3>
|
||||
<p>Here are common data types you can use:</p>
|
||||
<ul>
|
||||
<li><b>integer</b> - signed four-byte Integer</li>
|
||||
<li><b>text</b> - variable-length character string</li>
|
||||
<li><b>boolean</b> – true/false</li>
|
||||
<li><b>date</b> - calendar date (year, month, day)</li>
|
||||
</ul>
|
||||
<p>For more information on data types, visit
|
||||
<a href='http://www.postgresql.org/docs/9.3/static/datatype.html#DATATYPE-TABLE' target='_blank'>this website</a>.</p>"
|
||||
schema:
|
||||
title: "Database Schema"
|
||||
filter: "Search..."
|
||||
sensitive: "The contents of this column may contain particularly sensitive or private information. Please exercise caution when using the contents of this column."
|
||||
types:
|
||||
bool:
|
||||
yes: "Yes"
|
||||
no: "No"
|
||||
null_: "Null"
|
||||
export: "Export"
|
||||
view_json: "View JSON"
|
||||
save: "Save Changes"
|
||||
saverun: "Save Changes and Run"
|
||||
run: "Run"
|
||||
undo: "Discard Changes"
|
||||
edit: "Edit"
|
||||
delete: "Delete"
|
||||
recover: "Undelete Query"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
show_table: "Table"
|
||||
show_graph: "Graph"
|
||||
others_dirty: "A query has unsaved changes that will be lost if you navigate away."
|
||||
run_time: "Query completed in %{value} ms."
|
||||
result_count:
|
||||
one: "%{count} result."
|
||||
other: "%{count} results."
|
||||
max_result_count:
|
||||
one: "Showing top %{count} result."
|
||||
other: "Showing top %{count} results."
|
||||
query_name: "Query"
|
||||
query_groups: "Groups"
|
||||
link: "Link for"
|
||||
report_name: "Report"
|
||||
query_description: "Description"
|
||||
query_time: "Last run"
|
||||
query_user: "Created by"
|
||||
column: "Column %{number}"
|
||||
explain_label: "Include query plan?"
|
||||
save_params: "Set Defaults"
|
||||
reset_params: "Reset"
|
||||
search_placeholder: "Search..."
|
||||
no_search_results: "Sorry, we couldn't find any results matching your text."
|
||||
form:
|
||||
errors:
|
||||
invalid: "Invalid"
|
||||
no_such_category: "No such category"
|
||||
no_such_group: "No such group"
|
||||
invalid_date: "%{date} is a invalid date"
|
||||
invalid_time: "%{time} is a invalid time"
|
||||
group:
|
||||
reports: "Reports"
|
||||
admin:
|
||||
api:
|
||||
scopes:
|
||||
descriptions:
|
||||
discourse_data_explorer:
|
||||
run_queries: "Run Data Explorer queries. Restrict the API key to a set of queries by specifying queries IDs."
|
||||
discourse_automation:
|
||||
scriptables:
|
||||
recurring_data_explorer_result_pm:
|
||||
fields:
|
||||
recipients:
|
||||
label: Send to User, Group or Email
|
||||
query_id:
|
||||
label: Data Explorer Query
|
||||
query_params:
|
||||
label: Data Explorer Query parameters
|
||||
skip_empty:
|
||||
label: Skip sending PM if there are no results
|
||||
attach_csv:
|
||||
label: Attach the CSV file to the PM
|
||||
users_from_group:
|
||||
label: Send individual PM to each group member
|
||||
recurring_data_explorer_result_topic:
|
||||
fields:
|
||||
topic_id:
|
||||
label: The topic to post query results in
|
||||
query_id:
|
||||
label: Data Explorer Query
|
||||
query_params:
|
||||
label: Data Explorer Query parameters
|
||||
skip_empty:
|
||||
label: Skip posting if there are no results
|
||||
attach_csv:
|
||||
label: Attach the CSV file to the post
|
|
@ -0,0 +1,10 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
en_GB:
|
||||
js:
|
||||
explorer:
|
||||
query_description: "Description"
|
116
plugins/discourse-data-explorer/config/locales/client.es.yml
Normal file
116
plugins/discourse-data-explorer/config/locales/client.es.yml
Normal file
|
@ -0,0 +1,116 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
es:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "Eliminar los punto y coma de la consulta."
|
||||
dirty: "Debes guardar la consulta antes de ejecutarla."
|
||||
explorer:
|
||||
or: "o"
|
||||
admins_only: "El explorador de datos solo esta disponible para administradores."
|
||||
allow_groups: "Permitir a los grupos acceder a esta consulta"
|
||||
title: "Explorador de datos"
|
||||
create: "Crear nueva"
|
||||
create_placeholder: "Nombre de la consulta..."
|
||||
description_placeholder: "Escribe una descripción aquí"
|
||||
import:
|
||||
label: "Importar"
|
||||
modal: "Importar una consulta"
|
||||
unparseable_json: "Archivo JSON no analizable."
|
||||
wrong_json: "Archivo JSON incorrecto. Debería contener un objeto «query», con al menos una propiedad «sql»."
|
||||
help:
|
||||
label: "Ayuda"
|
||||
modal_title: "Ayuda sobre el explorador de datos"
|
||||
auto_resolution: "<h2>Resolución automática de entidades</h2> <p>Cuando tu consulta devuelva el ID de una entidad, puede que el explorador de datos lo sustituya por el nombre de la entidad y otra información útil en los resultados de la consulta. La resolución automática está disponible para <i><b>user_id</b></i>, <i><b>group_id</b></i>, <i><b>topic_id</b></i>, <i><b>category_id</b></i> y <i><b>badge_id</b></i>. Para probarla, ejecuta esta consulta:</p> <pre><code>SELECT user_id\nFROM posts</code></pre>"
|
||||
custom_params: "<h2>Crear parámetros personalizados</h2><p>Para crear parámetros personalizados en tus consultas, pon lo siguiente al principio siguiendo el formato:</p><pre><code>-- [params]\n-- int :num = 1\n\nSELECT :num</code></pre> <p><i>Nota: hay que incluir la primera línea con [params], además de los dos guiones que van al principio de la línea, y también debes incluir los guiones en cada parámetro personalizado que quieras declarar.</i></p>"
|
||||
default_values: "<h3>Valores por defecto</h3> <p>Puedes declarar parámetros de manera que tengan o no un valor por defecto. Los valores por defecto aparecerán en un campo de texto debajo del editor de consultas, y puedes editarlos según lo necesites. Los parámetros declarados sin un valor por defecto también crearán un campo de texto, pero estará vacío y marcado en rojo.</p><pre><code>-- [params]\n-- text :username = my_username\n-- int :age</code></pre>"
|
||||
data_types: "<h3>Tipos de datos</h3> <p>Algunos tipos de datos que puedes usar:</p> <ul> <li><b>integer</b> - número entero de 4 bytes, positivo o negativo</li><li><b>text</b> - conjunto de caracteres de longitud variable</li><li><b>boolean</b> - true/false (verdadero/falso)</li><li><b>date</b> - date (año, mes, día)</li></ul><p>Para más información sobre los tipos de datos, visita <a href='http://www.postgresql.org/docs/9.3/static/datatype.html#DATATYPE-TABLE' target='_blank'>esta página</a>.</p>"
|
||||
schema:
|
||||
title: "Esquema de base de datos"
|
||||
filter: "Buscar..."
|
||||
sensitive: "Los contenidos de esta columna pueden incluir información particularmente sensible o privada. Ten cuidado cuando la uses."
|
||||
types:
|
||||
bool:
|
||||
yes: "Sí"
|
||||
no: "No"
|
||||
null_: "Nulo"
|
||||
export: "Exportar"
|
||||
view_json: "Ver JSON"
|
||||
save: "Guardar cambios"
|
||||
saverun: "Guardar cambios y ejecutar"
|
||||
run: "Ejecutar"
|
||||
undo: "Descartar cambios"
|
||||
edit: "Editar"
|
||||
delete: "Eliminar"
|
||||
recover: "Recuperar consulta"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
show_table: "Tabla"
|
||||
show_graph: "Gráfico"
|
||||
others_dirty: "Una consulta tiene cambios no guardados que se perderán si sales."
|
||||
run_time: "Consulta completada en %{value} ms."
|
||||
result_count:
|
||||
one: "%{count} resultado."
|
||||
other: "%{count} resultados."
|
||||
max_result_count:
|
||||
one: "Mostrando el mejor resultado (%{count})."
|
||||
other: "Mostrando los mejores %{count} resultados."
|
||||
query_name: "Consulta"
|
||||
query_groups: "Grupos"
|
||||
link: "Enlace para"
|
||||
report_name: "Informe"
|
||||
query_description: "Descripción"
|
||||
query_time: "Última ejecución"
|
||||
query_user: "Creado por"
|
||||
column: "Columna %{number}"
|
||||
explain_label: "¿Incluir plan de consulta?"
|
||||
save_params: "Establecer configuración por defecto"
|
||||
reset_params: "Restablecer"
|
||||
search_placeholder: "Buscar..."
|
||||
no_search_results: "Lo sentimos, no hemos podido encontrar ningún resultado que coincida con tu texto."
|
||||
form:
|
||||
errors:
|
||||
invalid: "No válido"
|
||||
no_such_category: "No existe dicha categoria"
|
||||
no_such_group: "No existe dicho grupo"
|
||||
invalid_date: "%{date} es una fecha no válida"
|
||||
invalid_time: "%{time} es una hora no válida"
|
||||
group:
|
||||
reports: "Informes"
|
||||
admin:
|
||||
api:
|
||||
scopes:
|
||||
descriptions:
|
||||
discourse_data_explorer:
|
||||
run_queries: "Ejecuta consultas del Explorador de Datos. Restringe la clave API a un conjunto de consultas especificando los ID de las mismas."
|
||||
discourse_automation:
|
||||
scriptables:
|
||||
recurring_data_explorer_result_pm:
|
||||
fields:
|
||||
recipients:
|
||||
label: Enviar a usuario, grupo o correo electrónico
|
||||
query_id:
|
||||
label: Consulta del explorador de datos
|
||||
query_params:
|
||||
label: Parámetros de consulta del explorador de datos
|
||||
skip_empty:
|
||||
label: Omitir el envío de MP si no hay resultados
|
||||
attach_csv:
|
||||
label: Adjuntar el archivo CSV al MP
|
||||
recurring_data_explorer_result_topic:
|
||||
fields:
|
||||
topic_id:
|
||||
label: El tema en el que se publicarán los resultados de la consulta
|
||||
query_id:
|
||||
label: Consulta del Explorador de datos
|
||||
query_params:
|
||||
label: Parámetros de consulta del Explorador de datos
|
||||
skip_empty:
|
||||
label: Omitir la publicación si no hay resultados
|
||||
attach_csv:
|
||||
label: Adjuntar el archivo CSV a la publicación
|
40
plugins/discourse-data-explorer/config/locales/client.et.yml
Normal file
40
plugins/discourse-data-explorer/config/locales/client.et.yml
Normal file
|
@ -0,0 +1,40 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
et:
|
||||
js:
|
||||
explorer:
|
||||
or: "või"
|
||||
title: "Andmete eksportija"
|
||||
create: "Loo uus"
|
||||
create_placeholder: "Päringu nimi..."
|
||||
import:
|
||||
label: "Impordi"
|
||||
modal: "Impordi päring"
|
||||
help:
|
||||
label: "Abi"
|
||||
schema:
|
||||
title: "Andmebaasi skeem"
|
||||
filter: "Otsi..."
|
||||
types:
|
||||
bool:
|
||||
yes: "Jah"
|
||||
no: "Ei"
|
||||
null_: "Nulli"
|
||||
export: "Ekspordi"
|
||||
save: "Salvesta muudatused"
|
||||
saverun: "Salvesta muudatused ja käivita"
|
||||
run: "Käivita"
|
||||
undo: "Loobu muudatustest"
|
||||
edit: "Muuda"
|
||||
delete: "Kustuta"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
query_groups: "Grupid"
|
||||
query_description: "Kirjeldus"
|
||||
save_params: "Määra vaikeväärtused"
|
||||
reset_params: "Nulli"
|
||||
search_placeholder: "Otsi..."
|
|
@ -0,0 +1,41 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
fa_IR:
|
||||
js:
|
||||
explorer:
|
||||
or: "یا"
|
||||
import:
|
||||
label: "ورود دادهها"
|
||||
help:
|
||||
label: "کمک"
|
||||
schema:
|
||||
filter: "جستجو..."
|
||||
types:
|
||||
bool:
|
||||
yes: "بله"
|
||||
no: "خیر"
|
||||
export: "خروجی گرفتن"
|
||||
save: "ذخیره تغییرات"
|
||||
run: "اجرا"
|
||||
edit: "ویرایش"
|
||||
delete: "حذف"
|
||||
result_count:
|
||||
one: "%{count} نتیجه."
|
||||
other: "%{count} نتیجه."
|
||||
max_result_count:
|
||||
one: "نمایش %{count} نتیجه برتر."
|
||||
other: "نمایش %{count} نتیجه برتر."
|
||||
query_groups: "گروه ها"
|
||||
link: "پیوند برای"
|
||||
report_name: "گزارش"
|
||||
query_description: "توضیح"
|
||||
query_time: "آخرین اجرا"
|
||||
query_user: "ایجاد شده توسط"
|
||||
reset_params: "بازنشانی"
|
||||
search_placeholder: "جستجو..."
|
||||
group:
|
||||
reports: "گزارشات"
|
116
plugins/discourse-data-explorer/config/locales/client.fi.yml
Normal file
116
plugins/discourse-data-explorer/config/locales/client.fi.yml
Normal file
|
@ -0,0 +1,116 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
fi:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "Poista puolipisteet kyselystä."
|
||||
dirty: "Kysely täytyy tallentaa ennen suorittamista."
|
||||
explorer:
|
||||
or: "tai"
|
||||
admins_only: "Dataselain on vain ylläpitäjille."
|
||||
allow_groups: "Salli ryhmien käyttää tätä kyselyä"
|
||||
title: "Dataselain"
|
||||
create: "Luo uusi"
|
||||
create_placeholder: "Kyselyn nimi..."
|
||||
description_placeholder: "Kirjoita kuvaus tähän"
|
||||
import:
|
||||
label: "Tuo"
|
||||
modal: "Tuo kysely"
|
||||
unparseable_json: "JSON-tiedosto ei ole jäsennettävissä."
|
||||
wrong_json: "Väärä JSON-tiedosto. JSON-tiedoston tulisi sisältää query-objekti, jolla tulisi olla ainakin sql-ominaisuus."
|
||||
help:
|
||||
label: "Apua"
|
||||
modal_title: "Dataselaimen ohje"
|
||||
auto_resolution: "<h2>Automaattinen yksikön selvitys</h2> <p>Kun kyselysi palauttaa yksikön, Dataselain voi korvata sen automaattisesti yksikön nimellä ja muilla hyödyllisillä tiedoilla kyselyn tuloksissa. Automaattinen selvitys on käytettävissä <i><b>user_id</b></i>-, <i><b>group_id</b></i>-, <i><b>topic_id</b></i>-, <i><b>category_id</b></i>- ja <i><b>badge_id</b></i>-tietueissa. Voit kokeilla tätä suorittamalla tämän kyselyn:</p> <pre><code>SELECT user_id\nFROM posts</code></pre>"
|
||||
custom_params: "<h2>Mukautettujen parametrien luominen</h2> <p>Voit luoda mukautettuja parametreja kyselyillesi lisäämällä tämän kyselysi yläosaan ja käyttämällä muotoa:</p> <pre><code>-- [params]\n-- int :num = 1\n\nSELECT :num</code></pre> <p><i>Huomautus: ensimmäinen rivi, jossa on [params], on pakollinen yhdessä sitä edeltävien väliviivojen kanssa, sekä jokainen mukautettu parametri, jonka haluat esitellä.</i></p>"
|
||||
default_values: "<h3>Oletusarvot</h3> <p>Voit esitellä parametreja oletusarvoilla tai ilman. Oletusarvot näkyvät tekstikentässä kyselyeditorin alla, ja voit muokata niitä tarpeidesi mukaan. Ilman oletusarvoja esitellyt parametrit luovat tekstikentän, mutta ne ovat tyhjiä ja korostettu punaisena.</p> <pre><code>-- [params]\n-- text :username = my_username\n-- int :age</code></pre>"
|
||||
data_types: "<h3>Datatyyppejä</h3> <p>Nämä ovat yleisiä datatyyppejä, joita voit käyttää:</p> <ul> <li><b>kokonaisluku</b> – etumerkillinen nelitavuinen kokonaisluku</li> <li><b>teksti</b> – vaihtelevanpituinen merkkijono</li> <li><b>totuusarvo</b> – tosi/epätosi</li> <li><b>päivämäärä</b> – kalenteripäivä (vuosi, kuukausi, päivä)</li> </ul> <p>Lisätietoja datatyypeistä saat <a href='http://www.postgresql.org/docs/9.3/static/datatype.html#DATATYPE-TABLE' target='_blank'>tältä verkkosivustolta</a>.</p>"
|
||||
schema:
|
||||
title: "Tietokannan rakenne"
|
||||
filter: "Hae..."
|
||||
sensitive: "Tämän sarakkeen sisältö voi sisältää erityisen arkaluontoista tai yksityistä tietoa. Noudata erityistä harkintaa käyttäessäsi sarakkeen sisältämää tietoa."
|
||||
types:
|
||||
bool:
|
||||
yes: "Kyllä"
|
||||
no: "Ei"
|
||||
null_: "Tyhjä"
|
||||
export: "Vie"
|
||||
view_json: "Näytä JSON"
|
||||
save: "Tallenna muutokset"
|
||||
saverun: "Tallenna muutokset ja suorita"
|
||||
run: "Suorita"
|
||||
undo: "Kumoa muutokset"
|
||||
edit: "Muokkaa"
|
||||
delete: "Poista"
|
||||
recover: "Palauta poistettu kysely"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
show_table: "Taulukko"
|
||||
show_graph: "Kaavio"
|
||||
others_dirty: "Kyselyyn tehdyt tallentamattomat muutokset menetetään, jos navigoit pois."
|
||||
run_time: "Kysely suoritettu %{value} millisekunnissa."
|
||||
result_count:
|
||||
one: "%{count} tulos."
|
||||
other: "%{count} tulosta."
|
||||
max_result_count:
|
||||
one: "Näytetään osuvin %{count} tulos."
|
||||
other: "Näytetään osuvimmat %{count} tulosta."
|
||||
query_name: "Kysely"
|
||||
query_groups: "Ryhmät"
|
||||
link: "Linkki:"
|
||||
report_name: "Raportti"
|
||||
query_description: "Kuvaus"
|
||||
query_time: "Viimeksi suoritettu"
|
||||
query_user: "Luonut"
|
||||
column: "Sarake %{number}"
|
||||
explain_label: "Sisällytä kyselysuunnitelma?"
|
||||
save_params: "Aseta oletusarvot"
|
||||
reset_params: "Nollaa"
|
||||
search_placeholder: "Hae..."
|
||||
no_search_results: "Pahoittelut, emme löytäneet tekstiäsi vastaavia tuloksia."
|
||||
form:
|
||||
errors:
|
||||
invalid: "Virheellinen"
|
||||
no_such_category: "Tällaista luokkaa ei ole"
|
||||
no_such_group: "Tällaista ryhmää ei ole"
|
||||
invalid_date: "%{date} on virheellinen päivämäärä"
|
||||
invalid_time: "%{time} on virheellinen aika"
|
||||
group:
|
||||
reports: "Raportit"
|
||||
admin:
|
||||
api:
|
||||
scopes:
|
||||
descriptions:
|
||||
discourse_data_explorer:
|
||||
run_queries: "Suorita Dataselaimen kyselyitä. Rajoita API-avain tiettyihin kyselyihin määrittämällä kyselyiden tunnuksia."
|
||||
discourse_automation:
|
||||
scriptables:
|
||||
recurring_data_explorer_result_pm:
|
||||
fields:
|
||||
recipients:
|
||||
label: Lähetä käyttäjälle, ryhmälle tai sähköpostiosoitteeseen
|
||||
query_id:
|
||||
label: Dataselaimen kysely
|
||||
query_params:
|
||||
label: Dataselaimen kyselyparametrit
|
||||
skip_empty:
|
||||
label: Ohita yksityisviestin lähettäminen, jos tuloksia ei ole
|
||||
attach_csv:
|
||||
label: Liitä CSV-tiedosto yksityisviestiin
|
||||
recurring_data_explorer_result_topic:
|
||||
fields:
|
||||
topic_id:
|
||||
label: Ketju, johon kyselyn tulokset lähetetään
|
||||
query_id:
|
||||
label: Dataselaimen kysely
|
||||
query_params:
|
||||
label: Dataselaimen kyselyparametrit
|
||||
skip_empty:
|
||||
label: Ohita viestin lähettäminen, jos tuloksia ei ole
|
||||
attach_csv:
|
||||
label: Liitä CSV-tiedosto viestiin
|
116
plugins/discourse-data-explorer/config/locales/client.fr.yml
Normal file
116
plugins/discourse-data-explorer/config/locales/client.fr.yml
Normal file
|
@ -0,0 +1,116 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
fr:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "Supprimez les points-virgules de la requête."
|
||||
dirty: "Vous devez enregistrer la requête avant de l'exécuter."
|
||||
explorer:
|
||||
or: "ou"
|
||||
admins_only: "L'explorateur de données n'est disponible que pour les administrateurs."
|
||||
allow_groups: "Autoriser les groupes à accéder à cette requête"
|
||||
title: "Explorateur de données"
|
||||
create: "Nouvelle requête"
|
||||
create_placeholder: "Nom de la requête…"
|
||||
description_placeholder: "Saisissez une description ici"
|
||||
import:
|
||||
label: "Importer"
|
||||
modal: "Importer une requête"
|
||||
unparseable_json: "Fichier JSON non analysable."
|
||||
wrong_json: "Fichier JSON incorrect. Un fichier JSON doit contenir un objet « query », qui doit au moins avoir une propriété « sql »."
|
||||
help:
|
||||
label: "Aide"
|
||||
modal_title: "Aide de l'explorateur de données"
|
||||
auto_resolution: "<h2>Résolution d'entité automatique</h2> <p>Lorsque votre requête renvoie un identifiant d'entité, l'explorateur de données peut le remplacer automatiquement par le nom de l'entité et d'autres informations utiles dans les résultats de la requête. La résolution automatique est disponible pour <i><b>user_id</b></i>, <i><b>group_id</b></i>, <i><b>topic_id</b></i>, <i><b>category_id</b></i> et <i><b>badge_id</b></i>. Pour essayer cette fonction, exécutez cette requête :</p> <pre><code>SELECT user_id\nFROM posts</code></pre>"
|
||||
custom_params: "<h2>Création de paramètres personnalisés</h2> <p>Pour créer des paramètres personnalisés pour vos requêtes, placez ceci en haut de votre requête et respectez le format :</p> <pre><code>-- [params]\n-- int :num = 1\n\nSELECT :num</code></pre> <p><i>Remarque : la première ligne avec [params] est requise, ainsi que les deux tirets qui la précèdent et chaque paramètre personnalisé que vous souhaitez déclarer.</i></p>"
|
||||
default_values: "<h3>Valeurs par défaut</h3> <p>Vous pouvez déclarer des paramètres avec ou sans valeurs par défaut. Les valeurs par défaut s'afficheront dans un champ de texte sous l'éditeur de requête, que vous pourrez modifier selon vos besoins. Les paramètres déclarés sans valeurs par défaut généreront toujours un champ de texte, mais seront vides et surlignés en rouge.</p> <pre><code>-- [params]\n-- text :username = my_username\n-- int :age</code></pre>"
|
||||
data_types: "<h3>Types de données</h3> <p>Voici les types de données courants que vous pouvez utiliser :</p> <ul> <li><b>integer</b> - entier signé à quatre octets</li> <li><b>text</b> - chaîne de caractères de longueur variable</li> <li><b>boolean</b> - vrai/faux</li> <li><b>date</b> - date calendaire (année, mois, jour)</li> </ul> <p>Pour plus d'informations concernant les types de données, visitez <a href='http://www.postgresql.org/docs/9.3/static/datatype.html#DATATYPE-TABLE' target='_blank'>ce site Web</a>.</p>"
|
||||
schema:
|
||||
title: "Schéma de la base de données"
|
||||
filter: "Recherche…"
|
||||
sensitive: "Le contenu de cette colonne peut contenir des informations particulièrement sensibles ou privées. Veuillez faire preuve de prudence lorsque vous utilisez ce contenu."
|
||||
types:
|
||||
bool:
|
||||
yes: "Oui"
|
||||
no: "Non"
|
||||
null_: "Nul"
|
||||
export: "Exporter"
|
||||
view_json: "Afficher JSON"
|
||||
save: "Enregistrer les modifications"
|
||||
saverun: "Enregistrer les modifications et exécuter"
|
||||
run: "Exécuter"
|
||||
undo: "Annuler les modifications"
|
||||
edit: "Modifier"
|
||||
delete: "Supprimer"
|
||||
recover: "Annuler la suppression de la requête"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
show_table: "Tableau"
|
||||
show_graph: "Graphique"
|
||||
others_dirty: "Une requête comporte des modifications non enregistrées qui seront perdues si vous quittez la page"
|
||||
run_time: "Requête exécutée en %{value} millisecondes."
|
||||
result_count:
|
||||
one: "%{count} résultat."
|
||||
other: "%{count} résultats."
|
||||
max_result_count:
|
||||
one: "Affichage du premier (%{count}) résultat."
|
||||
other: "Affichage des %{count} premiers résultats."
|
||||
query_name: "Requête"
|
||||
query_groups: "Groupes"
|
||||
link: "Lien pour"
|
||||
report_name: "Rapport"
|
||||
query_description: "Description"
|
||||
query_time: "Dernière exécution"
|
||||
query_user: "Créée par"
|
||||
column: "Colonne %{number}"
|
||||
explain_label: "Inclure le plan d'exécution ?"
|
||||
save_params: "Définir les valeurs par défaut"
|
||||
reset_params: "Réinitialiser"
|
||||
search_placeholder: "Recherche…"
|
||||
no_search_results: "Désolé, nous n'avons trouvé aucun résultat correspondant à votre texte."
|
||||
form:
|
||||
errors:
|
||||
invalid: "Invalide"
|
||||
no_such_category: "Aucune catégorie de ce type"
|
||||
no_such_group: "Aucun groupe de ce type"
|
||||
invalid_date: "%{date} est une date invalide"
|
||||
invalid_time: "%{time} est une heure invalide"
|
||||
group:
|
||||
reports: "Rapports"
|
||||
admin:
|
||||
api:
|
||||
scopes:
|
||||
descriptions:
|
||||
discourse_data_explorer:
|
||||
run_queries: "Exécutez des requêtes de l'explorateur de données. Limitez la clé API à un ensemble de requêtes en spécifiant des ID de requêtes."
|
||||
discourse_automation:
|
||||
scriptables:
|
||||
recurring_data_explorer_result_pm:
|
||||
fields:
|
||||
recipients:
|
||||
label: Envoyer à l'utilisateur, au groupe ou à l'adresse e-mail
|
||||
query_id:
|
||||
label: Requête de l'explorateur de données
|
||||
query_params:
|
||||
label: Paramètres de requête de l'explorateur de données
|
||||
skip_empty:
|
||||
label: Ignorer l'envoi d'un MP s'il n'y a aucun résultat
|
||||
attach_csv:
|
||||
label: Joindre le fichier CSV au MD
|
||||
recurring_data_explorer_result_topic:
|
||||
fields:
|
||||
topic_id:
|
||||
label: Le sujet dans lequel les résultats de la requête doivent être affichés
|
||||
query_id:
|
||||
label: Requête de l'explorateur de données
|
||||
query_params:
|
||||
label: Paramètres de requête de l'explorateur de données
|
||||
skip_empty:
|
||||
label: Ignorer la publication s'il n'y a aucun résultat
|
||||
attach_csv:
|
||||
label: Joindre le fichier CSV à la publication
|
31
plugins/discourse-data-explorer/config/locales/client.gl.yml
Normal file
31
plugins/discourse-data-explorer/config/locales/client.gl.yml
Normal file
|
@ -0,0 +1,31 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
gl:
|
||||
js:
|
||||
explorer:
|
||||
or: "ou"
|
||||
import:
|
||||
label: "Importar"
|
||||
help:
|
||||
label: "Axuda"
|
||||
schema:
|
||||
filter: "Buscar..."
|
||||
types:
|
||||
bool:
|
||||
yes: "Si"
|
||||
no: "Non"
|
||||
export: "Exportar"
|
||||
save: "Gardar os cambios"
|
||||
edit: "Editar"
|
||||
delete: "Eliminar"
|
||||
query_groups: "Grupos"
|
||||
query_description: "Descrición"
|
||||
query_user: "Creado por"
|
||||
reset_params: "Restabelecer"
|
||||
search_placeholder: "Buscar..."
|
||||
group:
|
||||
reports: "Informes"
|
122
plugins/discourse-data-explorer/config/locales/client.he.yml
Normal file
122
plugins/discourse-data-explorer/config/locales/client.he.yml
Normal file
|
@ -0,0 +1,122 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
he:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "הסרת הנקודה פסיק מהשאילתה."
|
||||
dirty: "עליך לשמור את השאילתה בטרם ההרצה."
|
||||
explorer:
|
||||
or: "או"
|
||||
admins_only: "דפדפן הנתונים זמין למנהלים בלבד."
|
||||
allow_groups: "לאפשר לקבוצות לגשת לשאילתה הזאת"
|
||||
title: "דפדפן נתונים"
|
||||
create: "יצירת חדשה"
|
||||
create_placeholder: "שם השאילתה…"
|
||||
description_placeholder: "יש להקליד תיאור כאן"
|
||||
import:
|
||||
label: "ייבוא"
|
||||
modal: "ייבוא שאילתה"
|
||||
unparseable_json: "קובץ JSON שאינו ניתן לפענוח."
|
||||
wrong_json: "קובץ JSON שגוי. קובץ JSON צריך להכיל רכיב ‚query’, שצריך לכלול לפחות מאפיין ‚sql’."
|
||||
help:
|
||||
label: "עזרה"
|
||||
modal_title: "עזרה עם דפדפן הנתונים"
|
||||
auto_resolution: "<h2>אבחנת יישות אוטומטית</h2> <p>כאשר השאילתה שלך מחזירה מזהה יישות, סייר הנתונים עשוי להחליף אותו בשם היישות ובפרטים מועילים נוספים אוטומטית בתוצאות השאילתה. אבחנה אוטומטית זמינה עבור <i><b>user_id</b></i>, <i><b>group_id</b></i>, <i><b>topic_id</b></i>, <i><b>category_id</b></i> ו־<i><b>badge_id</b></i>. כדי לנסות זאת יש להריץ את השאילתה הבאה:</p> <pre><code>SELECT user_id\nFROM posts</code></pre>"
|
||||
custom_params: "<h2>יצירת משתנים מותאמים אישית</h2> <p>כדי ליצור משתנים מותאמים אישית לשאילתות שלך, יש להציב זאת בראש השאילתה ולהיצמד לתבנית:</p> <pre><code>-- [params]\n-- int :num = 1\n\nSELECT :num</code></pre> <p><i>לתשומך לבך: השורה הראשונה עם [params] נחוצה, בצירוף שני מינוסים לפניה וכל משתנה מותאם אישית שעליו ברצונך להכריז.</i></p>"
|
||||
default_values: "<h3>ערכי בררת מחדל</h3> <p>ניתן להכריז על משתנים עם או בלי ערכי בררת מחדל. ערכי בררת מחדל יופיעו בשדה טקסט מעל לעורך השאילתות אותם ניתן לערוך בהתאם לצורך. משתנים שמוכרזים ללא ערכי בררת מחדל עדיין ייצרו שדה טקסט אך הם יהיו ריקים ויודגשו באדום.</p> <pre><code>-- [params]\n-- text :username = my_username\n-- int :age</code></pre>"
|
||||
data_types: "<h3>סוגי נתונים</h3> <p>להלן סוגי הנתונים הנפוצים בהם ניתן להשתמש:</p> <ul> <li><b>integer</b> - מספר שלם וחיובי בגודל ארבעה בתים</li> <li><b>text</b> - מחרוזת תווים באורך משתנה</li> <li><b>boolean</b> – אמת/שקר</li> <li><b>date</b> - תאריך בלוח השנה (שנה, חודש, יום)</li> </ul> <p>למידע נוסף על סוגי נתונים ניתן לבקר <a href='http://www.postgresql.org/docs/9.3/static/datatype.html#DATATYPE-TABLE' target='_blank'>באתר הזה</a>.</p>"
|
||||
schema:
|
||||
title: "סכמת מסד הנתונים"
|
||||
filter: "חיפוש…"
|
||||
sensitive: "תכני העמודה הזו עשויים להכיל מידע רגיש או פרטי. נא לנקוט במשנה זהירות בעת השימוש בעמודה זו."
|
||||
types:
|
||||
bool:
|
||||
yes: "כן"
|
||||
no: "לא"
|
||||
null_: "ריק"
|
||||
export: "ייצוא"
|
||||
view_json: "הצגת JSON"
|
||||
save: "שמירת שינויים"
|
||||
saverun: "שמירת השינויים והרצה"
|
||||
run: "הרצה"
|
||||
undo: "התעלמות מהשינויים"
|
||||
edit: "עריכה"
|
||||
delete: "מחיקה"
|
||||
recover: "ביטול מחיקת השאילתה"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
show_table: "טבלה"
|
||||
show_graph: "תרשים"
|
||||
others_dirty: "בשאילתה נערכו שינויים שטרם נשמרו ועשויים ללכת לאיבוד עקב ניווט למקום אחר."
|
||||
run_time: "השאילתה הושלמה תוך %{value} מילישניות."
|
||||
result_count:
|
||||
one: "תוצאה אחת."
|
||||
two: "2 תוצאות."
|
||||
many: "%{count} תוצאות."
|
||||
other: "%{count} תוצאות."
|
||||
max_result_count:
|
||||
one: "התוצאה המובילה מופיעה (%{count})."
|
||||
two: "%{count} התוצאות המובילות מופיעות."
|
||||
many: "%{count} התוצאות המובילות מופיעות."
|
||||
other: "%{count} התוצאות המובילות מופיעות."
|
||||
query_name: "שאילתה"
|
||||
query_groups: "קבוצות"
|
||||
link: "קישור אל"
|
||||
report_name: "דוח"
|
||||
query_description: "תיאור"
|
||||
query_time: "הרצה אחרונה"
|
||||
query_user: "נוצר על ידי"
|
||||
column: "עמודה %{number}"
|
||||
explain_label: "לכלול תכנית שאילתה?"
|
||||
save_params: "הגדרת בררות מחדל"
|
||||
reset_params: "איפוס"
|
||||
search_placeholder: "חיפוש…"
|
||||
no_search_results: "לא הצלחנו למצוא תוצאות שעונות לטקסט שלך, עמך הסליחה."
|
||||
form:
|
||||
errors:
|
||||
invalid: "שגוי"
|
||||
no_such_category: "אין קטגוריה כזו"
|
||||
no_such_group: "אין קבוצה כזו"
|
||||
invalid_date: "%{date} הוא תאריך שגוי"
|
||||
invalid_time: "%{time} היא שעה שגויה"
|
||||
group:
|
||||
reports: "דוחות"
|
||||
admin:
|
||||
api:
|
||||
scopes:
|
||||
descriptions:
|
||||
discourse_data_explorer:
|
||||
run_queries: "הרצת שאילתות של סייר נתונים. ניתן להגביל את מפתח ה־API לסדרה של שאילתות על ידי ציון מזהה שאילתה."
|
||||
discourse_automation:
|
||||
scriptables:
|
||||
recurring_data_explorer_result_pm:
|
||||
fields:
|
||||
recipients:
|
||||
label: שליחה למשתמש, לקבוצה או לדוא״ל
|
||||
query_id:
|
||||
label: שאילתת דפדפן נתונים
|
||||
query_params:
|
||||
label: משתני שאילתת דפדפן נתונים
|
||||
skip_empty:
|
||||
label: דילוג על שליחת הודעה פרטית אם אין תוצאות
|
||||
attach_csv:
|
||||
label: צירוף קובץ ה־CSV להודעה הפרטית
|
||||
users_from_group:
|
||||
label: שליחת הודעה פרטית לכל חבר בקבוצה
|
||||
recurring_data_explorer_result_topic:
|
||||
fields:
|
||||
topic_id:
|
||||
label: הנושא לפרסום תוצאות השאילתה אליו
|
||||
query_id:
|
||||
label: שאילתת דפדפן נתונים
|
||||
query_params:
|
||||
label: משתני שאילתת דפדפן נתונים
|
||||
skip_empty:
|
||||
label: דילוג על פרסום אם אין תוצאות
|
||||
attach_csv:
|
||||
label: צירוף קובץ ה־CSV לפוסט
|
33
plugins/discourse-data-explorer/config/locales/client.hr.yml
Normal file
33
plugins/discourse-data-explorer/config/locales/client.hr.yml
Normal file
|
@ -0,0 +1,33 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
hr:
|
||||
js:
|
||||
explorer:
|
||||
or: "ili"
|
||||
help:
|
||||
label: "Pomoć"
|
||||
schema:
|
||||
filter: "Pretraži..."
|
||||
types:
|
||||
bool:
|
||||
yes: "Da"
|
||||
no: "Ne"
|
||||
export: "Izvoz"
|
||||
save: "Zabilježi promjene"
|
||||
edit: "Izmijeni"
|
||||
delete: "Pobriši"
|
||||
result_count:
|
||||
one: "%{count} rezultat."
|
||||
few: "%{count} rezultata."
|
||||
other: "%{count} rezultata."
|
||||
query_groups: "Grupe"
|
||||
query_description: "Opis"
|
||||
query_user: "Napravio"
|
||||
reset_params: "Resetirati"
|
||||
search_placeholder: "Pretraži..."
|
||||
group:
|
||||
reports: "Izvještaji"
|
73
plugins/discourse-data-explorer/config/locales/client.hu.yml
Normal file
73
plugins/discourse-data-explorer/config/locales/client.hu.yml
Normal file
|
@ -0,0 +1,73 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
hu:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "Pontosvesszők eltávolítása a lekérdezésből."
|
||||
dirty: "Futtatás előtt el kell mentenie a lekérdezést."
|
||||
explorer:
|
||||
or: "vagy"
|
||||
admins_only: "Az adatfelfedező csak az adminok számára érhető el."
|
||||
allow_groups: "Engedélyezés csoportok számára, hogy hozzáférjenek ehhez a lekérdezéshez"
|
||||
title: "Adatfelfedező"
|
||||
create: "Új létrehozása"
|
||||
create_placeholder: "Lekérdezés neve…"
|
||||
description_placeholder: "Adjon meg egy leírást"
|
||||
import:
|
||||
label: "Importálás"
|
||||
modal: "Lekérdezés importálása"
|
||||
unparseable_json: "Nem értelmezhető JSON-fájl."
|
||||
wrong_json: "Hibás JSON-fájl. A JSON-fájlnak kellene tartalmaznia egy „query” objektumot, amelynek legalább egy „sql” tulajdonsággal kell rendelkeznie."
|
||||
help:
|
||||
label: "Súgó"
|
||||
modal_title: "Adatfelfedező súgó"
|
||||
auto_resolution: "<h2>Automatikus entitásfeloldás</h2> <p>Ha egy lekérdezés egy entitásazonosítót ad vissza, akkor az Adatfelfedező automatikusan lecserélheti az entitás nevére és más hasznos információkra a találatokban. Az automatikus feloldás ezeknél érhető el: <i><b>user_id</b></i>, <i><b>group_id</b></i>, <i><b>topic_id</b></i>, <i><b>category_id</b></i> és <i><b>badge_id</b></i>. Próbálja ki ezen lekérdezés futtatásával:</p> <pre><code>SELECT user_id\nFROM posts</code></pre>"
|
||||
custom_params: "<h2>Egyéni paraméterek létrehozása</h2> <p>Hogy egyéni paramétereket hozzon létre a lekérdezéseihez, tegye ezt a lekérdezés fölé, és kövesse a formátumot:</p> <pre><code>-- [params]\n-- int :num = 1\n\nSELECT :num</code></pre> <p><i>Megjegyzés: a [params] sor kötelező, az azt megelőző két kötőjellel együtt, és deklarálnia kell minden egyes egyéni paramétert.</i></p>"
|
||||
default_values: "<h3>Alapértelmezett értékek</h3> <p>Deklarálhatja a paramétereket alapértelmezett értékkel vagy azok nélkül. Az alapértelmezett értékek meg fognak jelenni a szövegmezőben, a lekérdezésszerkesztő alatt, amellyel a céljainak megfelelően szerkesztheti azokat. Az alapértelmezett érték nélküli paraméterek is szövegmezőt fognak kapni, de üresek lesznek, és pirossal lesznek kiemelve..</p> <pre><code>-- [params]\n-- text :username = saját_felhasználónév\n-- int :age</code></pre>"
|
||||
data_types: "<h3>Adattípusok</h3> <p>Itt szerepel néhány gyakori használható adattípus:</p> <ul> <li><b>integer</b> - előjeles négybájtos egész</li> <li><b>text</b> - változó hosszúságú karakterlánc</li> <li><b>boolean</b> – igaz/hamis</li> <li><b>date</b> - naptári dátum (év, hó, nap)</li> </ul> <p>További információkért az adattípusokról, keresse fel <a href='http://www.postgresql.org/docs/9.3/static/datatype.html#DATATYPE-TABLE' target='_blank'>ezt a weboldalt</a>.</p>"
|
||||
schema:
|
||||
title: "Adatbázis-séma"
|
||||
filter: "Keresés…"
|
||||
sensitive: "Az oszlop tartalma különösen érzékeny vagy privát információkat tartalmazhat. Legyen óvatos az oszlop tartalmának használatakor."
|
||||
types:
|
||||
bool:
|
||||
yes: "Igen"
|
||||
no: "Nem"
|
||||
null_: "Null"
|
||||
export: "Exportálás"
|
||||
save: "Módosítások mentése"
|
||||
saverun: "Módosítások mentése és futtatása"
|
||||
run: "Futtatás"
|
||||
undo: "Módosítások elvetése"
|
||||
edit: "Szerkesztés"
|
||||
delete: "Törlés"
|
||||
recover: "Lekérdezés törlésének visszavonása"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
show_table: "Tábla"
|
||||
show_graph: "Grafikon"
|
||||
others_dirty: "A lekérdezés nem mentett módosításokat tartalmaz, amelyek elvesznek, ha elnavigál."
|
||||
run_time: "A lekérdezés %{value} ms alatt fejeződött be."
|
||||
result_count:
|
||||
one: "%{count} találat."
|
||||
other: "%{count} találat."
|
||||
query_name: "Lekérdezés"
|
||||
query_groups: "Csoportok"
|
||||
link: "Hivatkozás ehhez:"
|
||||
report_name: "Jelentés"
|
||||
query_description: "Leírás"
|
||||
query_time: "Utolsó futás"
|
||||
query_user: "Létrehozta:"
|
||||
column: "%{number}. oszlop"
|
||||
explain_label: "Beleveszi a lekérdezési tervet?"
|
||||
save_params: "Alapértékek beállítása"
|
||||
reset_params: "Alaphelyzet"
|
||||
search_placeholder: "Keresés…"
|
||||
no_search_results: "Sajnos nem találtunk a szövegének megfelelő találatot."
|
||||
group:
|
||||
reports: "Jelentések"
|
59
plugins/discourse-data-explorer/config/locales/client.hy.yml
Normal file
59
plugins/discourse-data-explorer/config/locales/client.hy.yml
Normal file
|
@ -0,0 +1,59 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
hy:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "Հեռացրեք կետ-ստորակատերերը հարցումից"
|
||||
dirty: "Դուք պետք է պահպանեք հարցումը գործարկելուց առաջ"
|
||||
explorer:
|
||||
or: "կամ"
|
||||
admins_only: "Տվյալների ուսումնասիրումը հասանելի է միասյն ադմիններին:"
|
||||
title: "Տվյալների Ուսումնասիրում"
|
||||
create: "Ստեղծել Նոր"
|
||||
create_placeholder: "Հարցման անուն..."
|
||||
description_placeholder: "Մուտքագրեք նկարագրություն այստեղ"
|
||||
import:
|
||||
label: "Ներմուծել"
|
||||
modal: "Ներմուծել Հարցում"
|
||||
help:
|
||||
label: "Օգնություն"
|
||||
schema:
|
||||
title: "Տվյալների Բազայի Սխեմա"
|
||||
filter: "Որոնում..."
|
||||
sensitive: "Այս սյունակը կարող է պարունակել մասնակի անձնական կամ անձնական տեղեկություններ: Խնդրում ենք ուշադիր լինել այս սյունակի բովանդակությունն օգտագործելիս:"
|
||||
types:
|
||||
bool:
|
||||
yes: "Այո"
|
||||
no: "Ոչ"
|
||||
null_: "Զրոյական"
|
||||
export: "Արտահանել"
|
||||
save: "Պահպանել Փոփոխությունները"
|
||||
saverun: "Պահպանել փոփոխությունները և Գործարկել"
|
||||
run: "Գործարկել"
|
||||
undo: "Չեղարկել Փոփոխությունները"
|
||||
edit: "Խմբագրել"
|
||||
delete: "Ջնջել"
|
||||
recover: "Վերականգնել Հարցումը"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
others_dirty: "Հարցումն ունի չպահպանված փոփոխություններ, որոնք կկորչեն, եթե Դուք տեղափոխվեք:"
|
||||
result_count:
|
||||
one: "%{count} արդյունք"
|
||||
other: "%{count} արդյունք"
|
||||
query_name: "Հարցում"
|
||||
query_groups: "Խմբեր"
|
||||
query_description: "Նկարագրություն"
|
||||
query_time: "Վերջին գործարկումը"
|
||||
query_user: "Ստեղծել է՝"
|
||||
explain_label: "Ներառե՞լ հարցման պլանը:"
|
||||
save_params: "Սահմանել Լռելյայններ"
|
||||
reset_params: "Վերահաստատե;"
|
||||
search_placeholder: "Որոնում..."
|
||||
no_search_results: "Ներողություն, մենք չկարողացանք գտնել Ձեր տեքստի հետ համընկնող որևէ արդյունք:"
|
||||
group:
|
||||
reports: "Հաշվետվություններ"
|
26
plugins/discourse-data-explorer/config/locales/client.id.yml
Normal file
26
plugins/discourse-data-explorer/config/locales/client.id.yml
Normal file
|
@ -0,0 +1,26 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
id:
|
||||
js:
|
||||
explorer:
|
||||
or: "atau"
|
||||
help:
|
||||
label: "Bantuan"
|
||||
schema:
|
||||
filter: "Cari..."
|
||||
types:
|
||||
bool:
|
||||
yes: "Ya"
|
||||
no: "Tidak"
|
||||
export: "Ekspor"
|
||||
save: "Simpan perubahan"
|
||||
edit: "Ubah"
|
||||
delete: "Hapus"
|
||||
query_groups: "Grup"
|
||||
query_description: "Deskripsi"
|
||||
reset_params: "Reset"
|
||||
search_placeholder: "Cari..."
|
116
plugins/discourse-data-explorer/config/locales/client.it.yml
Normal file
116
plugins/discourse-data-explorer/config/locales/client.it.yml
Normal file
|
@ -0,0 +1,116 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
it:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "Rimuovi i punti e virgola dalla query."
|
||||
dirty: "È necessario salvare la query prima di eseguirla."
|
||||
explorer:
|
||||
or: "oppure"
|
||||
admins_only: "Data explorer è disponibile solo per gli amministratori."
|
||||
allow_groups: "Consenti ai gruppi di accedere a questa query"
|
||||
title: "Data Explorer"
|
||||
create: "Crea Nuovo"
|
||||
create_placeholder: "Nome query..."
|
||||
description_placeholder: "Inserisci una descrizione qui"
|
||||
import:
|
||||
label: "Importa"
|
||||
modal: "Importa una query"
|
||||
unparseable_json: "File JSON non analizzabile."
|
||||
wrong_json: "File JSON errato. Un file JSON dovrebbe contenere un oggetto \"query\" contenente almeno una proprietà \"sql\"."
|
||||
help:
|
||||
label: "Guida"
|
||||
modal_title: "Guida di Data Explorer"
|
||||
auto_resolution: "<h2>Risoluzione automatica dell'entità</h2> <p>Quando la query restituisce l'ID di un'entità, Data Explorer può sostituirla automaticamente con il nome dell'entità e altre informazioni utili nei risultati delle query. La risoluzione automatica è disponibile per i dati <i><b>user_id</b></i>, <i><b>group_id</b></i>, <i><b>topic_id</b></i>, <i><b>category_id</b></i> e <i><b>badge_id</b></i>. Fai una prova, eseguendo questa query:</p> <pre><code>SELECT user_id\nFROM posts</code></pre>"
|
||||
custom_params: "<h2>Creazione di parametri personalizzati</h2> <p>Per creare parametri personalizzati per le query, inserisci questo codice nella parte superiore della query e segui il formato:</p> <pre><code>-- [params]\n-- int :num = 1\n\nSELECT :num</code></pre> <p><i>Nota: la prima riga con [params] è obbligatoria, come i due trattini che precedono e tutti i parametri personalizzati che intendi dichiarare.</i></p>"
|
||||
default_values: "<h3>Valori predefiniti</h3> <p>È possibile dichiarare parametri con o senza valori predefiniti. I valori predefiniti verranno visualizzati in un campo di testo sotto l'editor di query, che può essere modificato in base alle esigenze. I parametri dichiarati senza valori predefiniti genereranno comunque un campo di testo, ma saranno vuoti ed evidenziati in rosso.</p> <pre><code>-- [params]\n-- text :username = my_username\n-- int :age</code></pre>"
|
||||
data_types: "<h3>Tipi di dati</h3> <p>Di seguito sono riportati i tipi di dati comuni che puoi utilizzare:</p> <ul> <li><b>integer</b> - intero a quattro byte con segno</li> <li><b>text</b> - stringa di caratteri di lunghezza variabile</li> <li><b>boolean</b> – valore vero/falso</li> <li><b>data</b> - data di calendario (anno, mese, giorno)</li> </ul> <p>Per ulteriori informazioni sui tipi di dati, visita <a href='http://www.postgresql.org/docs/9.3/static/datatype.html#DATATYPE-TABLE' target='_blank'>questo sito web</a>.</p>"
|
||||
schema:
|
||||
title: "Schema del Database "
|
||||
filter: "Ricerca..."
|
||||
sensitive: "Questa colonna potrebbe contenere dati sensibili o informazioni private. Occorre prestare particolare attenzione nell'uso dei contenuti di questa colonna."
|
||||
types:
|
||||
bool:
|
||||
yes: "Sì"
|
||||
no: "No"
|
||||
null_: "Null"
|
||||
export: "Esporta"
|
||||
view_json: "Visualizza JSON"
|
||||
save: "Salva Modifiche"
|
||||
saverun: "Salva modifiche ed esegui"
|
||||
run: "Esegui"
|
||||
undo: "Annulla Modifiche"
|
||||
edit: "Modifica"
|
||||
delete: "Elimina"
|
||||
recover: "Ripristina Query"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
show_table: "Tabella"
|
||||
show_graph: "Grafico"
|
||||
others_dirty: "Una query ha modifiche non salvate che andranno perse se esci."
|
||||
run_time: "Query completata in %{value} ms."
|
||||
result_count:
|
||||
one: "%{count} risultato."
|
||||
other: "%{count} risultati."
|
||||
max_result_count:
|
||||
one: "Visualizzazione del primo (%{count}) risultato"
|
||||
other: "Visualizzazione dei primi %{count} risultati."
|
||||
query_name: "Query"
|
||||
query_groups: "Gruppi"
|
||||
link: "Collegamento per"
|
||||
report_name: "Rapporto"
|
||||
query_description: "Descrizione"
|
||||
query_time: "Ultima esecuzione"
|
||||
query_user: "Creato da"
|
||||
column: "Colonna %{number}"
|
||||
explain_label: "Includere il piano query?"
|
||||
save_params: "Imposta Predefiniti"
|
||||
reset_params: "Reimposta"
|
||||
search_placeholder: "Ricerca..."
|
||||
no_search_results: "Spiacenti, non abbiamo trovato alcun risultato che corrisponda al tuo testo."
|
||||
form:
|
||||
errors:
|
||||
invalid: "Non valido"
|
||||
no_such_category: "Categoria inesistente"
|
||||
no_such_group: "Gruppo inesistente"
|
||||
invalid_date: "%{date} è una data non valida"
|
||||
invalid_time: "%{time} è un orario non valido"
|
||||
group:
|
||||
reports: "Rapporti"
|
||||
admin:
|
||||
api:
|
||||
scopes:
|
||||
descriptions:
|
||||
discourse_data_explorer:
|
||||
run_queries: "Esegui le query di Data Explorer. Limita la chiave API a un insieme di query specificando i loro ID."
|
||||
discourse_automation:
|
||||
scriptables:
|
||||
recurring_data_explorer_result_pm:
|
||||
fields:
|
||||
recipients:
|
||||
label: Invia a utente, gruppo o e-mail
|
||||
query_id:
|
||||
label: Query di Data Explorer
|
||||
query_params:
|
||||
label: Parametri della query di Data Explorer
|
||||
skip_empty:
|
||||
label: Salta l'invio di MP se non ci sono risultati
|
||||
attach_csv:
|
||||
label: Allega il file CSV al MP
|
||||
recurring_data_explorer_result_topic:
|
||||
fields:
|
||||
topic_id:
|
||||
label: L'argomento in cui pubblicare i risultati della query
|
||||
query_id:
|
||||
label: Query di Data Explorer
|
||||
query_params:
|
||||
label: Parametri della query di Data Explorer
|
||||
skip_empty:
|
||||
label: Salta la pubblicazione se non ci sono risultati
|
||||
attach_csv:
|
||||
label: Allega il file CSV all'argomento
|
114
plugins/discourse-data-explorer/config/locales/client.ja.yml
Normal file
114
plugins/discourse-data-explorer/config/locales/client.ja.yml
Normal file
|
@ -0,0 +1,114 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
ja:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "クエリからセミコロンを削除します。"
|
||||
dirty: "クエリは保存してから実行する必要があります。"
|
||||
explorer:
|
||||
or: "または"
|
||||
admins_only: "データエクスプローラーは管理者のみが利用できます。"
|
||||
allow_groups: "このクエリへのアクセスをグループに許可する"
|
||||
title: "データエクスプローラー"
|
||||
create: "新規作成"
|
||||
create_placeholder: "クエリ名..."
|
||||
description_placeholder: "説明をここに入力"
|
||||
import:
|
||||
label: "インポート"
|
||||
modal: "クエリをインポート"
|
||||
unparseable_json: "解析不能な JSON ファイルです。"
|
||||
wrong_json: "不正な JSON ファイル。JSON ファイルには 'query' オブジェクトと、その中に少なくとも 'sql' プロパティが含まれている必要があります。"
|
||||
help:
|
||||
label: "ヘルプ"
|
||||
modal_title: "データエクスプローラーのヘルプ"
|
||||
auto_resolution: "<h2>エンティティの自動解決</h2> <p>クエリがエンティティ ID を返す場合、データエクスプローラーはクエリ結果において、それを自動的にエンティティ名とその他の有用な情報に置き換えることができます。自動解決は、<i><b>user_id</b></i>、<i><b>group_id</b></i>、<i><b>topic_id</b></i>、<i><b>category_id</b></i>、および <i><b>badge_id</b></i> で利用できます。次のクエリを実行して試してください。</p>\n<pre><code>SELECT user_id\nFROM posts</code></pre>"
|
||||
custom_params: "<h2>カスタムパラメーターの作成</h2> <p>クエリのカスタムパラメーターを作成するには、次のコードをクエリの先頭に配置し、フォーマットに従います。</p> <pre><code>-- [params]\n-- int :num = 1\n\nSELECT :num</code></pre> <p><i>注意: 最初の [params] の行は必須です。また、その前と宣言するカスタムパラメーターの前に 2 つのダッシュが必要です。</i></p>"
|
||||
default_values: "<h3>デフォルト値</h3> <p>パラメーターはデフォルト値の有無に関係なく宣言できます。デフォルト値はクエリエディターの下のテキストフィールドに表示され、必要に応じて編集することができます。パラメーターはデフォルト値なしで宣言された場合でもテキストフィールドを生成しますが、空となり、赤でハイライトされます。</p>\n<pre><code>-- [params]\n-- text :username = my_username\n-- int :age</code></pre>"
|
||||
data_types: "<h3>データ型</h3> <p>以下は、使用できる共通のデータ型です。</p> <ul> <li><b>整数</b> - 符号付き 4 バイト整数</li> <li><b>テキスト</b> - 可変長文字列</li> <li><b>ブール</b> – true/false</li> <li><b>日付</b> - 暦日(年、月、日)</li> </ul> <p>データ型の詳細については、<a href='http://www.postgresql.org/docs/9.3/static/datatype.html#DATATYPE-TABLE' target='_blank'>こちらのウェブサイト</a>をご覧ください。</p>"
|
||||
schema:
|
||||
title: "データベーススキーマ"
|
||||
filter: "検索..."
|
||||
sensitive: "この列のコンテンツには、機密性の特に高い情報または個人情報が含まれることがあります。この列のコンテンツを使用する際には十分注意してください。"
|
||||
types:
|
||||
bool:
|
||||
yes: "はい"
|
||||
no: "いいえ"
|
||||
null_: "Null"
|
||||
export: "エクスポート"
|
||||
view_json: "JSON を表示"
|
||||
save: "変更を保存"
|
||||
saverun: "変更を保存して実行"
|
||||
run: "実行"
|
||||
undo: "変更を破棄"
|
||||
edit: "編集"
|
||||
delete: "削除"
|
||||
recover: "クエリの削除を取り消す"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
show_table: "テーブル"
|
||||
show_graph: "グラフ"
|
||||
others_dirty: "クエリには保存されていない変更があり、移動すると失われます。"
|
||||
run_time: "クエリは %{value} ミリ秒で完了しました。"
|
||||
result_count:
|
||||
other: "%{count} 件の結果。"
|
||||
max_result_count:
|
||||
other: "上位 %{count} 件を表示中。"
|
||||
query_name: "クエリ"
|
||||
query_groups: "グループ"
|
||||
link: "リンク"
|
||||
report_name: "レポート"
|
||||
query_description: "説明"
|
||||
query_time: "最終実行"
|
||||
query_user: "作成者"
|
||||
column: "列 %{number}"
|
||||
explain_label: "クエリプランを含めますか?"
|
||||
save_params: "デフォルトを設定"
|
||||
reset_params: "リセット"
|
||||
search_placeholder: "検索..."
|
||||
no_search_results: "残念ながら、テキストに一致する結果は見つかりませんでした。"
|
||||
form:
|
||||
errors:
|
||||
invalid: "無効です"
|
||||
no_such_category: "そのようなカテゴリはありません"
|
||||
no_such_group: "そのようなグループはありません"
|
||||
invalid_date: "%{date} は無効な日付です"
|
||||
invalid_time: "%{time} は無効な時刻です"
|
||||
group:
|
||||
reports: "レポート"
|
||||
admin:
|
||||
api:
|
||||
scopes:
|
||||
descriptions:
|
||||
discourse_data_explorer:
|
||||
run_queries: "データエクスプローラーのクエリを実行します。クエリ ID を指定して、API キーを一連のクエリに制限します。"
|
||||
discourse_automation:
|
||||
scriptables:
|
||||
recurring_data_explorer_result_pm:
|
||||
fields:
|
||||
recipients:
|
||||
label: ユーザー、グループ、またはメールに送る
|
||||
query_id:
|
||||
label: データエクスプローラーのクエリ
|
||||
query_params:
|
||||
label: データエクスプローラーのクエリパラメーター
|
||||
skip_empty:
|
||||
label: 結果がない場合は PM の送信をスキップする
|
||||
attach_csv:
|
||||
label: CSV ファイルを PM に添付する
|
||||
recurring_data_explorer_result_topic:
|
||||
fields:
|
||||
topic_id:
|
||||
label: クエリ結果を投稿するトピック
|
||||
query_id:
|
||||
label: データエクスプローラーのクエリ
|
||||
query_params:
|
||||
label: データエクスプローラーのクエリパラメーター
|
||||
skip_empty:
|
||||
label: 結果が無い場合は投稿をスキップする
|
||||
attach_csv:
|
||||
label: CSV ファイルを投稿に添付する
|
61
plugins/discourse-data-explorer/config/locales/client.ko.yml
Normal file
61
plugins/discourse-data-explorer/config/locales/client.ko.yml
Normal file
|
@ -0,0 +1,61 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
ko:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "쿼리에서 세미콜론을 제거하십시오."
|
||||
dirty: "실행하기 전에 쿼리를 저장해야합니다."
|
||||
explorer:
|
||||
or: "또는"
|
||||
admins_only: "데이터 탐색기는 관리자 만 사용할 수 있습니다."
|
||||
allow_groups: "그룹이이 쿼리에 액세스하도록 허용"
|
||||
title: "데이터 탐색기"
|
||||
create: "새로 만들기"
|
||||
create_placeholder: "검색어 이름 ..."
|
||||
description_placeholder: "여기에 설명을 입력하세요"
|
||||
import:
|
||||
label: "가져오기"
|
||||
modal: "검색어 가져 오기"
|
||||
help:
|
||||
label: "도움말"
|
||||
schema:
|
||||
title: "데이터베이스 스키마"
|
||||
filter: "검색..."
|
||||
sensitive: "이 열의 내용에는 특히 민감한 정보 또는 개인 정보가 포함될 수 있습니다. 이 열의 내용을 사용할 때는주의하십시오."
|
||||
types:
|
||||
bool:
|
||||
yes: "네"
|
||||
no: "아니오"
|
||||
null_: "없는"
|
||||
export: "내보내기"
|
||||
save: "변경사항 저장"
|
||||
saverun: "변경 사항 저장 및 실행"
|
||||
run: "운영"
|
||||
undo: "변경 사항을 취소"
|
||||
edit: "수정"
|
||||
delete: "삭제"
|
||||
recover: "삭제 취소 쿼리"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
others_dirty: "검색어에 저장하지 않은 변경 사항이있어 이동하면 사라집니다."
|
||||
result_count:
|
||||
other: "%{count}개의 결과."
|
||||
query_name: "쿼리"
|
||||
query_groups: "그룹"
|
||||
link: "링크"
|
||||
report_name: "보고서"
|
||||
query_description: "설명"
|
||||
query_time: "마지막 실행"
|
||||
query_user: "작성자"
|
||||
explain_label: "쿼리 계획을 포함 하시겠습니까?"
|
||||
save_params: "기본값 설정"
|
||||
reset_params: "리셋"
|
||||
search_placeholder: "검색..."
|
||||
no_search_results: "죄송합니다. 귀하의 텍스트와 일치하는 결과를 찾을 수 없습니다."
|
||||
group:
|
||||
reports: "보고서"
|
36
plugins/discourse-data-explorer/config/locales/client.lt.yml
Normal file
36
plugins/discourse-data-explorer/config/locales/client.lt.yml
Normal file
|
@ -0,0 +1,36 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
lt:
|
||||
js:
|
||||
explorer:
|
||||
or: "arba"
|
||||
import:
|
||||
label: "Importuoti"
|
||||
help:
|
||||
label: "Pagalba"
|
||||
schema:
|
||||
filter: "Paieška..."
|
||||
types:
|
||||
bool:
|
||||
yes: "Taip"
|
||||
no: "Ne"
|
||||
export: "Eksportuoti"
|
||||
save: "Išsaugoti pakeitimus"
|
||||
edit: "Redaguoti"
|
||||
delete: "Pašalinti"
|
||||
result_count:
|
||||
one: "%{count} rezultatas."
|
||||
few: "%{count} rezultatai."
|
||||
many: "%{count} rezultatai."
|
||||
other: "%{count} rezultatai."
|
||||
query_groups: "Grupės"
|
||||
query_description: "Aprašymas"
|
||||
query_user: "Sukurta"
|
||||
reset_params: "Atstatyti"
|
||||
search_placeholder: "Paieška..."
|
||||
group:
|
||||
reports: "Pranešimai"
|
28
plugins/discourse-data-explorer/config/locales/client.lv.yml
Normal file
28
plugins/discourse-data-explorer/config/locales/client.lv.yml
Normal file
|
@ -0,0 +1,28 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
lv:
|
||||
js:
|
||||
explorer:
|
||||
or: "vai"
|
||||
import:
|
||||
label: "Importēt"
|
||||
help:
|
||||
label: "Palīdzība"
|
||||
schema:
|
||||
filter: "Meklēt..."
|
||||
types:
|
||||
bool:
|
||||
yes: "Jā"
|
||||
no: "Nē"
|
||||
export: "Eksportēt"
|
||||
save: "Saglabāt izmaiņas"
|
||||
edit: "Labot"
|
||||
delete: "Dzēst"
|
||||
query_groups: "Grupas"
|
||||
query_description: "Apraksts"
|
||||
reset_params: "Atlikt"
|
||||
search_placeholder: "Meklēt..."
|
|
@ -0,0 +1,47 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
nb_NO:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "Fjern semikolonene fra spørringen."
|
||||
dirty: "Du må lagre spørringen før du kjører den."
|
||||
explorer:
|
||||
or: "eller"
|
||||
admins_only: "Datautforskeren er kun tilgjengelig for administratorer."
|
||||
title: "Datautforsker"
|
||||
create: "Opprett ny"
|
||||
create_placeholder: "Spørringsnavn…"
|
||||
import:
|
||||
label: "Importer"
|
||||
modal: "Importer en spørring"
|
||||
help:
|
||||
label: "Hjelp"
|
||||
schema:
|
||||
filter: "Søk…"
|
||||
types:
|
||||
bool:
|
||||
yes: "Ja"
|
||||
no: "Nei"
|
||||
null_: "Null"
|
||||
export: "Eksporter"
|
||||
save: "Lagre endringer"
|
||||
undo: "Forkast endringer"
|
||||
edit: "Rediger"
|
||||
delete: "Slett"
|
||||
recover: "Angre sletting av spørring"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
others_dirty: "En spørring har ulagrede endringer, og vil gå tapt hvis du navigerer vekk."
|
||||
query_groups: "Grupper"
|
||||
query_description: "Beskrivelse"
|
||||
explain_label: "Inkluder spørringsplan?"
|
||||
save_params: "Sett forvalg"
|
||||
reset_params: "Tilbakestill"
|
||||
search_placeholder: "Søk…"
|
||||
group:
|
||||
reports: "Rapporter"
|
116
plugins/discourse-data-explorer/config/locales/client.nl.yml
Normal file
116
plugins/discourse-data-explorer/config/locales/client.nl.yml
Normal file
|
@ -0,0 +1,116 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
nl:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "Verwijder de puntkomma's uit de query."
|
||||
dirty: "Je moet de query opslaan voordat je deze uitvoert."
|
||||
explorer:
|
||||
or: "of"
|
||||
admins_only: "De gegevensverkenner is alleen beschikbaar voor beheerders."
|
||||
allow_groups: "Groepen hebben toegang tot deze query"
|
||||
title: "Gegevensverkenner"
|
||||
create: "Nieuwe maken"
|
||||
create_placeholder: "Querynaam..."
|
||||
description_placeholder: "Voer hier een beschrijving in"
|
||||
import:
|
||||
label: "Importeren"
|
||||
modal: "Een query importeren"
|
||||
unparseable_json: "Onparseerbaar JSON-bestand."
|
||||
wrong_json: "Onjuist JSON-bestand. Een JSON-bestand moet een 'query'-object bevatten, dat minimaal een 'sql'-eigenschap moet hebben."
|
||||
help:
|
||||
label: "Help"
|
||||
modal_title: "Gegevensverkenner-help"
|
||||
auto_resolution: "<h2>Automatische omzetting van entiteiten</h2> <p>Wanneer je query een entiteit-ID retourneert, kan Gegevensverkenner deze automatisch vervangen door de entiteitnaam en andere nuttige informatie in queryresultaten. Automatische omzetting is beschikbaar voor <i><b>user_id</b></i>, <i><b>group_id</b></i>, <i><b>topic_id</b></i>, <i><b>category_id</b></i> en <i><b>badge_id</b></i>. Voer deze query uit om dit te proberen:</p> <pre><code>SELECT user_id\nFROM posts</code></pre>"
|
||||
custom_params: "<h2>Aangepaste parameters maken</h2> <p>Om aangepaste parameters te maken voor je query's, plaats je dit bovenaan je query en volg je de notatie:</p> <pre><code>-- [params]\n-- int :num = 1\n\nSELECT :num</code></pre> <p><i>Opmerking : de eerste regel met [params] is vereist, samen met twee streepjes ervoor en elke aangepaste parameter die je wilt declareren.</i></p>"
|
||||
default_values: "<h3>Standaardwaarden</h3> <p>Je kunt parameters declareren met of zonder standaardwaarde. Standaardwaarden worden weergegeven in een tekstveld onder de query-editor, die je naar wens kunt bewerken. Parameters die zijn gedeclareerd zonder standaardwaarde leveren nog steeds een tekstveld op, maar zijn leeg en rood gemarkeerd.</p> <pre><code>-- [params]\n-- text :username = my_username\n-- int :age</code></pre>"
|
||||
data_types: "<h3>Gegevenstypen</h3> <p>Hier volgen algemene gegevenstypen die je kunt gebruiken:</p> <ul> <li><b>integer</b> - positief of negatief geheel getal van vier bytes</li> <li><b>text</b> - tekenreeks met variabele lengte</li> <li><b>boolean</b> – true/false</li> <li><b>date</b> - kalenderdatum (jaar, maand, dag)</li> </ul> <p>Ga voor meer informatie over gegevenstypen naar <a href='http://www.postgresql.org/docs/9.3/static/datatype.html#DATATYPE-TABLE' target='_blank'>deze website</a>.</p>"
|
||||
schema:
|
||||
title: "Databaseschema"
|
||||
filter: "Zoeken..."
|
||||
sensitive: "De inhoud van deze kolom kan bijzonder gevoelige of privégegevens bevatten. Wees voorzichtig bij gebruik van de inhoud van deze kolom."
|
||||
types:
|
||||
bool:
|
||||
yes: "Ja"
|
||||
no: "Nee"
|
||||
null_: "Null"
|
||||
export: "Exporteren"
|
||||
view_json: "JSON weergeven"
|
||||
save: "Wijzigingen opslaan"
|
||||
saverun: "Wijzigingen opslaan en uitvoeren"
|
||||
run: "Uitvoeren"
|
||||
undo: "Wijzigingen negeren"
|
||||
edit: "Bewerken"
|
||||
delete: "Verwijderen"
|
||||
recover: "Query verwijderen ongedaan maken"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
show_table: "Tabel"
|
||||
show_graph: "Grafiek"
|
||||
others_dirty: "Een query heeft niet-opgeslagen wijzigingen die verloren gaan als je de pagina verlaat."
|
||||
run_time: "Query voltooid in %{value} ms."
|
||||
result_count:
|
||||
one: "%{count} resultaat."
|
||||
other: "%{count} resultaten."
|
||||
max_result_count:
|
||||
one: "Eerste %{count} resultaat wordt weergegeven."
|
||||
other: "Eerste %{count} resultaten worden weergegeven."
|
||||
query_name: "Query"
|
||||
query_groups: "Groepen"
|
||||
link: "Link voor"
|
||||
report_name: "Rapport"
|
||||
query_description: "Beschrijving"
|
||||
query_time: "Laatste uitvoering"
|
||||
query_user: "Gemaakt door"
|
||||
column: "Kolom %{number}"
|
||||
explain_label: "Queryplan bijvoegen?"
|
||||
save_params: "Standaardwaarden instellen"
|
||||
reset_params: "Herstellen"
|
||||
search_placeholder: "Zoeken..."
|
||||
no_search_results: "Er zijn geen resultaten gevonden voor je tekst."
|
||||
form:
|
||||
errors:
|
||||
invalid: "Ongeldig"
|
||||
no_such_category: "Geen dergelijke categorie"
|
||||
no_such_group: "Geen dergelijke groep"
|
||||
invalid_date: "%{date} is een ongeldige datum"
|
||||
invalid_time: "%{time} is een ongeldige tijd"
|
||||
group:
|
||||
reports: "Rapporten"
|
||||
admin:
|
||||
api:
|
||||
scopes:
|
||||
descriptions:
|
||||
discourse_data_explorer:
|
||||
run_queries: "Voer Gegevensverkenner-query's uit. Beperk de API-sleutel tot een reeks query's door query-ID's op te geven."
|
||||
discourse_automation:
|
||||
scriptables:
|
||||
recurring_data_explorer_result_pm:
|
||||
fields:
|
||||
recipients:
|
||||
label: Verzenden naar gebruiker, groep of e-mailadres
|
||||
query_id:
|
||||
label: Gegevensverkenner-query
|
||||
query_params:
|
||||
label: Gegevensverkenner-queryparameters
|
||||
skip_empty:
|
||||
label: PB verzenden overslaan als er geen resultaten zijn
|
||||
attach_csv:
|
||||
label: Voeg het CSV-bestand bij de PB
|
||||
recurring_data_explorer_result_topic:
|
||||
fields:
|
||||
topic_id:
|
||||
label: Het topic om queryresultaten in te plaatsen
|
||||
query_id:
|
||||
label: Gegevensverkenner-query
|
||||
query_params:
|
||||
label: Gegevensverkenner-queryparameters
|
||||
skip_empty:
|
||||
label: Plaatsen overslaan als er geen resultaten zijn
|
||||
attach_csv:
|
||||
label: Voeg het CSV-bestand bij het bericht
|
|
@ -0,0 +1,98 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
pl_PL:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "Usuń średniki z zapytania."
|
||||
dirty: "Musisz zapisać zapytanie zanim je wykonasz."
|
||||
explorer:
|
||||
or: "lub"
|
||||
admins_only: "Eksplorator danych jest dostępny wyłącznie dla administratorów."
|
||||
allow_groups: "Zezwól grupom na dostęp do tego zapytania"
|
||||
title: "Eksplorator danych"
|
||||
create: "Stwórz nowy"
|
||||
create_placeholder: "Nazwa zapytania…"
|
||||
description_placeholder: "Wprowadź opis zapytania"
|
||||
import:
|
||||
label: "Import"
|
||||
modal: "Importuj zapytanie"
|
||||
unparseable_json: "Nieprzetwarzalny plik JSON."
|
||||
wrong_json: "Nieprawidłowy plik JSON. Plik JSON powinien zawierać obiekt 'query', który powinien mieć przynajmniej właściwość 'sql'."
|
||||
help:
|
||||
label: "Pomoc"
|
||||
modal_title: "Pomoc eksploratora danych"
|
||||
auto_resolution: "<h2>Automatyczne rozpoznawanie encji</h2> <p>Gdy Twoje zapytanie zwróci identyfikator encji, eksplorator danych może automatycznie zastąpić go nazwą encji i innymi przydatnymi informacjami w wynikach zapytania. Automatyczne rozpoznawanie jest dostępne dla <i><b>user_id</b></i>, <i><b>group_id</b></i>, <i><b>topic_id</b></i>, <i><b>category_id</b></i> i <i><b>badge_id</b></i>. Aby to wypróbować, wykonaj następujące zapytanie:</p> <pre><code>SELECT user_id\nFROM posts</code></pre>"
|
||||
custom_params: "<h2>Tworzenie niestandardowych parametrów</h2> <p>Aby utworzyć niestandardowe parametry dla zapytań, umieść to na początku zapytania i postępuj zgodnie z formatem:</p> <pre><code>-- [params]\n-- int :num = 1\n\nSELECT :num</code></pre> <p><i>Uwaga: pierwszy wiersz z [params] jest wymagany, wraz z dwoma myślnikami poprzedzającymi go i każdym niestandardowym parametrem, który chcesz zadeklarować.</i></p>"
|
||||
default_values: "<h3>Wartości domyślne</h3> <p>Możesz zadeklarować parametry z wartościami domyślnymi lub bez nich. Wartości domyślne pojawią się w polu tekstowym poniżej edytora zapytań, które możesz edytować zgodnie z własnymi potrzebami. Parametry zadeklarowane bez wartości domyślnych nadal będą generować pole tekstowe, ale będzie ono puste i podświetlone na czerwono.</p> <pre><code>-- [params]\n-- text :username = my_username\n-- int :age</code></pre>"
|
||||
data_types: "<h3>Typy danych</h3> <p>Tutaj są powszechnie używane typy danych, których możesz użyć:</p> <ul> <li><b>liczba całkowita</b> - podpisane czterobajtowe liczby całkowite</li> <li><b>tekst</b> - ciąg znaków o zmiennej długości</li> <li><b>liczba logiczna</b> - prawda/fałsz</li> <li><b>data</b> - data kalendarzowa (rok, miesiąc, dzień)</li> </ul> <p>Aby uzyskać więcej informacji na temat typów danych, odwiedź <a href='http://www.postgresql.org/docs/9.3/static/datatype.html#DATATYPE-TABLE' target='_blank'>tę stronę</a></p>"
|
||||
schema:
|
||||
title: "Schemat bazy danych"
|
||||
filter: "Wyszukiwanie..."
|
||||
sensitive: "Zawartość tej kolumny może zawierać szczególnie wrażliwe lub prywatne dane. Prosimy o zachowanie ostrożności przy używaniu tych danych."
|
||||
types:
|
||||
bool:
|
||||
yes: "Tak"
|
||||
no: "Nie"
|
||||
null_: "Null"
|
||||
export: "Eksport"
|
||||
view_json: "Wyświetl JSON"
|
||||
save: "Zapisz zmiany"
|
||||
saverun: "Zapisz zmiany i wykonaj"
|
||||
run: "Wykonaj"
|
||||
undo: "Odrzuć zmiany"
|
||||
edit: "Edytuj"
|
||||
delete: "Usuń"
|
||||
recover: "Przywróć zapytanie"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
show_table: "Tabela"
|
||||
show_graph: "Wykres"
|
||||
others_dirty: "Zapytanie ma niezapisane zmiany, które utracisz, jeśli przejdziesz w inne miejsce."
|
||||
run_time: "Zapytanie zakończone w %{value} ms."
|
||||
result_count:
|
||||
one: "%{count}wynik."
|
||||
few: "%{count}wyniki."
|
||||
many: "%{count}wyników."
|
||||
other: "%{count}wyników."
|
||||
query_name: "Zapytanie"
|
||||
query_groups: "Grupy"
|
||||
link: "Link do"
|
||||
report_name: "Zgłoszenie"
|
||||
query_description: "Opis"
|
||||
query_time: "Ostatnio wykonano"
|
||||
query_user: "Stworzone przez"
|
||||
column: "Kolumna %{number}"
|
||||
explain_label: "Dołączyć plan zapytania?"
|
||||
save_params: "Ustaw domyślne"
|
||||
reset_params: "Przywróć"
|
||||
search_placeholder: "Wyszukiwanie..."
|
||||
no_search_results: "Nie znaleziono żadnych wyników."
|
||||
group:
|
||||
reports: "Zgłoszenia"
|
||||
admin:
|
||||
api:
|
||||
scopes:
|
||||
descriptions:
|
||||
discourse_data_explorer:
|
||||
run_queries: "Uruchamiaj zapytania eksploratora danych. Ogranicz klucz API do zestawu zapytań, określając identyfikatory zapytań."
|
||||
discourse_automation:
|
||||
scriptables:
|
||||
recurring_data_explorer_result_pm:
|
||||
fields:
|
||||
recipients:
|
||||
label: Wyślij do użytkownika, grupy lub na adres e-mail
|
||||
query_id:
|
||||
label: Zapytanie eksploratora danych
|
||||
query_params:
|
||||
label: Parametry zapytania eksploratora danych
|
||||
recurring_data_explorer_result_topic:
|
||||
fields:
|
||||
query_id:
|
||||
label: Zapytanie eksploratora danych
|
||||
query_params:
|
||||
label: Parametry zapytania eksploratora danych
|
55
plugins/discourse-data-explorer/config/locales/client.pt.yml
Normal file
55
plugins/discourse-data-explorer/config/locales/client.pt.yml
Normal file
|
@ -0,0 +1,55 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
pt:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "Remova os pontos e vírgulas da consulta."
|
||||
dirty: "Deve guardar a consulta antes de executar."
|
||||
explorer:
|
||||
or: "ou"
|
||||
admins_only: "O explorador de dados está disponível apenas para os administradores."
|
||||
title: "Explorador de Dados"
|
||||
create: "Criar Nova"
|
||||
create_placeholder: "Nome da consulta..."
|
||||
import:
|
||||
label: "Importar"
|
||||
modal: "Importar Uma Consulta"
|
||||
help:
|
||||
label: "Ajuda"
|
||||
schema:
|
||||
title: "Esquema da Base de Dados"
|
||||
filter: "Pesquisar..."
|
||||
sensitive: "O conteúdo desta coluna pode conter informação particularmente sensível ou privada. Por favor, tenha cuidado ao utilizar os conteúdos desta coluna."
|
||||
types:
|
||||
bool:
|
||||
yes: "Sim"
|
||||
no: "Não"
|
||||
export: "Exportar"
|
||||
save: "Guardar alterações"
|
||||
saverun: "Guardar Alterações e Executar"
|
||||
run: "Executar"
|
||||
undo: "Ignorar Alterações"
|
||||
edit: "Editar"
|
||||
delete: "Eliminar"
|
||||
recover: "Recuperar Consulta"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
others_dirty: "Uma consulta tem alterações não guardadas que serão perdidas se navegar para outro lado."
|
||||
run_time: "Consulta concluída em %{value} ms."
|
||||
result_count:
|
||||
one: "%{count} resultado."
|
||||
other: "%{count} resultados."
|
||||
query_groups: "Grupos"
|
||||
query_description: "Descrição"
|
||||
column: "Coluna %{number}"
|
||||
explain_label: "Incluir plano de consulta?"
|
||||
save_params: "Definir Predefinições"
|
||||
reset_params: "Repor"
|
||||
search_placeholder: "Pesquisar..."
|
||||
group:
|
||||
reports: "Relatórios"
|
116
plugins/discourse-data-explorer/config/locales/client.pt_BR.yml
Normal file
116
plugins/discourse-data-explorer/config/locales/client.pt_BR.yml
Normal file
|
@ -0,0 +1,116 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
pt_BR:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "Remover os pontos e vírgulas da consulta."
|
||||
dirty: "Você precisa salvar a consulta antes de executar."
|
||||
explorer:
|
||||
or: "ou"
|
||||
admins_only: "O explorador de dados só está disponível para administradores."
|
||||
allow_groups: "Permitir que grupos acessem esta consulta"
|
||||
title: "Explorador de Dados"
|
||||
create: "Criar Nova"
|
||||
create_placeholder: "Nome da consulta…"
|
||||
description_placeholder: "Insira uma descrição aqui"
|
||||
import:
|
||||
label: "Importar"
|
||||
modal: "Importar Uma Consulta"
|
||||
unparseable_json: "Arquivo JSON não analisável"
|
||||
wrong_json: "Arquivo JSON incorreto. Um arquivo JSON deve conter um objeto \"consulta\", que deve ter pelo menos a propriedade \"sql\""
|
||||
help:
|
||||
label: "Ajuda"
|
||||
modal_title: "Ajuda do Explorador de Dados"
|
||||
auto_resolution: "<h2>Resolução de entidade automática</h2> <p>Quando a sua consulta retorna uma id de entidade, o Explorador de Dados pode substituí-la automaticamente pelo nome da entidade e outras informações úteis nos resultados da consulta. A resolução automática está disponível para <i><b>user_id</b></i>, <i><b>group_id</b></i>, <i><b>topic_id</b></i>, <i><b>category_id</b></i> e <i><b>badge_id</b></i>. Para tentar isso, execute esta consulta:</p> <pre><code>SELECT user_id\nFROM posts</code></pre>"
|
||||
custom_params: "<h2>Criação de parâmetros personalizados/h2> <p>Para criar parâmetros personalizados para suas consultas, insira isto no topo da sua consulta e siga o formato:</p> <pre><code>-- [params]\n-- int :num = 1\n\nSELECT :num</code></pre> <p><i>Observação: a primeira linha com [params] é necessária, junto com dois traços antes dela e de cada parâmetro personalizado que você deseja criar.</i></p>"
|
||||
default_values: "<h3>Valores padrão</h3> <p>Você pode declarar parâmetros com ou sem valores padrão. Valores padrão serão exibidos em um campo de texto abaixo do editor de consultas, que pode ser editado conforme necessário. Parâmetros declarados sem valores padrão ainda irão gerar um campo de texto, mas estarão vazios e destacados em vermelho.</p> <pre><code>-- [params]\n-- text :username = my_username\n-- int :age</code></pre>"
|
||||
data_types: "<h3>Tipos de dados</h3> <p>Estes são os tipos de dados comuns que você pode usar:</p> <ul> <li><b>integer</b> - Integer assinado de quatro bytes</li> <li><b>text</b> - cadeia de caracteres de tamanho variado</li> <li><b>boolean</b> – verdadeiro/falso</li> <li><b>data</b> - data do calendário (ano, mês, dia)</li> </ul> <p>Para mais informações sobre tipos de dados, acesse <a href='http://www.postgresql.org/docs/9.3/static/datatype.html#DATATYPE-TABLE' target='_blank'>este site</a>.</p>"
|
||||
schema:
|
||||
title: "Esquema de Banco de Dados"
|
||||
filter: "Pesquisar…"
|
||||
sensitive: "O conteúdo desta coluna pode conter informações particularmente sensíveis ou privadas. Por favor, tenha cuidado ao usar o conteúdo desta coluna."
|
||||
types:
|
||||
bool:
|
||||
yes: "Sim"
|
||||
no: "Não"
|
||||
null_: "Nulo"
|
||||
export: "Exportar"
|
||||
view_json: "Visualizar JSON"
|
||||
save: "Salvar Mudanças"
|
||||
saverun: "Salvar Mudanças e Executar"
|
||||
run: "Executar"
|
||||
undo: "Descartar Mudanças"
|
||||
edit: "Editar"
|
||||
delete: "Apagar"
|
||||
recover: "Desfazer Exclusão da Consulta"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
show_table: "Tabela"
|
||||
show_graph: "Gráfico"
|
||||
others_dirty: "Uma consulta tem alterações não salvas que serão perdidas se você sair desta página."
|
||||
run_time: "Consulta concluída em %{value} ms."
|
||||
result_count:
|
||||
one: "%{count} resultado."
|
||||
other: "%{count} resultados."
|
||||
max_result_count:
|
||||
one: "Mostrando melhor %{count} resultado."
|
||||
other: "Mostrando melhores %{count} resultados."
|
||||
query_name: "Consulta"
|
||||
query_groups: "Grupos"
|
||||
link: "Link para"
|
||||
report_name: "Reportar"
|
||||
query_description: "Descrição"
|
||||
query_time: "Última execução"
|
||||
query_user: "Criado por"
|
||||
column: "Coluna %{number}"
|
||||
explain_label: "Incluir plano de consulta?"
|
||||
save_params: "Definir Padrões"
|
||||
reset_params: "Redefinir"
|
||||
search_placeholder: "Pesquisar..."
|
||||
no_search_results: "Desculpe, não conseguimos encontrar nenhum resultado contendo o seu texto."
|
||||
form:
|
||||
errors:
|
||||
invalid: "Inválido"
|
||||
no_such_category: "Não há esta categoria"
|
||||
no_such_group: "Não há este grupo"
|
||||
invalid_date: "%{date} é uma data inválida"
|
||||
invalid_time: "%{time} é um horário inválido"
|
||||
group:
|
||||
reports: "Relatórios"
|
||||
admin:
|
||||
api:
|
||||
scopes:
|
||||
descriptions:
|
||||
discourse_data_explorer:
|
||||
run_queries: "Execute consultas do Explorador de Dados. Restrinja a chave de API a um conjunto de consultas ao especificar suas IDs."
|
||||
discourse_automation:
|
||||
scriptables:
|
||||
recurring_data_explorer_result_pm:
|
||||
fields:
|
||||
recipients:
|
||||
label: Enviar ao(à) usuário(a), grupo ou e-mail
|
||||
query_id:
|
||||
label: Consulta do explorador de dados
|
||||
query_params:
|
||||
label: Parâmetros da consulta do explorador de dados
|
||||
skip_empty:
|
||||
label: Pular envio de PM se não houver resultados
|
||||
attach_csv:
|
||||
label: Anexe um arquivo CSV à MP
|
||||
recurring_data_explorer_result_topic:
|
||||
fields:
|
||||
topic_id:
|
||||
label: O tópico no qual postar os resultados da consulta
|
||||
query_id:
|
||||
label: Consulta do Data Explorer
|
||||
query_params:
|
||||
label: Parâmetros da consulta do Data Explorer
|
||||
skip_empty:
|
||||
label: Pular postagem se não houver resultados
|
||||
attach_csv:
|
||||
label: Anexe um arquivo CSV à postagem
|
30
plugins/discourse-data-explorer/config/locales/client.ro.yml
Normal file
30
plugins/discourse-data-explorer/config/locales/client.ro.yml
Normal file
|
@ -0,0 +1,30 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
ro:
|
||||
js:
|
||||
explorer:
|
||||
or: "sau"
|
||||
import:
|
||||
label: "Importă"
|
||||
help:
|
||||
label: "Ajutor"
|
||||
schema:
|
||||
filter: "Caută..."
|
||||
types:
|
||||
bool:
|
||||
yes: "Da"
|
||||
no: "Nu"
|
||||
export: "Exportă"
|
||||
save: "Salvează schimbările"
|
||||
edit: "Editează"
|
||||
delete: "Șterge"
|
||||
query_groups: "Grupuri"
|
||||
query_description: "Descriere"
|
||||
reset_params: "Resetează"
|
||||
search_placeholder: "Caută..."
|
||||
group:
|
||||
reports: "Rapoarte"
|
120
plugins/discourse-data-explorer/config/locales/client.ru.yml
Normal file
120
plugins/discourse-data-explorer/config/locales/client.ru.yml
Normal file
|
@ -0,0 +1,120 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
ru:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "Удалите точку с запятой из запроса."
|
||||
dirty: "Вы должны сохранить запрос перед его выполнением."
|
||||
explorer:
|
||||
or: "или"
|
||||
admins_only: "Исследование данных доступно только для администраторов."
|
||||
allow_groups: "Разрешить группам доступ к этому запросу"
|
||||
title: "Проводник данных"
|
||||
create: "Создать"
|
||||
create_placeholder: "Имя запроса..."
|
||||
description_placeholder: "Введите описание"
|
||||
import:
|
||||
label: "Импорт"
|
||||
modal: "Импорт запроса"
|
||||
unparseable_json: "Невозможно распарсить JSON-файл."
|
||||
wrong_json: "Неверный формат JSON-файла. Файл должен содержать объект 'запрос', у которого как минимум должно быть свойство 'sql'."
|
||||
help:
|
||||
label: "Справка"
|
||||
modal_title: "Проводник данных. Справка."
|
||||
auto_resolution: "<h2>Автоматическое разрешение</h2> <p>Когда запрос возвращает идентификатор объекта, проводник данных может автоматически заменить его именем объекта и другой полезной информацией в результатах запроса. Автоматическое разрешение доступно для <i><b>user_id</b></i>, <i><b>group_id</b></i>, <i><b>topic_id</b></i>, <i><b>category_id</b></i> и <i><b>badge_id</b></i>. Для пробы запустите этот запрос:</p> <pre><code>SELECT user_id\nFROM posts</code></pre>"
|
||||
custom_params: "<h2>Создание настраиваемых параметров</h2> <p>Чтобы создать настраиваемые параметры для запроса, поместите эти данные в верхней части запроса, придерживаясь следующего формата:</p> <pre><code>-- [params]\n-- int :num = 1\n\nSELECT :num</code></pre> <p><i>Примечание: первая строка с [params] обязательна, с двумя дефисами перед ней и всеми настраиваемыми параметрами, которые вы хотите объявить.</i></p>"
|
||||
default_values: "<h3>Значения по умолчанию</h3> <p>Вы можете объявлять параметры со значениями по умолчанию или без таковых. Значения по умолчанию будут отображаться в текстовом поле под редактором запросов, эти значения вы можете менять по своему усмотрению. Параметры, объявленные без значений по умолчанию, по-прежнему будут создавать текстовое поле, но такие поля будут пустыми и будут выделены красным цветом.</p> <pre><code>-- [params]\n-- text :username = my_username\n-- int :age</code></pre>"
|
||||
data_types: "<h3>Типы данных</h3> <p>Основные типы данных, которые можно использовать:</p> <ul> <li><b>целое число</b> — знаковое четырехбайтовое целое число,</li> <li><b>текст</b> — строка символов переменной длины,</li> <li><b>логическое значение</b> — истина, ложь,</li> <li><b>дата</b> — календарная дата (год, месяц, день).</li> </ul> <p>Подробнее о типах данных смотрите на <a href='http://www.postgresql.org/docs/9.3/static/datatype.html#DATATYPE-TABLE' target='_blank'>этом веб-сайте</a>.</p>"
|
||||
schema:
|
||||
title: "Схема базы данных"
|
||||
filter: "Поиск..."
|
||||
sensitive: "Эта колонка может содержать конфиденциальную информацию. Будьте осторожны при использовании её содержимого."
|
||||
types:
|
||||
bool:
|
||||
yes: "Да"
|
||||
no: "Нет"
|
||||
null_: "Null"
|
||||
export: "Экспорт"
|
||||
view_json: "Просмотреть JSON"
|
||||
save: "Сохранить изменения"
|
||||
saverun: "Сохранить изменения и выполнить запрос"
|
||||
run: "Выполнить запрос"
|
||||
undo: "Отменить изменения"
|
||||
edit: "Редактировать"
|
||||
delete: "Удалить"
|
||||
recover: "Восстановить запрос"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
show_table: "Таблица"
|
||||
show_graph: "График"
|
||||
others_dirty: "Запрос имеет несохранённые изменения, которые будут потеряны, если вы уйдёте со страницы."
|
||||
run_time: "Запрос завершен за %{value} мс."
|
||||
result_count:
|
||||
one: "%{count} результат."
|
||||
few: "%{count} результата."
|
||||
many: "%{count} результатов."
|
||||
other: "%{count} результатов."
|
||||
max_result_count:
|
||||
one: "Показан %{count} лучший результат."
|
||||
few: "Показаны %{count} лучших результата."
|
||||
many: "Показаны %{count} лучших результатов."
|
||||
other: "Показаны %{count} лучшего результата."
|
||||
query_name: "Запрос"
|
||||
query_groups: "Группы"
|
||||
link: "Ссылка для"
|
||||
report_name: "Отчёт"
|
||||
query_description: "Описание"
|
||||
query_time: "Время выполнения последнего запроса"
|
||||
query_user: "Создано"
|
||||
column: "Колонка %{number}"
|
||||
explain_label: "Отобразить план запроса"
|
||||
save_params: "Установка значений по умолчанию"
|
||||
reset_params: "Сброс"
|
||||
search_placeholder: "Поиск..."
|
||||
no_search_results: "К сожалению, мы не смогли найти результаты, соответствующие вашему запросу."
|
||||
form:
|
||||
errors:
|
||||
invalid: "Недопустимое значение"
|
||||
no_such_category: "Нет такой категории"
|
||||
no_such_group: "Нет такой группы"
|
||||
invalid_date: "%{date} — недопустимая дата"
|
||||
invalid_time: "%{time} — недопустимое время"
|
||||
group:
|
||||
reports: "Отчёты"
|
||||
admin:
|
||||
api:
|
||||
scopes:
|
||||
descriptions:
|
||||
discourse_data_explorer:
|
||||
run_queries: "Выполнение запросов проводника данных. Ограничьте ключ API набором запросов, указав идентификаторы запросов."
|
||||
discourse_automation:
|
||||
scriptables:
|
||||
recurring_data_explorer_result_pm:
|
||||
fields:
|
||||
recipients:
|
||||
label: Отправить пользователю, в группу или на адрес электронной почты
|
||||
query_id:
|
||||
label: Запрос проводника данных
|
||||
query_params:
|
||||
label: Параметры запроса проводника данных
|
||||
skip_empty:
|
||||
label: Пропустить отправку личного сообщения, если нет результатов
|
||||
attach_csv:
|
||||
label: Прикрепить CSV-файл к личному сообщению
|
||||
recurring_data_explorer_result_topic:
|
||||
fields:
|
||||
topic_id:
|
||||
label: Тема для публикации результатов запроса
|
||||
query_id:
|
||||
label: Запрос проводника данных
|
||||
query_params:
|
||||
label: Параметры запроса для проводника данных
|
||||
skip_empty:
|
||||
label: Пропустить публикацию, если нет результатов
|
||||
attach_csv:
|
||||
label: Прикрепить CSV-файл к публикации
|
42
plugins/discourse-data-explorer/config/locales/client.sk.yml
Normal file
42
plugins/discourse-data-explorer/config/locales/client.sk.yml
Normal file
|
@ -0,0 +1,42 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
sk:
|
||||
js:
|
||||
explorer:
|
||||
or: "alebo"
|
||||
admins_only: "Prieskumník údajov je dostupný iba administrátorom."
|
||||
title: "Prieskumník údajov"
|
||||
create: "Vztvoriť novú"
|
||||
import:
|
||||
label: "Import"
|
||||
help:
|
||||
label: "Pomoc"
|
||||
schema:
|
||||
filter: "Hľadať"
|
||||
types:
|
||||
bool:
|
||||
yes: "Áno"
|
||||
no: "Nie"
|
||||
export: "Exportovať"
|
||||
save: "Uložiť zmeny"
|
||||
edit: "Upraviť"
|
||||
delete: "Zmazať"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
result_count:
|
||||
one: "%{count} výsledok."
|
||||
few: "%{count} výsledkov."
|
||||
many: "%{count} výsledky."
|
||||
other: "%{count} výsledky."
|
||||
query_groups: "Skupiny"
|
||||
query_description: "Popis"
|
||||
query_user: "Vytvoril"
|
||||
save_params: "Predvolené"
|
||||
reset_params: "Resetovať"
|
||||
search_placeholder: "Hľadať"
|
||||
group:
|
||||
reports: "Hlásenia"
|
68
plugins/discourse-data-explorer/config/locales/client.sl.yml
Normal file
68
plugins/discourse-data-explorer/config/locales/client.sl.yml
Normal file
|
@ -0,0 +1,68 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
sl:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "Odstranite podpičja iz poizvedbe."
|
||||
dirty: "Pred zagonom morate poizvedbo shraniti."
|
||||
explorer:
|
||||
or: "ali"
|
||||
admins_only: "Raziskovalec podatkov je na voljo samo skrbnikom."
|
||||
allow_groups: "Dovoli skupinam dostop do te poizvedbe"
|
||||
title: "Raziskovalec podatkov"
|
||||
create: "Ustvari novo"
|
||||
create_placeholder: "Ime poizvedbe..."
|
||||
description_placeholder: "Vnesite opis tukaj"
|
||||
import:
|
||||
label: "Uvozi"
|
||||
modal: "Uvozi poizvedbo"
|
||||
help:
|
||||
label: "Pomoč"
|
||||
schema:
|
||||
title: "Shema zbirke podatkov"
|
||||
filter: "Išči..."
|
||||
sensitive: "Vsebina tega stolpca lahko vsebuje posebno občutljive ali zasebne podatke. Pri uporabi vsebine tega stolpca bodite previdni."
|
||||
types:
|
||||
bool:
|
||||
yes: "Da"
|
||||
no: "Ne"
|
||||
null_: "Null"
|
||||
export: "Izvozi"
|
||||
save: "Shrani spremembe"
|
||||
saverun: "Shrani spremembe in zaženi"
|
||||
run: "Zaženi"
|
||||
undo: "Zavrzi spremembe"
|
||||
edit: "Uredi"
|
||||
delete: "Izbriši"
|
||||
recover: "Povrni poizvedbo"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
show_table: "Tabela"
|
||||
show_graph: "Graf"
|
||||
others_dirty: "Poizvedba ima neshranjene spremembe, ki se bodo izgubile, če boste šli drugam."
|
||||
run_time: "Poizvedba končana v %{value} ms."
|
||||
result_count:
|
||||
one: "%{count} zadetek."
|
||||
two: "%{count} zadetka."
|
||||
few: "%{count} zadetki."
|
||||
other: "%{count} zadetkov."
|
||||
query_name: "Poizvedba"
|
||||
query_groups: "Skupine"
|
||||
link: "Povezava za"
|
||||
report_name: "Poročilo"
|
||||
query_description: "Opis"
|
||||
query_time: "Zadnja izvedba"
|
||||
query_user: "Ustvaril"
|
||||
column: "Stolpec %{number}"
|
||||
explain_label: "Vključi načrt poizvedbe?"
|
||||
save_params: "Nastavi privzete vrednosti"
|
||||
reset_params: "Ponastavi"
|
||||
search_placeholder: "Išči..."
|
||||
no_search_results: "Oprostite, nismo našli nobenega zadetka, ki bi ustrezal vaši poizvedbi."
|
||||
group:
|
||||
reports: "Poročila"
|
23
plugins/discourse-data-explorer/config/locales/client.sq.yml
Normal file
23
plugins/discourse-data-explorer/config/locales/client.sq.yml
Normal file
|
@ -0,0 +1,23 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
sq:
|
||||
js:
|
||||
explorer:
|
||||
or: "ose"
|
||||
import:
|
||||
label: "Importo"
|
||||
types:
|
||||
bool:
|
||||
yes: "Po"
|
||||
no: "Jo"
|
||||
export: "Eksporto"
|
||||
save: "Ruaj ndryshimet"
|
||||
edit: "Redakto"
|
||||
delete: "Fshij"
|
||||
query_groups: "Grupet"
|
||||
query_description: "Përshkrimi"
|
||||
reset_params: "Rivendos"
|
20
plugins/discourse-data-explorer/config/locales/client.sr.yml
Normal file
20
plugins/discourse-data-explorer/config/locales/client.sr.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
sr:
|
||||
js:
|
||||
explorer:
|
||||
or: "ili"
|
||||
types:
|
||||
bool:
|
||||
yes: "Da"
|
||||
no: "Ne"
|
||||
export: "Izvoz"
|
||||
save: "Sačuvaj izmene"
|
||||
edit: "Izmeni"
|
||||
delete: "Obriši"
|
||||
query_groups: "Grupe"
|
||||
query_description: "Opis"
|
82
plugins/discourse-data-explorer/config/locales/client.sv.yml
Normal file
82
plugins/discourse-data-explorer/config/locales/client.sv.yml
Normal file
|
@ -0,0 +1,82 @@
|
|||
# WARNING: Never edit this file.
|
||||
# It will be overwritten when translations are pulled from Crowdin.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://translate.discourse.org/
|
||||
|
||||
sv:
|
||||
js:
|
||||
errors:
|
||||
explorer:
|
||||
no_semicolons: "Ta bort semikolon från sökningen."
|
||||
dirty: "Du måste spara sökningen innan du kör."
|
||||
explorer:
|
||||
or: "eller"
|
||||
admins_only: "Datautforskaren är endast tillgänglig för administratörer."
|
||||
allow_groups: "Tillåt att grupper går till denna sökning"
|
||||
title: "Datautforskaren"
|
||||
create: "Skapa ny"
|
||||
create_placeholder: "Sökningsnamn..."
|
||||
description_placeholder: "Ange en beskrivning här"
|
||||
import:
|
||||
label: "Importera"
|
||||
modal: "Importera en sökning"
|
||||
unparseable_json: "JSON-fil kan inte tolkas."
|
||||
wrong_json: "Fel JSON-fil. En JSON-fil bör innehålla ett 'query'-objekt, som åtminstone bör ha en 'sql'-egenskap."
|
||||
help:
|
||||
label: "Hjälp"
|
||||
modal_title: "Hjälp med Datautforskaren"
|
||||
auto_resolution: "<h2>Automatisk enhetsmatchning</h2> <p>När din fråga returnerar ett enhets-ID kan Datautforskaren automatiskt ersätta det med enhetens namn och annan användbar information i frågeresultaten. Automatisk matchning finns tillgängligt för <i><b>user_id</b></i>, <i><b>group_id</b></i>, <i><b>topic_id</b></i>, <i><b>category_id</b></i> och <i><b>badge_id</b></i>. För att prova detta kör du den här frågan:</p> <pre><code>SELECT user_id\nFROM posts</code></pre>"
|
||||
custom_params: "<h2>Skapa anpassade parametrar</h2> <p>För att skapa anpassade parametrar för dina sökfrågor skriver du detta högst upp i din sökfråga, enligt formatet:</p> <pre><code>-- [params]\n-- int :num = 1\n\nSELECT :num</code></pre> <p><i>Obs: Den första raden med [params] krävs, med två bindestreck före den och varje anpassad parameter som du vill ange.</i></p>"
|
||||
default_values: "<h3>Standardvärden</h3> <p>Du kan ange parametrar med eller utan standardvärden. Standardvärden kommer att visas i ett textfält under frågeredigeraren, som du kan redigera enligt dina behov. Parametrar som anges utan standardvärden kommer fortfarande att generera ett textfält, men kommer att vara tomma och rödmarkerade.</p> <pre><code>-- [params]\n-- text :username = my_username\n-- int :age</code></pre>"
|
||||
data_types: "<h3>Datatyper</h3> <p>Här är vanliga datatyper som du kan använda:</p> <ul> <li><b>heltal</b> - signerat fyra byte-heltal</li> <li><b>text</b> - teckensträng av varierande längd</li> <li><b>boolesk</b> – sant/falsk</li> <li><b>datum</b> - kalenderdatum (år, månad, dag)</li> </ul> <p>För mer information om datatyper, besök <a href='http://www.postgresql.org/docs/9.3/static/datatype.html#DATATYPE-TABLE' target='_blank'>denna webbplats</a>.</p>"
|
||||
schema:
|
||||
title: "Databasschema"
|
||||
filter: "Sök..."
|
||||
sensitive: "Innehållet i denna kolumn kan innehålla särskilt känslig eller privat information. Var försiktig när du använder innehållet i denna kolumn."
|
||||
types:
|
||||
bool:
|
||||
yes: "Ja"
|
||||
no: "Nej"
|
||||
null_: "Tomt"
|
||||
export: "Exportera"
|
||||
save: "Spara ändringar"
|
||||
saverun: "Spara ändringar och kör"
|
||||
run: "Kör"
|
||||
undo: "Ignorera ändringar"
|
||||
edit: "Redigera"
|
||||
delete: "Radera"
|
||||
recover: "Återställ sökning"
|
||||
download_json: "JSON"
|
||||
download_csv: "CSV"
|
||||
show_table: "Tabell"
|
||||
show_graph: "Graf"
|
||||
others_dirty: "En sökning har ändringar som inte har sparats och kommer att förloras om du lämnar sidan."
|
||||
run_time: "Sökning slutförd efter %{value} ms."
|
||||
result_count:
|
||||
one: "%{count} resultat."
|
||||
other: "%{count} resultat."
|
||||
max_result_count:
|
||||
one: "Visar det %{count} översta resultatet."
|
||||
other: "Visar de %{count} översta resultaten."
|
||||
query_name: "Sökning"
|
||||
query_groups: "Grupper"
|
||||
link: "Länk för"
|
||||
report_name: "Rapport"
|
||||
query_description: "Beskrivning"
|
||||
query_time: "Senast körd"
|
||||
query_user: "Skapad av"
|
||||
column: "Kolumn %{number}"
|
||||
explain_label: "Inkludera sökningsplan?"
|
||||
save_params: "Ange förvalda värden"
|
||||
reset_params: "Återställ"
|
||||
search_placeholder: "Sök..."
|
||||
no_search_results: "Tyvärr kunde vi inte hitta några resultat som stämmer överens med din text."
|
||||
group:
|
||||
reports: "Rapporter"
|
||||
admin:
|
||||
api:
|
||||
scopes:
|
||||
descriptions:
|
||||
discourse_data_explorer:
|
||||
run_queries: "Kör frågor för datautforskaren. Begränsa API-nyckeln till en viss uppsättning frågor genom att specificera fråge-ID:n."
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue