Merge master

This commit is contained in:
Neil Lalonde 2020-06-24 13:47:36 -04:00
commit 607d00f780
No known key found for this signature in database
GPG Key ID: FF871CA9037D0A91
4650 changed files with 229833 additions and 157327 deletions

View File

@ -3,7 +3,7 @@ app/assets/javascripts/main_include_admin.js
app/assets/javascripts/vendor.js
app/assets/javascripts/locales/i18n.js
app/assets/javascripts/ember-addons/
app/assets/javascripts/discourse/lib/autosize.js.es6
app/assets/javascripts/discourse/lib/autosize.js
lib/javascripts/locale/
lib/javascripts/messageformat.js
lib/highlight_js/
@ -13,3 +13,5 @@ vendor/
test/javascripts/test_helper.js
test/javascripts/fixtures
test/javascripts/helpers/assertions.js
node_modules/
dist/

View File

@ -7,3 +7,24 @@
# DEV: enforces no self-closing-void-elements
dafd3c3b47f116c6c1dc56cb18df614c11747733
# Rename many `.js.es6` files to `.js`
032205e2029cbf82dc8f05b459fb93adf2503c60
# Rename pretty-text from es6 -> js
c15056650647e8650288f973d9038500dc9cf7bb
# Rename wizard from es6 -> js
1ac02422011f89716ab27250d39b0e0212e03892
# DEV: enforces block-indentation of ember-template-lint rules
b66b277dc44bcd2122dc21965dab209c30636214
# DEV: enforces double quotes ember-template-lint
c4644c61d97c823b7dd940ffaf0967a104f4b58c
# Migrate to app directory
7a2e8d3ead63c7d99e1069fc7823e933f931ba85
# DEV: Fix indentation for routes.rb
985900818ff985b04def6aa4c5d99c1aa6dbd45c

View File

@ -6,7 +6,7 @@ on:
- master
pull_request:
branches-ignore:
- 'tests-passed'
- "tests-passed"
jobs:
build:
@ -28,12 +28,12 @@ jobs:
fail-fast: false
matrix:
build_types: [ 'BACKEND', 'FRONTEND', 'LINT' ]
target: [ 'PLUGINS', 'CORE' ]
os: [ ubuntu-latest ]
ruby: [ '2.6' ]
postgres: [ '10' ]
redis: [ '4.x' ]
build_types: ["BACKEND", "FRONTEND", "LINT"]
target: ["PLUGINS", "CORE"]
os: [ubuntu-latest]
ruby: ["2.6"]
postgres: ["12"]
redis: ["4.x"]
services:
postgres:
@ -57,14 +57,24 @@ jobs:
fetch-depth: 1
- name: Setup Git
run: git config --global user.email "ci@ci.invalid" && git config --global user.name "Discourse CI"
run: |
git config --global user.email "ci@ci.invalid"
git config --global user.name "Discourse CI"
- name: Setup packages
if: env.BUILD_TYPE != 'LINT'
run: |
sudo apt-get -yqq install postgresql-client libpq-dev gifsicle jpegoptim optipng jhead && \
sudo apt-get update
sudo apt-get -yqq install postgresql-client libpq-dev gifsicle jpegoptim optipng jhead
wget -qO- https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-pngquant | sudo sh
- name: Update imagemagick
if: env.BUILD_TYPE == 'BACKEND'
run: |
wget https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-imagemagick
chmod +x install-imagemagick
sudo ./install-imagemagick
- name: Setup redis
uses: shogo82148/actions-setup-redis@v1
if: env.BUILD_TYPE != 'LINT'
@ -75,10 +85,12 @@ jobs:
uses: actions/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
architecture: 'x64'
- name: Setup bundler
run: gem install bundler -v 2.1.1 --no-doc
run: |
gem install bundler -v 2.1.4 --no-doc
bundle config deployment 'true'
bundle config without 'development'
- name: Bundler cache
uses: actions/cache@v1
@ -90,7 +102,7 @@ jobs:
${{ runner.os }}-gem-
- name: Setup gems
run: bundle install --without development --deployment --jobs 4 --retry 3
run: bundle install --jobs 4
- name: Get yarn cache directory
id: yarn-cache-dir
@ -114,11 +126,15 @@ jobs:
- name: Create database
if: env.BUILD_TYPE != 'LINT'
run: bin/rake db:create && bin/rake db:migrate
run: |
bin/rake db:create
bin/rake db:migrate
- name: Create parallel databases
if: env.BUILD_TYPE == 'BACKEND' && env.TARGET == 'CORE'
run: bin/rake parallel:create && bin/rake parallel:migrate
run: |
bin/rake parallel:create
bin/rake parallel:migrate
- name: Rubocop
if: env.BUILD_TYPE == 'LINT'
@ -126,17 +142,29 @@ jobs:
- name: ESLint
if: env.BUILD_TYPE == 'LINT'
run: yarn eslint app/assets/javascripts test/javascripts && yarn eslint --ext .es6 app/assets/javascripts test/javascripts plugins
run: |
yarn eslint app/assets/javascripts test/javascripts
yarn eslint --global I18n --ext .es6 plugins
- name: Prettier
if: env.BUILD_TYPE == 'LINT'
run: |
yarn prettier -v
yarn prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6" "plugins/**/*.scss" "plugins/**/*.es6"
yarn prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.js" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6" "plugins/**/*.scss" "plugins/**/*.es6"
- name: Core English locale
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'
run: bundle exec ruby script/i18n_lint.rb "config/**/locales/{client,server}.en.yml"
- name: Plugin English locale
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'PLUGINS'
run: bundle exec ruby script/i18n_lint.rb "plugins/**/locales/{client,server}.en.yml"
- name: Core RSpec
if: env.BUILD_TYPE == 'BACKEND' && env.TARGET == 'CORE'
run: bin/turbo_rspec && bin/rake plugin:spec
run: |
bin/turbo_rspec
bin/rake plugin:spec
- name: Plugin RSpec
if: env.BUILD_TYPE == 'BACKEND' && env.TARGET == 'PLUGINS'
@ -154,5 +182,5 @@ jobs:
- name: Plugin QUnit # Tests core plugins in TARGET=CORE, and all plugins in TARGET=PLUGINS
if: env.BUILD_TYPE == 'FRONTEND'
run: bundle exec rake plugin:qunit
run: bundle exec rake plugin:qunit['*','1200000']
timeout-minutes: 30

13
.gitignore vendored
View File

@ -54,7 +54,7 @@ bootsnap-compile-cache/
!/plugins/discourse-narrative-bot
!/plugins/discourse-presence
!/plugins/discourse-local-dates
!/plugins/discourse-internet-explorer
!/plugins/discourse-unsupported-browser
/plugins/*/auto_generated/
/spec/fixtures/plugins/my_plugin/auto_generated
@ -122,7 +122,7 @@ vendor/bundle/*
*.swn
# ignore nodejs files
/node_modules
node_modules
/package-lock.json
/vendor/data/GeoLite2-City.mmdb
@ -132,3 +132,12 @@ vendor/bundle/*
# ignore auto-generated plugin js assets
/app/assets/javascripts/plugins/*
# ignore generated api documentation files
openapi/*
# ember-cli generated
dist
# Copyright Deposits
copyright

View File

@ -1,146 +1,9 @@
require:
- rubocop-discourse
inherit_gem:
rubocop-discourse: default.yml
AllCops:
TargetRubyVersion: 2.4
DisabledByDefault: true
Exclude:
- "db/schema.rb"
- "bundle/**/*"
- "vendor/**/*"
- "node_modules/**/*"
- "public/**/*"
- "plugins/**/gems/**/*"
# Prefer &&/|| over and/or.
Style/AndOr:
Enabled: true
Style/FrozenStringLiteralComment:
Enabled: true
# Align `when` with `case`.
Layout/CaseIndentation:
Enabled: true
# Align comments with method definitions.
Layout/CommentIndentation:
Enabled: true
# No extra empty lines.
Layout/EmptyLines:
Enabled: true
# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
Style/HashSyntax:
Enabled: true
# Two spaces, no tabs (for indentation).
Layout/IndentationWidth:
Enabled: true
Layout/SpaceAfterColon:
Enabled: true
Layout/SpaceAfterComma:
Enabled: true
Layout/SpaceAroundEqualsInParameterDefault:
Enabled: true
Layout/SpaceAroundKeyword:
Enabled: true
Layout/SpaceAroundOperators:
Enabled: true
Layout/SpaceBeforeFirstArg:
Enabled: true
# Defining a method with parameters needs parentheses.
Style/MethodDefParentheses:
Enabled: true
# Use `foo {}` not `foo{}`.
Layout/SpaceBeforeBlockBraces:
Enabled: true
# Use `foo { bar }` not `foo {bar}`.
Layout/SpaceInsideBlockBraces:
Enabled: true
# Use `{ a: 1 }` not `{a:1}`.
Layout/SpaceInsideHashLiteralBraces:
Enabled: true
Layout/SpaceInsideParens:
Enabled: true
# Detect hard tabs, no hard tabs.
Layout/Tab:
Enabled: true
# Blank lines should not have any spaces.
Layout/TrailingEmptyLines:
Enabled: true
# No trailing whitespace.
Layout/TrailingWhitespace:
Enabled: true
Lint/Debugger:
Enabled: true
Layout/BlockAlignment:
Enabled: true
# Align `end` with the matching keyword or starting expression except for
# assignments, where it should be aligned with the LHS.
Layout/EndAlignment:
Enabled: true
EnforcedStyleAlignWith: variable
# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
Lint/RequireParentheses:
Enabled: true
Lint/ShadowingOuterLocalVariable:
Enabled: true
Layout/MultilineMethodCallIndentation:
Enabled: true
EnforcedStyle: indented
Layout/HashAlignment:
Enabled: true
Bundler/OrderedGems:
# Still work to do in ensuring we don't link old files
Discourse/NoAddReferenceOrAliasesActiveRecordMigration:
Enabled: false
Style/SingleLineMethods:
Discourse/NoResetColumnInformationInMigrations:
Enabled: true
Style/Semicolon:
Enabled: true
AllowAsExpressionSeparator: true
Style/RedundantReturn:
Enabled: true
DiscourseCops/NoChdir:
Enabled: true
Exclude:
- 'spec/**/*' # Specs are run sequentially, so chdir can be used
- 'plugins/*/spec/**/*'
DiscourseCops/NoURIEscapeEncode:
Enabled: true
Style/GlobalVars:
Enabled: true
Severity: warning
Exclude:
- 'lib/tasks/**/*'
- 'script/**/*'
- 'spec/**/*.rb'
- 'plugins/*/spec/**/*'

View File

@ -1,11 +1,55 @@
module.exports = {
// extends: "recommended",
extends: "recommended",
ignore: ["**/*.raw"],
// Pending:
// "eol-last": "always",
rules: {
"block-indentation": true,
"deprecated-render-helper": true,
"linebreak-style": true,
"link-rel-noopener": "strict",
"no-abstract-roles": true,
"no-args-paths": true,
"no-attrs-in-components": true,
"no-debugger": true,
"no-duplicate-attributes": true,
"no-extra-mut-helper-argument": true,
"no-html-comments": true,
"no-index-component-invocation": true,
"no-inline-styles": false,
"no-input-block": true,
"no-input-tagname": true,
"no-implicit-this": false,
"no-invalid-interactive": true,
"no-invalid-link-text": true,
"no-invalid-meta": true,
"no-invalid-role": true,
"no-log": true,
"no-negated-condition": true,
"no-nested-interactive": true,
"no-multiple-empty-lines": true,
"no-obsolete-elements": true,
"no-outlet-outside-routes": true,
"no-partial": true,
"no-positive-tabindex": false,
"no-quoteless-attributes": true,
"no-shadowed-elements": true,
"no-trailing-spaces": true,
"no-triple-curlies": true,
"no-unbound": true,
"no-unnecessary-concat": true,
"no-unnecessary-component-helper": true,
"no-unused-block-params": true,
quotes: "double",
"require-button-type": true,
"require-iframe-title": true,
"require-valid-alt-text": false,
"self-closing-void-elements": true,
"table-groups": true,
"simple-unless": true,
"style-concatenation": true,
"no-invalid-interactive": true
"table-groups": true,
"link-href-attributes": false
}
};

View File

@ -1,86 +0,0 @@
language: ruby
git:
depth: false
branches:
only:
- master
- beta
- stable
env:
global:
- TRAVIS_NODE_VERSION="10"
- DISCOURSE_HOSTNAME=www.example.com
- RUBY_GLOBAL_METHOD_CACHE_SIZE=131072
matrix:
- "RAILS_MASTER=0 QUNIT_RUN=0 RUN_LINT=0"
- "RAILS_MASTER=0 QUNIT_RUN=1 RUN_LINT=0"
- "RAILS_MASTER=0 QUNIT_RUN=0 RUN_LINT=1"
addons:
chrome: stable
postgresql: "9.6"
apt:
update: true
packages:
- gifsicle
- jpegoptim
- optipng
- jhead
matrix:
fast_finish: true
rvm:
- 2.6.3
services:
- redis-server
sudo: required
dist: xenial
cache:
yarn: true
directories:
- vendor/bundle
before_install:
- wget -qO- https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-pngquant | sudo sh
- nvm install node
- node --version
- gem install bundler -v 1.17.3
- git clone --depth=1 https://github.com/discourse/discourse-backup-uploads-to-s3.git plugins/discourse-backup-uploads-to-s3
- git clone --depth=1 https://github.com/discourse/discourse-spoiler-alert.git plugins/discourse-spoiler-alert
- git clone --depth=1 https://github.com/discourse/discourse-cakeday.git plugins/discourse-cakeday
- git clone --depth=1 https://github.com/discourse/discourse-canned-replies.git plugins/discourse-canned-replies
- git clone --depth=1 https://github.com/discourse/discourse-chat-integration.git plugins/discourse-chat-integration
- git clone --depth=1 https://github.com/discourse/discourse-assign.git plugins/discourse-assign
- git clone --depth=1 https://github.com/discourse/discourse-patreon.git plugins/discourse-patreon
- git clone --depth=1 https://github.com/discourse/discourse-user-notes.git plugins/discourse-user-notes
- git clone --depth=1 https://github.com/discourse/discourse-group-tracker
- export PATH=$HOME/.yarn/bin:$PATH
install:
- bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails seed-fu; fi"
- bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3; fi"
- bash -c "if [ '$QUNIT_RUN' == '1' ] || [ '$RUN_LINT' == '1' ]; then yarn install --dev; fi"
- bash -c "if [ '$RUN_LINT' != '1' ]; then bundle exec rake db:create && LOAD_PLUGINS=1 bundle exec rake db:migrate; fi"
script:
- |
bash -c "
if [ '$RUN_LINT' == '1' ]; then
npx lefthook run lints
else
if [ '$QUNIT_RUN' == '1' ]; then
bundle exec rake qunit:test['1200000'] && \
bundle exec rake qunit:test['1200000','/wizard/qunit'] && \
bundle exec rake plugin:qunit
else
bundle exec rspec && bundle exec rake plugin:spec
fi
fi
"

View File

@ -62,12 +62,6 @@ source_file = plugins/discourse-details/config/locales/server.en.yml
source_lang = en
type = YML
[discourse-org.corepluginnginx-performance-reportserveryml]
file_filter = plugins/discourse-nginx-performance-report/config/locales/server.<lang>.yml
source_file = plugins/discourse-nginx-performance-report/config/locales/server.en.yml
source_lang = en
type = YML
[discourse-org.core-plugin-local-dates-client-yml]
file_filter = plugins/discourse-local-dates/config/locales/client.<lang>.yml
source_file = plugins/discourse-local-dates/config/locales/client.en.yml

View File

@ -29,5 +29,3 @@ Javascript
Ruby
Rails - Copyright (c) 2005-2013 David Heinemeier Hansson, Rails Core Team contributors (MIT)
Thin - Copyright (c) 2012-2013 Marc-Andre Cournoyer

View File

@ -4,7 +4,7 @@ if github.pr_json && (github.pr_json["additions"] || 0) > 250 || (github.pr_json
warn("This pull request is big! We prefer smaller PRs whenever possible, as they are easier to review. Can this be split into a few smaller PRs?")
end
prettier_offenses = `yarn --silent prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6"`.split("\n")
prettier_offenses = `yarn --silent prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.js" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6"`.split("\n")
unless prettier_offenses.empty?
fail(%{
@ -22,9 +22,9 @@ end
files = (git.added_files + git.modified_files)
.select { |path| !path.start_with?("plugins/") }
.select { |path| path.end_with?("es6") || path.end_with?("rb") }
.select { |path| path.end_with?("es6") || path.end_with?("js") || path.end_with?("rb") }
js_files = files.select { |path| path.end_with?(".js.es6") }
js_files = files.select { |path| path.end_with?(".js.es6") || path.end_with?(".js") }
js_test_files = js_files.select { |path| path.end_with?("-test.js.es6") }
super_offenses = []

45
Gemfile
View File

@ -18,16 +18,18 @@ else
# this allows us to include the bits of rails we use without pieces we do not.
#
# To issue a rails update bump the version number here
gem 'actionmailer', '6.0.1'
gem 'actionpack', '6.0.1'
gem 'actionview', '6.0.1'
gem 'activemodel', '6.0.1'
gem 'activerecord', '6.0.1'
gem 'activesupport', '6.0.1'
gem 'railties', '6.0.1'
gem 'actionmailer', '6.0.3.1'
gem 'actionpack', '6.0.3.1'
gem 'actionview', '6.0.3.1'
gem 'activemodel', '6.0.3.1'
gem 'activerecord', '6.0.3.1'
gem 'activesupport', '6.0.3.1'
gem 'railties', '6.0.3.1'
gem 'sprockets-rails'
end
gem 'json'
# TODO: At the moment Discourse does not work with Sprockets 4, we would need to correct internals
# This is a desired upgrade we should get to.
gem 'sprockets', '3.7.2'
@ -110,30 +112,23 @@ gem 'oj'
gem 'pg'
gem 'mini_sql'
gem 'pry-rails', require: false
gem 'pry-byebug', require: false
gem 'r2', require: false
gem 'rake'
gem 'thor', require: false
gem 'diffy', require: false
gem 'rinku'
gem 'sanitize'
gem 'sidekiq'
gem 'mini_scheduler'
# for sidekiq web
gem 'tilt', require: false
gem 'execjs', require: false
gem 'mini_racer'
# TODO: determine why highline is being held back and upgrade to latest
gem 'highline', '~> 1.7.0', require: false
# TODO: Upgrading breaks Sidekiq Web
# This is a bit of a hornets nest cause in an ideal world we much prefer
# if Sidekiq reused session and CSRF mitigation with Discourse on the
# _forum_session cookie instead of a rack.session cookie
gem 'rack', '2.0.8'
gem 'rack', '2.2.2'
gem 'rack-protection' # security
gem 'cbor', require: false
@ -161,10 +156,7 @@ group :test, :development do
gem 'listen', require: false
gem 'certified', require: false
gem 'fabrication', require: false
# TODO: upgrading to 1.10.1 cause it breaks our test suite.
# We want our test suite fixed though to support this upgrade.
gem 'mocha', '1.8.0', require: false
gem 'mocha', require: false
gem 'rb-fsevent', require: RUBY_PLATFORM =~ /darwin/i ? 'rb-fsevent' : false
@ -172,22 +164,21 @@ group :test, :development do
# we would like to upgrade it if possible
gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false
# TODO once 4.0.0 is released upgrade to it, at time of writing 3.9.0 is latest
gem 'rspec-rails', '4.0.0.beta2', require: false
gem 'rspec-rails'
gem 'shoulda-matchers', require: false
gem 'rspec-html-matchers'
gem 'pry-nav'
gem 'byebug', require: ENV['RM_INFO'].nil?, platform: :mri
gem 'rubocop', require: false
gem "rubocop-discourse", require: false
gem 'parallel_tests'
gem 'rswag-specs'
end
group :development do
gem 'ruby-prof', require: false, platform: :mri
gem 'bullet', require: !!ENV['BULLET']
gem 'better_errors', platform: :mri
gem 'better_errors', platform: :mri, require: !!ENV['BETTER_ERRORS']
gem 'binding_of_caller'
gem 'yaml-lint'
gem 'annotate'
@ -208,7 +199,7 @@ gem 'htmlentities', require: false
# we are open to it. by deferring require to the initializer we can configure discourse installs without it
gem 'flamegraph', require: false
gem 'rack-mini-profiler', require: false
gem 'rack-mini-profiler', require: ['enable_rails_patches']
gem 'unicorn', require: false, platform: :mri
gem 'puma', require: false
@ -258,3 +249,5 @@ end
gem 'webpush', require: false
gem 'colored2', require: false
gem 'maxminddb'
gem 'rails_failover', require: false

View File

@ -1,21 +1,21 @@
GEM
remote: https://rubygems.org/
specs:
actionmailer (6.0.1)
actionpack (= 6.0.1)
actionview (= 6.0.1)
activejob (= 6.0.1)
actionmailer (6.0.3.1)
actionpack (= 6.0.3.1)
actionview (= 6.0.3.1)
activejob (= 6.0.3.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.0.1)
actionview (= 6.0.1)
activesupport (= 6.0.1)
rack (~> 2.0)
actionpack (6.0.3.1)
actionview (= 6.0.3.1)
activesupport (= 6.0.3.1)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actionview (6.0.1)
activesupport (= 6.0.1)
actionview (6.0.3.1)
activesupport (= 6.0.3.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -24,49 +24,49 @@ GEM
actionview (>= 6.0.a)
active_model_serializers (0.8.4)
activemodel (>= 3.0)
activejob (6.0.1)
activesupport (= 6.0.1)
activejob (6.0.3.1)
activesupport (= 6.0.3.1)
globalid (>= 0.3.6)
activemodel (6.0.1)
activesupport (= 6.0.1)
activerecord (6.0.1)
activemodel (= 6.0.1)
activesupport (= 6.0.1)
activesupport (6.0.1)
activemodel (6.0.3.1)
activesupport (= 6.0.3.1)
activerecord (6.0.3.1)
activemodel (= 6.0.3.1)
activesupport (= 6.0.3.1)
activesupport (6.0.3.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
zeitwerk (~> 2.2)
zeitwerk (~> 2.2, >= 2.2.2)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
annotate (3.1.0)
annotate (3.1.1)
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0)
ast (2.4.0)
aws-eventstream (1.0.3)
aws-partitions (1.272.0)
aws-sdk-core (3.89.1)
aws-eventstream (~> 1.0, >= 1.0.2)
ast (2.4.1)
aws-eventstream (1.1.0)
aws-partitions (1.329.0)
aws-sdk-core (3.99.1)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.29.0)
aws-sdk-kms (1.31.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.60.2)
aws-sdk-core (~> 3, >= 3.83.0)
aws-sdk-s3 (1.66.0)
aws-sdk-core (~> 3, >= 3.96.1)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sdk-sns (1.21.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sdk-sns (1.25.1)
aws-sdk-core (~> 3, >= 3.99.0)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.0)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-sigv4 (1.2.0)
aws-eventstream (~> 1, >= 1.0.2)
barber (0.12.2)
ember-source (>= 1.0, < 3.1)
execjs (>= 1.2, < 3)
better_errors (2.5.1)
better_errors (2.7.1)
coderay (>= 1.0.0)
erubi (>= 1.0.0)
rack (>= 0.9.0)
@ -78,17 +78,17 @@ GEM
bullet (6.1.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
byebug (11.1.1)
byebug (11.1.3)
cbor (0.5.9.6)
certified (1.0.0)
chunky_png (1.3.11)
coderay (1.1.2)
coderay (1.1.3)
colored2 (3.1.2)
concurrent-ruby (1.1.6)
connection_pool (2.2.2)
cose (0.11.0)
connection_pool (2.2.3)
cose (1.0.0)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 0.3.0)
openssl-signature_algorithm (~> 0.4.0)
cppjieba_rb (0.3.3)
crack (0.4.3)
safe_yaml (~> 1.0.0)
@ -96,7 +96,7 @@ GEM
css_parser (1.7.1)
addressable
debug_inspector (0.0.3)
diff-lcs (1.3)
diff-lcs (1.4.1)
diffy (3.3.0)
discourse-ember-source (3.12.2.0)
discourse_image_optim (0.26.2)
@ -106,7 +106,7 @@ GEM
in_threads (~> 1.3)
progress (~> 3.0, >= 3.0.1)
docile (1.3.2)
email_reply_trimmer (0.1.12)
email_reply_trimmer (0.1.13)
ember-data-source (3.0.2)
ember-source (>= 2, < 3.0)
ember-handlebars-template (0.8.0)
@ -121,12 +121,12 @@ GEM
railties (>= 3.1)
ember-source (2.18.2)
erubi (1.9.0)
excon (0.72.0)
excon (0.75.0)
execjs (2.7.0)
exifr (1.3.6)
fabrication (2.21.0)
fabrication (2.21.1)
fakeweb (1.3.0)
faraday (0.17.3)
faraday (1.0.1)
multipart-post (>= 1.2, < 3)
fast_blank (1.0.0)
fast_xor (1.1.3)
@ -134,29 +134,31 @@ GEM
rake-compiler
fast_xs (0.8.0)
fastimage (2.1.7)
ffi (1.12.2)
ffi (1.13.1)
flamegraph (0.9.5)
fspath (3.1.2)
gc_tracer (1.5.1)
globalid (0.4.2)
activesupport (>= 4.2.0)
guess_html_encoding (0.0.11)
hashdiff (1.0.0)
hashie (3.6.0)
hashdiff (1.0.1)
hashie (4.1.0)
highline (1.7.10)
hkdf (0.3.0)
htmlentities (4.3.4)
http_accept_language (2.1.1)
i18n (1.8.2)
i18n (1.8.3)
concurrent-ruby (~> 1.0)
image_size (1.5.0)
in_threads (1.5.4)
jaro_winkler (1.5.4)
jmespath (1.4.0)
jquery-rails (4.3.5)
jquery-rails (4.4.0)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.3.0)
json-schema (2.8.1)
addressable (>= 2.4)
jwt (2.2.1)
kgio (2.11.3)
libv8 (7.3.492.27.1)
@ -171,8 +173,8 @@ GEM
logstash-event (1.2.02)
logstash-logger (0.26.1)
logstash-event (~> 1.2)
logster (2.6.3)
loofah (2.4.0)
logster (2.9.0)
loofah (2.6.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lru_redux (1.1.0)
@ -181,43 +183,41 @@ GEM
mini_mime (>= 0.1.1)
maxminddb (0.1.22)
memory_profiler (0.9.14)
message_bus (2.2.3)
message_bus (3.3.1)
rack (>= 1.1.3)
metaclass (0.0.4)
method_source (0.9.2)
method_source (1.0.0)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
mini_racer (0.2.9)
libv8 (>= 6.9.411)
mini_racer (0.2.14)
libv8 (> 7.3)
mini_scheduler (0.12.2)
sidekiq
mini_sql (0.2.4)
mini_sql (0.2.5)
mini_suffix (0.3.0)
ffi (~> 1.9)
minitest (5.14.0)
mocha (1.8.0)
metaclass (~> 0.0.1)
mock_redis (0.22.0)
minitest (5.14.1)
mocha (1.11.2)
mock_redis (0.24.0)
msgpack (1.3.3)
multi_json (1.14.1)
multi_xml (0.6.0)
multipart-post (2.1.1)
mustache (1.1.1)
nio4r (2.5.2)
nokogiri (1.10.8)
nokogiri (1.10.9)
mini_portile2 (~> 2.4.0)
nokogumbo (2.0.2)
nokogiri (~> 1.8, >= 1.8.4)
oauth (0.5.4)
oauth2 (1.4.2)
oauth2 (1.4.4)
faraday (>= 0.8, < 2.0)
jwt (>= 1.0, < 3.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
oj (3.10.2)
omniauth (1.9.0)
hashie (>= 3.4.6, < 3.7.0)
oj (3.10.6)
omniauth (1.9.1)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
omniauth-facebook (6.0.0)
omniauth-oauth2 (~> 1.2)
@ -240,35 +240,36 @@ GEM
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
onebox (1.9.27.2)
onebox (1.9.29)
addressable (~> 2.7.0)
htmlentities (~> 4.3)
multi_json (~> 1.11)
mustache
nokogiri (~> 1.7)
sanitize
openssl-signature_algorithm (0.3.0)
optimist (3.0.0)
parallel (1.19.1)
parallel_tests (2.31.0)
openssl-signature_algorithm (0.4.0)
optimist (3.0.1)
parallel (1.19.2)
parallel_tests (3.0.0)
parallel
parser (2.7.0.2)
ast (~> 2.4.0)
pg (1.2.2)
parser (2.7.1.4)
ast (~> 2.4.1)
pg (1.2.3)
progress (3.5.2)
pry (0.12.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
pry-nav (0.3.0)
pry (>= 0.9.10, < 0.13.0)
pry (0.13.1)
coderay (~> 1.1)
method_source (~> 1.0)
pry-byebug (3.9.0)
byebug (~> 11.0)
pry (~> 0.13.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.3)
puma (4.3.1)
public_suffix (4.0.5)
puma (4.3.5)
nio4r (~> 2.0)
r2 (0.2.7)
rack (2.0.8)
rack-mini-profiler (1.1.6)
rack (2.2.2)
rack-mini-profiler (2.0.2)
rack (>= 1.2.0)
rack-protection (2.0.8.1)
rack
@ -279,12 +280,15 @@ GEM
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
rails_multisite (2.0.7)
activerecord (> 4.2, < 7)
railties (> 4.2, < 7)
railties (6.0.1)
actionpack (= 6.0.1)
activesupport (= 6.0.1)
rails_failover (0.5.2)
activerecord (~> 6.0)
railties (~> 6.0)
rails_multisite (2.3.0)
activerecord (> 5.0, < 7)
railties (> 5.0, < 7)
railties (6.0.3.1)
actionpack (= 6.0.3.1)
activesupport (= 6.0.3.1)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
@ -293,17 +297,18 @@ GEM
rake (13.0.1)
rake-compiler (1.1.0)
rake
rb-fsevent (0.10.3)
rb-fsevent (0.10.4)
rb-inotify (0.10.1)
ffi (~> 1.0)
rbtrace (0.4.11)
rbtrace (0.4.13)
ffi (>= 1.0.6)
msgpack (>= 0.4.3)
optimist (>= 3.0.0)
rchardet (1.8.0)
redis (4.1.3)
redis (4.2.1)
redis-namespace (1.7.0)
redis (>= 3.0.4)
regexp_parser (1.7.1)
request_store (1.5.0)
rack (>= 1.4)
rexml (3.2.4)
@ -313,14 +318,14 @@ GEM
rqrcode (1.1.2)
chunky_png (~> 1.0)
rqrcode_core (~> 0.1)
rqrcode_core (0.1.1)
rqrcode_core (0.1.2)
rspec (3.9.0)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
rspec-mocks (~> 3.9.0)
rspec-core (3.9.1)
rspec-support (~> 3.9.1)
rspec-expectations (3.9.0)
rspec-core (3.9.2)
rspec-support (~> 3.9.3)
rspec-expectations (3.9.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-html-matchers (0.9.2)
@ -329,34 +334,44 @@ GEM
rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-rails (4.0.0.beta2)
rspec-rails (4.0.1)
actionpack (>= 4.2)
activesupport (>= 4.2)
railties (>= 4.2)
rspec-core (~> 3.8)
rspec-expectations (~> 3.8)
rspec-mocks (~> 3.8)
rspec-support (~> 3.8)
rspec-support (3.9.2)
rspec-core (~> 3.9)
rspec-expectations (~> 3.9)
rspec-mocks (~> 3.9)
rspec-support (~> 3.9)
rspec-support (3.9.3)
rswag-specs (2.3.1)
activesupport (>= 3.1, < 7.0)
json-schema (~> 2.2)
railties (>= 3.1, < 7.0)
rtlit (0.0.5)
rubocop (0.80.0)
jaro_winkler (~> 1.5.1)
rubocop (0.86.0)
parallel (~> 1.10)
parser (>= 2.7.0.1)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.7)
rexml
rubocop-ast (>= 0.0.3, < 1.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7)
rubocop-discourse (1.0.2)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-ast (0.0.3)
parser (>= 2.7.0.1)
rubocop-discourse (2.2.0)
rubocop (>= 0.69.0)
ruby-prof (1.3.0)
rubocop-rspec (>= 1.39.0)
rubocop-rspec (1.40.0)
rubocop (>= 0.68.1)
ruby-prof (1.4.1)
ruby-progressbar (1.10.1)
ruby-readability (0.7.0)
guess_html_encoding (>= 0.0.4)
nokogiri (>= 1.6.0)
rubyzip (2.2.0)
rubyzip (2.3.0)
safe_yaml (1.0.5)
sanitize (5.1.0)
sanitize (5.2.1)
crass (~> 1.0.2)
nokogiri (>= 1.8.0)
nokogumbo (~> 2.0)
@ -374,15 +389,15 @@ GEM
activesupport (>= 3.1)
shoulda-matchers (4.3.0)
activesupport (>= 4.2.0)
sidekiq (6.0.5)
sidekiq (6.0.7)
connection_pool (>= 2.2.2)
rack (~> 2.0)
rack-protection (>= 2.0.0)
redis (>= 4.1.0)
simplecov (0.18.3)
simplecov (0.18.5)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov-html (0.12.1)
simplecov-html (0.12.2)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
@ -396,19 +411,19 @@ GEM
thor (1.0.1)
thread_safe (0.3.6)
tilt (2.0.10)
tzinfo (1.2.6)
tzinfo (1.2.7)
thread_safe (~> 0.1)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.6)
unicode-display_width (1.6.1)
unicorn (5.5.3)
unf_ext (0.0.7.7)
unicode-display_width (1.7.0)
unicorn (5.5.5)
kgio (~> 2.6)
raindrops (~> 0.7)
uniform_notifier (1.13.0)
webmock (3.8.2)
webmock (3.8.3)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@ -416,20 +431,20 @@ GEM
hkdf (~> 0.2)
jwt (~> 2.0)
yaml-lint (0.0.10)
zeitwerk (2.2.2)
zeitwerk (2.3.0)
PLATFORMS
ruby
DEPENDENCIES
actionmailer (= 6.0.1)
actionpack (= 6.0.1)
actionview (= 6.0.1)
actionmailer (= 6.0.3.1)
actionpack (= 6.0.3.1)
actionview (= 6.0.3.1)
actionview_precompiler
active_model_serializers (~> 0.8.3)
activemodel (= 6.0.1)
activerecord (= 6.0.1)
activesupport (= 6.0.1)
activemodel (= 6.0.3.1)
activerecord (= 6.0.3.1)
activesupport (= 6.0.3.1)
addressable
annotate
aws-sdk-s3
@ -465,6 +480,7 @@ DEPENDENCIES
highline (~> 1.7.0)
htmlentities
http_accept_language
json
listen
lograge
logstash-event
@ -482,7 +498,7 @@ DEPENDENCIES
mini_sql
mini_suffix
minitest
mocha (= 1.8.0)
mocha
mock_redis
multi_json
mustache
@ -498,15 +514,16 @@ DEPENDENCIES
onebox
parallel_tests
pg
pry-nav
pry-byebug
pry-rails
puma
r2
rack (= 2.0.8)
rack (= 2.2.2)
rack-mini-profiler
rack-protection
rails_failover
rails_multisite
railties (= 6.0.1)
railties (= 6.0.3.1)
rake
rb-fsevent
rb-inotify (~> 0.9)
@ -519,14 +536,13 @@ DEPENDENCIES
rqrcode
rspec
rspec-html-matchers
rspec-rails (= 4.0.0.beta2)
rspec-rails
rswag-specs
rtlit
rubocop
rubocop-discourse
ruby-prof
ruby-readability
rubyzip
sanitize
sassc (= 2.0.1)
sassc-rails
seed-fu
@ -539,7 +555,6 @@ DEPENDENCIES
stackprof
test-prof
thor
tilt
uglifier
unf
unicorn
@ -548,4 +563,4 @@ DEPENDENCIES
yaml-lint
BUNDLED WITH
2.1.1
2.1.4

View File

@ -34,7 +34,7 @@ To get your environment setup, follow the community setup guide for your operati
If you're familiar with how Rails works and are comfortable setting up your own environment, you can also try out the [**Discourse Advanced Developer Guide**](docs/DEVELOPER-ADVANCED.md), which is aimed primarily at Ubuntu and macOS environments.
Before you get started, ensure you have the following minimum versions: [Ruby 2.5+](https://www.ruby-lang.org/en/downloads/), [PostgreSQL 10+](https://www.postgresql.org/download/), [Redis 2.6+](https://redis.io/download). If you're having trouble, please see our [**TROUBLESHOOTING GUIDE**](docs/TROUBLESHOOTING.md) first!
Before you get started, ensure you have the following minimum versions: [Ruby 2.6+](https://www.ruby-lang.org/en/downloads/), [PostgreSQL 10+](https://www.postgresql.org/download/), [Redis 4.0+](https://redis.io/download). If you're having trouble, please see our [**TROUBLESHOOTING GUIDE**](docs/TROUBLESHOOTING.md) first!
## Setting up Discourse
@ -66,7 +66,7 @@ Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https
## Contributing
[![Build Status](https://api.travis-ci.org/discourse/discourse.svg?branch=master)](https://travis-ci.org/discourse/discourse)
[![Build Status](https://github.com/discourse/discourse/workflows/CI/badge.svg)](https://github.com/discourse/discourse/actions)
Discourse is **100% free** and **open source**. We encourage and support an active, healthy community that
accepts contributions from the public &ndash; including you!
@ -93,7 +93,7 @@ The original Discourse code contributors can be found in [**AUTHORS.MD**](docs/A
## Copyright / License
Copyright 2014 - 2019 Civilized Discourse Construction Kit, Inc.
Copyright 2014 - 2020 Civilized Discourse Construction Kit, Inc.
Licensed under the GNU General Public License Version 2.0 (or later);
you may not use this work except in compliance with the License.

View File

@ -0,0 +1,25 @@
// discourse-skip-module
(function() {
setTimeout(function() {
const $activateButton = $("#activate-account-button");
$activateButton.on("click", function() {
$activateButton.prop("disabled", true);
const hpPath = document.getElementById("data-activate-account").dataset
.path;
$.ajax(hpPath)
.then(function(hp) {
$("#password_confirmation").val(hp.value);
$("#challenge").val(
hp.challenge
.split("")
.reverse()
.join("")
);
$("#activate-account-form").submit();
})
.fail(function() {
$activateButton.prop("disabled", false);
});
});
}, 50);
})();

View File

@ -1,24 +0,0 @@
(function() {
setTimeout(function() {
const $activateButton = $("#activate-account-button");
$activateButton.on("click", function() {
$activateButton.prop("disabled", true);
const hpPath = document.getElementById("data-activate-account").dataset
.path;
$.ajax(hpPath)
.then(function(hp) {
$("#password_confirmation").val(hp.value);
$("#challenge").val(
hp.challenge
.split("")
.reverse()
.join("")
);
$("#activate-account-form").submit();
})
.fail(function() {
$activateButton.prop("disabled", false);
});
});
}, 50);
})();

View File

@ -0,0 +1,122 @@
import Component from "@ember/component";
import loadScript from "discourse/lib/load-script";
import { observes } from "discourse-common/utils/decorators";
import { on } from "@ember/object/evented";
export default Component.extend({
mode: "css",
classNames: ["ace-wrapper"],
_editor: null,
_skipContentChangeEvent: null,
disabled: false,
@observes("editorId")
editorIdChanged() {
if (this.autofocus) {
this.send("focus");
}
},
@observes("content")
contentChanged() {
const content = this.content || "";
if (this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setValue(content);
}
},
@observes("mode")
modeChanged() {
if (this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setMode("ace/mode/" + this.mode);
}
},
@observes("disabled")
disabledStateChanged() {
this.changeDisabledState();
},
changeDisabledState() {
const editor = this._editor;
if (editor) {
const disabled = this.disabled;
editor.setOptions({
readOnly: disabled,
highlightActiveLine: !disabled,
highlightGutterLine: !disabled
});
editor.container.parentNode.setAttribute("data-disabled", disabled);
}
},
_destroyEditor: on("willDestroyElement", function() {
if (this._editor) {
this._editor.destroy();
this._editor = null;
}
if (this.appEvents) {
// xxx: don't run during qunit tests
this.appEvents.off("ace:resize", this, "resize");
}
$(window).off("ace:resize");
}),
resize() {
if (this._editor) {
this._editor.resize();
}
},
didInsertElement() {
this._super(...arguments);
loadScript("/javascripts/ace/ace.js").then(() => {
window.ace.require(["ace/ace"], loadedAce => {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
const editor = loadedAce.edit(this.element.querySelector(".ace"));
editor.setTheme("ace/theme/chrome");
editor.setShowPrintMargin(false);
editor.setOptions({ fontSize: "14px" });
editor.getSession().setMode("ace/mode/" + this.mode);
editor.on("change", () => {
this._skipContentChangeEvent = true;
this.set("content", editor.getSession().getValue());
this._skipContentChangeEvent = false;
});
editor.$blockScrolling = Infinity;
editor.renderer.setScrollMargin(10, 10);
this.element.setAttribute("data-editor", editor);
this._editor = editor;
this.changeDisabledState();
$(window)
.off("ace:resize")
.on("ace:resize", () => this.appEvents.trigger("ace:resize"));
if (this.appEvents) {
// xxx: don't run during qunit tests
this.appEvents.on("ace:resize", this, "resize");
}
if (this.autofocus) {
this.send("focus");
}
});
});
},
actions: {
focus() {
if (this._editor) {
this._editor.focus();
this._editor.navigateFileEnd();
}
}
}
});

View File

@ -1,124 +0,0 @@
import Component from "@ember/component";
import loadScript from "discourse/lib/load-script";
import { observes } from "discourse-common/utils/decorators";
import { on } from "@ember/object/evented";
export default Component.extend({
mode: "css",
classNames: ["ace-wrapper"],
_editor: null,
_skipContentChangeEvent: null,
disabled: false,
@observes("editorId")
editorIdChanged() {
if (this.autofocus) {
this.send("focus");
}
},
@observes("content")
contentChanged() {
const content = this.content || "";
if (this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setValue(content);
}
},
@observes("mode")
modeChanged() {
if (this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setMode("ace/mode/" + this.mode);
}
},
@observes("disabled")
disabledStateChanged() {
this.changeDisabledState();
},
changeDisabledState() {
const editor = this._editor;
if (editor) {
const disabled = this.disabled;
editor.setOptions({
readOnly: disabled,
highlightActiveLine: !disabled,
highlightGutterLine: !disabled
});
editor.container.parentNode.setAttribute("data-disabled", disabled);
}
},
_destroyEditor: on("willDestroyElement", function() {
if (this._editor) {
this._editor.destroy();
this._editor = null;
}
if (this.appEvents) {
// xxx: don't run during qunit tests
this.appEvents.off("ace:resize", this, this.resize);
}
$(window).off("ace:resize");
}),
resize() {
if (this._editor) {
this._editor.resize();
}
},
didInsertElement() {
this._super(...arguments);
loadScript("/javascripts/ace/ace.js").then(() => {
window.ace.require(["ace/ace"], loadedAce => {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
const editor = loadedAce.edit(this.element.querySelector(".ace"));
editor.setTheme("ace/theme/chrome");
editor.setShowPrintMargin(false);
editor.setOptions({ fontSize: "14px" });
editor.getSession().setMode("ace/mode/" + this.mode);
editor.on("change", () => {
this._skipContentChangeEvent = true;
this.set("content", editor.getSession().getValue());
this._skipContentChangeEvent = false;
});
editor.$blockScrolling = Infinity;
editor.renderer.setScrollMargin(10, 10);
this.element.setAttribute("data-editor", editor);
this._editor = editor;
this.changeDisabledState();
$(window)
.off("ace:resize")
.on("ace:resize", () => {
this.appEvents.trigger("ace:resize");
});
if (this.appEvents) {
// xxx: don't run during qunit tests
this.appEvents.on("ace:resize", this, "resize");
}
if (this.autofocus) {
this.send("focus");
}
});
});
},
actions: {
focus() {
if (this._editor) {
this._editor.focus();
this._editor.navigateFileEnd();
}
}
}
});

View File

@ -0,0 +1,74 @@
import I18n from "I18n";
import { scheduleOnce } from "@ember/runloop";
import Component from "@ember/component";
import discourseDebounce from "discourse/lib/debounce";
import { observes, on } from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["admin-backups-logs"],
showLoadingSpinner: false,
hasFormattedLogs: false,
noLogsMessage: I18n.t("admin.backups.logs.none"),
init() {
this._super(...arguments);
this._reset();
},
_reset() {
this.setProperties({ formattedLogs: "", index: 0 });
},
_scrollDown() {
const div = this.element;
div.scrollTop = div.scrollHeight;
},
@on("init")
@observes("logs.[]")
_resetFormattedLogs() {
if (this.logs.length === 0) {
this._reset(); // reset the cached logs whenever the model is reset
this.renderLogs();
}
},
@on("init")
@observes("logs.[]")
_updateFormattedLogs: discourseDebounce(function() {
const logs = this.logs;
if (logs.length === 0) return;
// do the log formatting only once for HELLish performance
let formattedLogs = this.formattedLogs;
for (let i = this.index, length = logs.length; i < length; i++) {
const date = logs[i].get("timestamp"),
message = logs[i].get("message");
formattedLogs += "[" + date + "] " + message + "\n";
}
// update the formatted logs & cache index
this.setProperties({
formattedLogs: formattedLogs,
index: logs.length
});
// force rerender
this.renderLogs();
scheduleOnce("afterRender", this, this._scrollDown);
}, 150),
renderLogs() {
const formattedLogs = this.formattedLogs;
if (formattedLogs && formattedLogs.length > 0) {
this.set("hasFormattedLogs", true);
} else {
this.set("hasFormattedLogs", false);
}
// add a loading indicator
if (this.get("status.isOperationRunning")) {
this.set("showLoadingSpinner", true);
} else {
this.set("showLoadingSpinner", false);
}
}
});

View File

@ -1,73 +0,0 @@
import { scheduleOnce } from "@ember/runloop";
import Component from "@ember/component";
import discourseDebounce from "discourse/lib/debounce";
import { observes, on } from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["admin-backups-logs"],
showLoadingSpinner: false,
hasFormattedLogs: false,
noLogsMessage: I18n.t("admin.backups.logs.none"),
init() {
this._super(...arguments);
this._reset();
},
_reset() {
this.setProperties({ formattedLogs: "", index: 0 });
},
_scrollDown() {
const div = this.element;
div.scrollTop = div.scrollHeight;
},
@on("init")
@observes("logs.[]")
_resetFormattedLogs() {
if (this.logs.length === 0) {
this._reset(); // reset the cached logs whenever the model is reset
this.renderLogs();
}
},
@on("init")
@observes("logs.[]")
_updateFormattedLogs: discourseDebounce(function() {
const logs = this.logs;
if (logs.length === 0) return;
// do the log formatting only once for HELLish performance
let formattedLogs = this.formattedLogs;
for (let i = this.index, length = logs.length; i < length; i++) {
const date = logs[i].get("timestamp"),
message = logs[i].get("message");
formattedLogs += "[" + date + "] " + message + "\n";
}
// update the formatted logs & cache index
this.setProperties({
formattedLogs: formattedLogs,
index: logs.length
});
// force rerender
this.renderLogs();
scheduleOnce("afterRender", this, this._scrollDown);
}, 150),
renderLogs() {
const formattedLogs = this.formattedLogs;
if (formattedLogs && formattedLogs.length > 0) {
this.set("hasFormattedLogs", true);
} else {
this.set("hasFormattedLogs", false);
}
// add a loading indicator
if (this.get("status.isOperationRunning")) {
this.set("showLoadingSpinner", true);
} else {
this.set("showLoadingSpinner", false);
}
}
});

View File

@ -1,30 +0,0 @@
import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
export default Component.extend({
tagName: "th",
classNames: ["sortable"],
chevronIcon: null,
toggleProperties() {
if (this.order === this.field) {
this.set("ascending", this.ascending ? null : true);
} else {
this.setProperties({ order: this.field, ascending: null });
}
},
toggleChevron() {
if (this.order === this.field) {
let chevron = iconHTML(this.ascending ? "chevron-up" : "chevron-down");
this.set("chevronIcon", `${chevron}`.htmlSafe());
} else {
this.set("chevronIcon", null);
}
},
click() {
this.toggleProperties();
},
didReceiveAttrs() {
this._super(...arguments);
this.toggleChevron();
}
});

View File

@ -0,0 +1,167 @@
import { makeArray } from "discourse-common/lib/helpers";
import { debounce, schedule } from "@ember/runloop";
import Component from "@ember/component";
import { number } from "discourse/lib/formatter";
import loadScript from "discourse/lib/load-script";
export default Component.extend({
classNames: ["admin-report-chart"],
limit: 8,
total: 0,
init() {
this._super(...arguments);
this.resizeHandler = () =>
debounce(this, this._scheduleChartRendering, 500);
},
didInsertElement() {
this._super(...arguments);
$(window).on("resize.chart", this.resizeHandler);
},
willDestroyElement() {
this._super(...arguments);
$(window).off("resize.chart", this.resizeHandler);
this._resetChart();
},
didReceiveAttrs() {
this._super(...arguments);
debounce(this, this._scheduleChartRendering, 100);
},
_scheduleChartRendering() {
schedule("afterRender", () => {
this._renderChart(
this.model,
this.element && this.element.querySelector(".chart-canvas")
);
});
},
_renderChart(model, chartCanvas) {
if (!chartCanvas) return;
const context = chartCanvas.getContext("2d");
const chartData = makeArray(model.get("chartData") || model.get("data"));
const prevChartData = makeArray(
model.get("prevChartData") || model.get("prev_data")
);
const labels = chartData.map(d => d.x);
const data = {
labels,
datasets: [
{
data: chartData.map(d => Math.round(parseFloat(d.y))),
backgroundColor: prevChartData.length
? "transparent"
: model.secondary_color,
borderColor: model.primary_color,
pointRadius: 3,
borderWidth: 1,
pointBackgroundColor: model.primary_color,
pointBorderColor: model.primary_color
}
]
};
if (prevChartData.length) {
data.datasets.push({
data: prevChartData.map(d => Math.round(parseFloat(d.y))),
borderColor: model.primary_color,
borderDash: [5, 5],
backgroundColor: "transparent",
borderWidth: 1,
pointRadius: 0
});
}
loadScript("/javascripts/Chart.min.js").then(() => {
this._resetChart();
if (!this.element) {
return;
}
this._chart = new window.Chart(context, this._buildChartConfig(data));
});
},
_buildChartConfig(data) {
return {
type: "line",
data,
options: {
tooltips: {
callbacks: {
title: tooltipItem =>
moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL")
}
},
legend: {
display: false
},
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
animation: {
duration: 0
},
layout: {
padding: {
left: 0,
top: 0,
right: 0,
bottom: 0
}
},
scales: {
yAxes: [
{
display: true,
ticks: {
userCallback: label => {
if (Math.floor(label) === label) return label;
},
callback: label => number(label),
sampleSize: 5,
maxRotation: 25,
minRotation: 25
}
}
],
xAxes: [
{
display: true,
gridLines: { display: false },
type: "time",
time: {
parser: "YYYY-MM-DD"
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50
}
}
]
}
}
};
},
_resetChart() {
if (this._chart) {
this._chart.destroy();
this._chart = null;
}
}
});

View File

@ -1,168 +0,0 @@
import { makeArray } from "discourse-common/lib/helpers";
import { debounce } from "@ember/runloop";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import { number } from "discourse/lib/formatter";
import loadScript from "discourse/lib/load-script";
export default Component.extend({
classNames: ["admin-report-chart"],
limit: 8,
total: 0,
init() {
this._super(...arguments);
this.resizeHandler = () =>
debounce(this, this._scheduleChartRendering, 500);
},
didInsertElement() {
this._super(...arguments);
$(window).on("resize.chart", this.resizeHandler);
},
willDestroyElement() {
this._super(...arguments);
$(window).off("resize.chart", this.resizeHandler);
this._resetChart();
},
didReceiveAttrs() {
this._super(...arguments);
debounce(this, this._scheduleChartRendering, 100);
},
_scheduleChartRendering() {
schedule("afterRender", () => {
this._renderChart(
this.model,
this.element && this.element.querySelector(".chart-canvas")
);
});
},
_renderChart(model, chartCanvas) {
if (!chartCanvas) return;
const context = chartCanvas.getContext("2d");
const chartData = makeArray(model.get("chartData") || model.get("data"));
const prevChartData = makeArray(
model.get("prevChartData") || model.get("prev_data")
);
const labels = chartData.map(d => d.x);
const data = {
labels,
datasets: [
{
data: chartData.map(d => Math.round(parseFloat(d.y))),
backgroundColor: prevChartData.length
? "transparent"
: model.secondary_color,
borderColor: model.primary_color,
pointRadius: 3,
borderWidth: 1,
pointBackgroundColor: model.primary_color,
pointBorderColor: model.primary_color
}
]
};
if (prevChartData.length) {
data.datasets.push({
data: prevChartData.map(d => Math.round(parseFloat(d.y))),
borderColor: model.primary_color,
borderDash: [5, 5],
backgroundColor: "transparent",
borderWidth: 1,
pointRadius: 0
});
}
loadScript("/javascripts/Chart.min.js").then(() => {
this._resetChart();
if (!this.element) {
return;
}
this._chart = new window.Chart(context, this._buildChartConfig(data));
});
},
_buildChartConfig(data) {
return {
type: "line",
data,
options: {
tooltips: {
callbacks: {
title: tooltipItem =>
moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL")
}
},
legend: {
display: false
},
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
animation: {
duration: 0
},
layout: {
padding: {
left: 0,
top: 0,
right: 0,
bottom: 0
}
},
scales: {
yAxes: [
{
display: true,
ticks: {
userCallback: label => {
if (Math.floor(label) === label) return label;
},
callback: label => number(label),
sampleSize: 5,
maxRotation: 25,
minRotation: 25
}
}
],
xAxes: [
{
display: true,
gridLines: { display: false },
type: "time",
time: {
parser: "YYYY-MM-DD"
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50
}
}
]
}
}
};
},
_resetChart() {
if (this._chart) {
this._chart.destroy();
this._chart = null;
}
}
});

View File

@ -0,0 +1,155 @@
import { makeArray } from "discourse-common/lib/helpers";
import { debounce, schedule } from "@ember/runloop";
import Component from "@ember/component";
import { number } from "discourse/lib/formatter";
import loadScript from "discourse/lib/load-script";
export default Component.extend({
classNames: ["admin-report-chart", "admin-report-stacked-chart"],
init() {
this._super(...arguments);
this.resizeHandler = () =>
debounce(this, this._scheduleChartRendering, 500);
},
didInsertElement() {
this._super(...arguments);
$(window).on("resize.chart", this.resizeHandler);
},
willDestroyElement() {
this._super(...arguments);
$(window).off("resize.chart", this.resizeHandler);
this._resetChart();
},
didReceiveAttrs() {
this._super(...arguments);
debounce(this, this._scheduleChartRendering, 100);
},
_scheduleChartRendering() {
schedule("afterRender", () => {
if (!this.element) {
return;
}
this._renderChart(
this.model,
this.element.querySelector(".chart-canvas")
);
});
},
_renderChart(model, chartCanvas) {
if (!chartCanvas) return;
const context = chartCanvas.getContext("2d");
const chartData = makeArray(model.get("chartData") || model.get("data"));
const data = {
labels: chartData[0].data.mapBy("x"),
datasets: chartData.map(cd => {
return {
label: cd.label,
stack: "pageviews-stack",
data: cd.data.map(d => Math.round(parseFloat(d.y))),
backgroundColor: cd.color
};
})
};
loadScript("/javascripts/Chart.min.js").then(() => {
this._resetChart();
this._chart = new window.Chart(context, this._buildChartConfig(data));
});
},
_buildChartConfig(data) {
return {
type: "bar",
data,
options: {
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
hover: { mode: "index" },
animation: {
duration: 0
},
tooltips: {
mode: "index",
intersect: false,
callbacks: {
beforeFooter: tooltipItem => {
let total = 0;
tooltipItem.forEach(
item => (total += parseInt(item.yLabel || 0, 10))
);
return `= ${total}`;
},
title: tooltipItem =>
moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL")
}
},
layout: {
padding: {
left: 0,
top: 0,
right: 0,
bottom: 0
}
},
scales: {
yAxes: [
{
stacked: true,
display: true,
ticks: {
userCallback: label => {
if (Math.floor(label) === label) return label;
},
callback: label => number(label),
sampleSize: 5,
maxRotation: 25,
minRotation: 25
}
}
],
xAxes: [
{
display: true,
gridLines: { display: false },
type: "time",
offset: true,
time: {
parser: "YYYY-MM-DD",
minUnit: "day"
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50
}
}
]
}
}
};
},
_resetChart() {
if (this._chart) {
this._chart.destroy();
this._chart = null;
}
}
});

View File

@ -1,156 +0,0 @@
import { makeArray } from "discourse-common/lib/helpers";
import { debounce } from "@ember/runloop";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import { number } from "discourse/lib/formatter";
import loadScript from "discourse/lib/load-script";
export default Component.extend({
classNames: ["admin-report-chart", "admin-report-stacked-chart"],
init() {
this._super(...arguments);
this.resizeHandler = () =>
debounce(this, this._scheduleChartRendering, 500);
},
didInsertElement() {
this._super(...arguments);
$(window).on("resize.chart", this.resizeHandler);
},
willDestroyElement() {
this._super(...arguments);
$(window).off("resize.chart", this.resizeHandler);
this._resetChart();
},
didReceiveAttrs() {
this._super(...arguments);
debounce(this, this._scheduleChartRendering, 100);
},
_scheduleChartRendering() {
schedule("afterRender", () => {
if (!this.element) {
return;
}
this._renderChart(
this.model,
this.element.querySelector(".chart-canvas")
);
});
},
_renderChart(model, chartCanvas) {
if (!chartCanvas) return;
const context = chartCanvas.getContext("2d");
const chartData = makeArray(model.get("chartData") || model.get("data"));
const data = {
labels: chartData[0].data.mapBy("x"),
datasets: chartData.map(cd => {
return {
label: cd.label,
stack: "pageviews-stack",
data: cd.data.map(d => Math.round(parseFloat(d.y))),
backgroundColor: cd.color
};
})
};
loadScript("/javascripts/Chart.min.js").then(() => {
this._resetChart();
this._chart = new window.Chart(context, this._buildChartConfig(data));
});
},
_buildChartConfig(data) {
return {
type: "bar",
data,
options: {
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
hover: { mode: "index" },
animation: {
duration: 0
},
tooltips: {
mode: "index",
intersect: false,
callbacks: {
beforeFooter: tooltipItem => {
let total = 0;
tooltipItem.forEach(
item => (total += parseInt(item.yLabel || 0, 10))
);
return `= ${total}`;
},
title: tooltipItem =>
moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL")
}
},
layout: {
padding: {
left: 0,
top: 0,
right: 0,
bottom: 0
}
},
scales: {
yAxes: [
{
stacked: true,
display: true,
ticks: {
userCallback: label => {
if (Math.floor(label) === label) return label;
},
callback: label => number(label),
sampleSize: 5,
maxRotation: 25,
minRotation: 25
}
}
],
xAxes: [
{
display: true,
gridLines: { display: false },
type: "time",
offset: true,
time: {
parser: "YYYY-MM-DD",
minUnit: "day"
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50
}
}
]
}
}
};
},
_resetChart() {
if (this._chart) {
this._chart.destroy();
this._chart = null;
}
}
});

View File

@ -0,0 +1,43 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
import { setting } from "discourse/lib/computed";
export default Component.extend({
classNames: ["admin-report-storage-stats"],
backupLocation: setting("backup_location"),
backupStats: alias("model.data.backups"),
uploadStats: alias("model.data.uploads"),
@discourseComputed("backupStats")
showBackupStats(stats) {
return stats && this.currentUser.admin;
},
@discourseComputed("backupLocation")
backupLocationName(backupLocation) {
return I18n.t(`admin.backups.location.${backupLocation}`);
},
@discourseComputed("backupStats.used_bytes")
usedBackupSpace(bytes) {
return I18n.toHumanSize(bytes);
},
@discourseComputed("backupStats.free_bytes")
freeBackupSpace(bytes) {
return I18n.toHumanSize(bytes);
},
@discourseComputed("uploadStats.used_bytes")
usedUploadSpace(bytes) {
return I18n.toHumanSize(bytes);
},
@discourseComputed("uploadStats.free_bytes")
freeUploadSpace(bytes) {
return I18n.toHumanSize(bytes);
}
});

View File

@ -1,42 +0,0 @@
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
import { setting } from "discourse/lib/computed";
export default Component.extend({
classNames: ["admin-report-storage-stats"],
backupLocation: setting("backup_location"),
backupStats: alias("model.data.backups"),
uploadStats: alias("model.data.uploads"),
@discourseComputed("backupStats")
showBackupStats(stats) {
return stats && this.currentUser.admin;
},
@discourseComputed("backupLocation")
backupLocationName(backupLocation) {
return I18n.t(`admin.backups.location.${backupLocation}`);
},
@discourseComputed("backupStats.used_bytes")
usedBackupSpace(bytes) {
return I18n.toHumanSize(bytes);
},
@discourseComputed("backupStats.free_bytes")
freeBackupSpace(bytes) {
return I18n.toHumanSize(bytes);
},
@discourseComputed("uploadStats.used_bytes")
usedUploadSpace(bytes) {
return I18n.toHumanSize(bytes);
},
@discourseComputed("uploadStats.free_bytes")
freeUploadSpace(bytes) {
return I18n.toHumanSize(bytes);
}
});

View File

@ -0,0 +1,413 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { makeArray } from "discourse-common/lib/helpers";
import { alias, or, and, equal, notEmpty, not } from "@ember/object/computed";
import EmberObject, { computed, action } from "@ember/object";
import { next } from "@ember/runloop";
import Component from "@ember/component";
import ReportLoader from "discourse/lib/reports-loader";
import { exportEntity } from "discourse/lib/export-csv";
import { outputExportResult } from "discourse/lib/export-result";
import Report, { SCHEMA_VERSION } from "admin/models/report";
import { isPresent } from "@ember/utils";
import { isTesting } from "discourse-common/config/environment";
const TABLE_OPTIONS = {
perPage: 8,
total: true,
limit: 20,
formatNumbers: true
};
const CHART_OPTIONS = {};
function collapseWeekly(data, average) {
let aggregate = [];
let bucket, i;
let offset = data.length % 7;
for (i = offset; i < data.length; i++) {
if (bucket && i % 7 === offset) {
if (average) {
bucket.y = parseFloat((bucket.y / 7.0).toFixed(2));
}
aggregate.push(bucket);
bucket = null;
}
bucket = bucket || { x: data[i].x, y: 0 };
bucket.y += data[i].y;
}
return aggregate;
}
export default Component.extend({
classNameBindings: [
"isVisible",
"isEnabled",
"isLoading",
"dasherizedDataSourceName"
],
classNames: ["admin-report"],
isEnabled: true,
disabledLabel: I18n.t("admin.dashboard.disabled"),
isLoading: false,
rateLimitationString: null,
dataSourceName: null,
report: null,
model: null,
reportOptions: null,
forcedModes: null,
showAllReportsLink: false,
filters: null,
showTrend: false,
showHeader: true,
showTitle: true,
showFilteringUI: false,
showDatesOptions: alias("model.dates_filtering"),
showRefresh: or("showDatesOptions", "model.available_filters.length"),
shouldDisplayTrend: and("showTrend", "model.prev_period"),
isVisible: not("isHidden"),
init() {
this._super(...arguments);
this._reports = [];
},
isHidden: computed("siteSettings.dashboard_hidden_reports", function() {
return (this.siteSettings.dashboard_hidden_reports || "")
.split("|")
.filter(Boolean)
.includes(this.dataSourceName);
}),
startDate: computed("filters.startDate", function() {
if (this.filters && isPresent(this.filters.startDate)) {
return moment(this.filters.startDate, "YYYY-MM-DD");
} else {
return moment();
}
}),
endDate: computed("filters.endDate", function() {
if (this.filters && isPresent(this.filters.endDate)) {
return moment(this.filters.endDate, "YYYY-MM-DD");
} else {
return moment();
}
}),
didReceiveAttrs() {
this._super(...arguments);
if (this.report) {
this._renderReport(this.report, this.forcedModes, this.currentMode);
} else if (this.dataSourceName) {
this._fetchReport();
}
},
showError: or("showTimeoutError", "showExceptionError", "showNotFoundError"),
showNotFoundError: equal("model.error", "not_found"),
showTimeoutError: equal("model.error", "timeout"),
showExceptionError: equal("model.error", "exception"),
hasData: notEmpty("model.data"),
@discourseComputed("dataSourceName", "model.type")
dasherizedDataSourceName(dataSourceName, type) {
return (dataSourceName || type || "undefined").replace(/_/g, "-");
},
@discourseComputed("dataSourceName", "model.type")
dataSource(dataSourceName, type) {
dataSourceName = dataSourceName || type;
return `/admin/reports/${dataSourceName}`;
},
@discourseComputed("displayedModes.length")
showModes(displayedModesLength) {
return displayedModesLength > 1;
},
@discourseComputed("currentMode", "model.modes", "forcedModes")
displayedModes(currentMode, reportModes, forcedModes) {
const modes = forcedModes ? forcedModes.split(",") : reportModes;
return makeArray(modes).map(mode => {
const base = `btn-default mode-btn ${mode}`;
const cssClass = currentMode === mode ? `${base} is-current` : base;
return {
mode,
cssClass,
icon: mode === "table" ? "table" : "signal"
};
});
},
@discourseComputed("currentMode")
modeComponent(currentMode) {
return `admin-report-${currentMode.replace(/_/g, "-")}`;
},
@discourseComputed(
"dataSourceName",
"startDate",
"endDate",
"filters.customFilters"
)
reportKey(dataSourceName, startDate, endDate, customFilters) {
if (!dataSourceName || !startDate || !endDate) return null;
startDate = startDate.toISOString(true).split("T")[0];
endDate = endDate.toISOString(true).split("T")[0];
let reportKey = "reports:";
reportKey += [
dataSourceName,
isTesting() ? "start" : startDate.replace(/-/g, ""),
isTesting() ? "end" : endDate.replace(/-/g, ""),
"[:prev_period]",
this.get("reportOptions.table.limit"),
// Convert all filter values to strings to ensure unique serialization
customFilters
? JSON.stringify(customFilters, (k, v) => (k ? `${v}` : v))
: null,
SCHEMA_VERSION
]
.filter(x => x)
.map(x => x.toString())
.join(":");
return reportKey;
},
@action
onChangeDateRange(range) {
this.send("refreshReport", {
startDate: range.from,
endDate: range.to
});
},
@action
applyFilter(id, value) {
let customFilters = this.get("filters.customFilters") || {};
if (typeof value === "undefined") {
delete customFilters[id];
} else {
customFilters[id] = value;
}
this.send("refreshReport", {
filters: customFilters
});
},
@action
refreshReport(options = {}) {
this.attrs.onRefresh({
type: this.get("model.type"),
startDate:
typeof options.startDate === "undefined"
? this.startDate
: options.startDate,
endDate:
typeof options.endDate === "undefined" ? this.endDate : options.endDate,
filters:
typeof options.filters === "undefined"
? this.get("filters.customFilters")
: options.filters
});
},
@action
exportCsv() {
const args = {
name: this.get("model.type"),
start_date: this.startDate.toISOString(true).split("T")[0],
end_date: this.endDate.toISOString(true).split("T")[0]
};
const customFilters = this.get("filters.customFilters");
if (customFilters) {
Object.assign(args, customFilters);
}
exportEntity("report", args).then(outputExportResult);
},
@action
changeMode(mode) {
this.set("currentMode", mode);
},
_computeReport() {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
if (!this._reports || !this._reports.length) {
return;
}
// on a slow network _fetchReport could be called multiple times between
// T and T+x, and all the ajax responses would occur after T+(x+y)
// to avoid any inconsistencies we filter by period and make sure
// the array contains only unique values
let filteredReports = this._reports.uniqBy("report_key");
let report;
const sort = r => {
if (r.length > 1) {
return r.findBy("type", this.dataSourceName);
} else {
return r;
}
};
if (!this.startDate || !this.endDate) {
report = sort(filteredReports)[0];
} else {
report = sort(
filteredReports.filter(r => r.report_key.includes(this.reportKey))
)[0];
if (!report) return;
}
if (report.error === "not_found") {
this.set("showFilteringUI", false);
}
this._renderReport(report, this.forcedModes, this.currentMode);
},
_renderReport(report, forcedModes, currentMode) {
const modes = forcedModes ? forcedModes.split(",") : report.modes;
currentMode = currentMode || (modes ? modes[0] : null);
this.setProperties({
model: report,
currentMode,
options: this._buildOptions(currentMode)
});
},
_fetchReport() {
this._super(...arguments);
this.setProperties({ isLoading: true, rateLimitationString: null });
next(() => {
let payload = this._buildPayload(["prev_period"]);
const callback = response => {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
this.set("isLoading", false);
if (response === 429) {
this.set(
"rateLimitationString",
I18n.t("admin.dashboard.too_many_requests")
);
} else if (response === 500) {
this.set("model.error", "exception");
} else if (response) {
this._reports.push(this._loadReport(response));
this._computeReport();
}
};
ReportLoader.enqueue(this.dataSourceName, payload.data, callback);
});
},
_buildPayload(facets) {
let payload = { data: { cache: true, facets } };
if (this.startDate) {
payload.data.start_date = moment(this.startDate)
.toISOString(true)
.split("T")[0];
}
if (this.endDate) {
payload.data.end_date = moment(this.endDate)
.toISOString(true)
.split("T")[0];
}
if (this.get("reportOptions.table.limit")) {
payload.data.limit = this.get("reportOptions.table.limit");
}
if (this.get("filters.customFilters")) {
payload.data.filters = this.get("filters.customFilters");
}
return payload;
},
_buildOptions(mode) {
if (mode === "table") {
const tableOptions = JSON.parse(JSON.stringify(TABLE_OPTIONS));
return EmberObject.create(
Object.assign(tableOptions, this.get("reportOptions.table") || {})
);
} else {
const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS));
return EmberObject.create(
Object.assign(chartOptions, this.get("reportOptions.chart") || {})
);
}
},
_loadReport(jsonReport) {
Report.fillMissingDates(jsonReport, { filledField: "chartData" });
if (jsonReport.chartData && jsonReport.modes[0] === "stacked_chart") {
jsonReport.chartData = jsonReport.chartData.map(chartData => {
if (chartData.length > 40) {
return {
data: collapseWeekly(chartData.data),
req: chartData.req,
label: chartData.label,
color: chartData.color
};
} else {
return chartData;
}
});
} else if (jsonReport.chartData && jsonReport.chartData.length > 40) {
jsonReport.chartData = collapseWeekly(
jsonReport.chartData,
jsonReport.average
);
}
if (jsonReport.prev_data) {
Report.fillMissingDates(jsonReport, {
filledField: "prevChartData",
dataField: "prev_data",
starDate: jsonReport.prev_startDate,
endDate: jsonReport.prev_endDate
});
if (jsonReport.prevChartData && jsonReport.prevChartData.length > 40) {
jsonReport.prevChartData = collapseWeekly(
jsonReport.prevChartData,
jsonReport.average
);
}
}
return Report.create(jsonReport);
}
});

View File

@ -1,421 +0,0 @@
import discourseComputed from "discourse-common/utils/decorators";
import { makeArray } from "discourse-common/lib/helpers";
import { alias, or, and, reads, equal, notEmpty } from "@ember/object/computed";
import EmberObject from "@ember/object";
import { next } from "@ember/runloop";
import Component from "@ember/component";
import ReportLoader from "discourse/lib/reports-loader";
import { exportEntity } from "discourse/lib/export-csv";
import { outputExportResult } from "discourse/lib/export-result";
import { isNumeric } from "discourse/lib/utilities";
import Report, { SCHEMA_VERSION } from "admin/models/report";
import ENV from "discourse-common/config/environment";
const TABLE_OPTIONS = {
perPage: 8,
total: true,
limit: 20,
formatNumbers: true
};
const CHART_OPTIONS = {};
function collapseWeekly(data, average) {
let aggregate = [];
let bucket, i;
let offset = data.length % 7;
for (i = offset; i < data.length; i++) {
if (bucket && i % 7 === offset) {
if (average) {
bucket.y = parseFloat((bucket.y / 7.0).toFixed(2));
}
aggregate.push(bucket);
bucket = null;
}
bucket = bucket || { x: data[i].x, y: 0 };
bucket.y += data[i].y;
}
return aggregate;
}
export default Component.extend({
classNameBindings: ["isEnabled", "isLoading", "dasherizedDataSourceName"],
classNames: ["admin-report"],
isEnabled: true,
disabledLabel: I18n.t("admin.dashboard.disabled"),
isLoading: false,
rateLimitationString: null,
dataSourceName: null,
report: null,
model: null,
reportOptions: null,
forcedModes: null,
showAllReportsLink: false,
filters: null,
startDate: null,
endDate: null,
showTrend: false,
showHeader: true,
showTitle: true,
showFilteringUI: false,
showDatesOptions: alias("model.dates_filtering"),
showRefresh: or("showDatesOptions", "model.available_filters.length"),
shouldDisplayTrend: and("showTrend", "model.prev_period"),
init() {
this._super(...arguments);
this._reports = [];
},
startDate: reads("filters.startDate"),
endDate: reads("filters.endDate"),
didReceiveAttrs() {
this._super(...arguments);
if (this.report) {
this._renderReport(this.report, this.forcedModes, this.currentMode);
} else if (this.dataSourceName) {
this._fetchReport();
}
},
showError: or("showTimeoutError", "showExceptionError", "showNotFoundError"),
showNotFoundError: equal("model.error", "not_found"),
showTimeoutError: equal("model.error", "timeout"),
showExceptionError: equal("model.error", "exception"),
hasData: notEmpty("model.data"),
@discourseComputed("dataSourceName", "model.type")
dasherizedDataSourceName(dataSourceName, type) {
return (dataSourceName || type || "undefined").replace(/_/g, "-");
},
@discourseComputed("dataSourceName", "model.type")
dataSource(dataSourceName, type) {
dataSourceName = dataSourceName || type;
return `/admin/reports/${dataSourceName}`;
},
@discourseComputed("displayedModes.length")
showModes(displayedModesLength) {
return displayedModesLength > 1;
},
@discourseComputed("currentMode", "model.modes", "forcedModes")
displayedModes(currentMode, reportModes, forcedModes) {
const modes = forcedModes ? forcedModes.split(",") : reportModes;
return makeArray(modes).map(mode => {
const base = `btn-default mode-btn ${mode}`;
const cssClass = currentMode === mode ? `${base} is-current` : base;
return {
mode,
cssClass,
icon: mode === "table" ? "table" : "signal"
};
});
},
@discourseComputed("currentMode")
modeComponent(currentMode) {
return `admin-report-${currentMode.replace(/_/g, "-")}`;
},
@discourseComputed("startDate")
normalizedStartDate(startDate) {
return startDate && typeof startDate.isValid === "function"
? moment
.utc(startDate.toISOString())
.locale("en")
.format("YYYYMMDD")
: moment(startDate)
.locale("en")
.format("YYYYMMDD");
},
@discourseComputed("endDate")
normalizedEndDate(endDate) {
return endDate && typeof endDate.isValid === "function"
? moment
.utc(endDate.toISOString())
.locale("en")
.format("YYYYMMDD")
: moment(endDate)
.locale("en")
.format("YYYYMMDD");
},
@discourseComputed(
"dataSourceName",
"normalizedStartDate",
"normalizedEndDate",
"filters.customFilters"
)
reportKey(dataSourceName, startDate, endDate, customFilters) {
if (!dataSourceName || !startDate || !endDate) return null;
let reportKey = "reports:";
reportKey += [
dataSourceName,
ENV.environment === "test" ? "start" : startDate.replace(/-/g, ""),
ENV.environment === "test" ? "end" : endDate.replace(/-/g, ""),
"[:prev_period]",
this.get("reportOptions.table.limit"),
customFilters
? JSON.stringify(customFilters, (key, value) =>
isNumeric(value) ? value.toString() : value
)
: null,
SCHEMA_VERSION
]
.filter(x => x)
.map(x => x.toString())
.join(":");
return reportKey;
},
actions: {
onChangeEndDate(date) {
const startDate = moment(this.startDate);
const newEndDate = moment(date).endOf("day");
if (newEndDate.isSameOrAfter(startDate)) {
this.set("endDate", newEndDate.format("YYYY-MM-DD"));
} else {
this.set("endDate", startDate.endOf("day").format("YYYY-MM-DD"));
}
this.send("refreshReport");
},
onChangeStartDate(date) {
const endDate = moment(this.endDate);
const newStartDate = moment(date).startOf("day");
if (newStartDate.isSameOrBefore(endDate)) {
this.set("startDate", newStartDate.format("YYYY-MM-DD"));
} else {
this.set("startDate", endDate.startOf("day").format("YYYY-MM-DD"));
}
this.send("refreshReport");
},
applyFilter(id, value) {
let customFilters = this.get("filters.customFilters") || {};
if (typeof value === "undefined") {
delete customFilters[id];
} else {
customFilters[id] = value;
}
this.attrs.onRefresh({
type: this.get("model.type"),
startDate: this.startDate,
endDate: this.endDate,
filters: customFilters
});
},
refreshReport() {
this.attrs.onRefresh({
type: this.get("model.type"),
startDate: this.startDate,
endDate: this.endDate,
filters: this.get("filters.customFilters")
});
},
exportCsv() {
const customFilters = this.get("filters.customFilters") || {};
exportEntity("report", {
name: this.get("model.type"),
start_date: this.startDate,
end_date: this.endDate,
category_id: customFilters.category,
group_id: customFilters.group
}).then(outputExportResult);
},
changeMode(mode) {
this.set("currentMode", mode);
}
},
_computeReport() {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
if (!this._reports || !this._reports.length) {
return;
}
// on a slow network _fetchReport could be called multiple times between
// T and T+x, and all the ajax responses would occur after T+(x+y)
// to avoid any inconsistencies we filter by period and make sure
// the array contains only unique values
let filteredReports = this._reports.uniqBy("report_key");
let report;
const sort = r => {
if (r.length > 1) {
return r.findBy("type", this.dataSourceName);
} else {
return r;
}
};
if (!this.startDate || !this.endDate) {
report = sort(filteredReports)[0];
} else {
const reportKey = this.reportKey;
report = sort(
filteredReports.filter(r => r.report_key.includes(reportKey))
)[0];
if (!report) return;
}
if (report.error === "not_found") {
this.set("showFilteringUI", false);
}
this._renderReport(report, this.forcedModes, this.currentMode);
},
_renderReport(report, forcedModes, currentMode) {
const modes = forcedModes ? forcedModes.split(",") : report.modes;
currentMode = currentMode || (modes ? modes[0] : null);
this.setProperties({
model: report,
currentMode,
options: this._buildOptions(currentMode)
});
},
_fetchReport() {
this._super(...arguments);
this.setProperties({ isLoading: true, rateLimitationString: null });
next(() => {
let payload = this._buildPayload(["prev_period"]);
const callback = response => {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
this.set("isLoading", false);
if (response === 429) {
this.set(
"rateLimitationString",
I18n.t("admin.dashboard.too_many_requests")
);
} else if (response === 500) {
this.set("model.error", "exception");
} else if (response) {
this._reports.push(this._loadReport(response));
this._computeReport();
}
};
ReportLoader.enqueue(this.dataSourceName, payload.data, callback);
});
},
_buildPayload(facets) {
let payload = { data: { cache: true, facets } };
if (this.startDate) {
payload.data.start_date = moment
.utc(this.startDate, "YYYY-MM-DD")
.toISOString();
}
if (this.endDate) {
payload.data.end_date = moment
.utc(this.endDate, "YYYY-MM-DD")
.toISOString();
}
if (this.get("reportOptions.table.limit")) {
payload.data.limit = this.get("reportOptions.table.limit");
}
if (this.get("filters.customFilters")) {
payload.data.filters = this.get("filters.customFilters");
}
return payload;
},
_buildOptions(mode) {
if (mode === "table") {
const tableOptions = JSON.parse(JSON.stringify(TABLE_OPTIONS));
return EmberObject.create(
Object.assign(tableOptions, this.get("reportOptions.table") || {})
);
} else {
const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS));
return EmberObject.create(
Object.assign(chartOptions, this.get("reportOptions.chart") || {})
);
}
},
_loadReport(jsonReport) {
Report.fillMissingDates(jsonReport, { filledField: "chartData" });
if (jsonReport.chartData && jsonReport.modes[0] === "stacked_chart") {
jsonReport.chartData = jsonReport.chartData.map(chartData => {
if (chartData.length > 40) {
return {
data: collapseWeekly(chartData.data),
req: chartData.req,
label: chartData.label,
color: chartData.color
};
} else {
return chartData;
}
});
} else if (jsonReport.chartData && jsonReport.chartData.length > 40) {
jsonReport.chartData = collapseWeekly(
jsonReport.chartData,
jsonReport.average
);
}
if (jsonReport.prev_data) {
Report.fillMissingDates(jsonReport, {
filledField: "prevChartData",
dataField: "prev_data",
starDate: jsonReport.prev_startDate,
endDate: jsonReport.prev_endDate
});
if (jsonReport.prevChartData && jsonReport.prevChartData.length > 40) {
jsonReport.prevChartData = collapseWeekly(
jsonReport.prevChartData,
jsonReport.average
);
}
}
return Report.create(jsonReport);
}
});

View File

@ -0,0 +1,108 @@
import I18n from "I18n";
import { isEmpty } from "@ember/utils";
import { empty } from "@ember/object/computed";
import { scheduleOnce } from "@ember/runloop";
import Component from "@ember/component";
import UserField from "admin/models/user-field";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { propertyEqual } from "discourse/lib/computed";
import { i18n } from "discourse/lib/computed";
import discourseComputed, {
observes,
on
} from "discourse-common/utils/decorators";
export default Component.extend(bufferedProperty("userField"), {
editing: empty("userField.id"),
classNameBindings: [":user-field"],
cantMoveUp: propertyEqual("userField", "firstField"),
cantMoveDown: propertyEqual("userField", "lastField"),
userFieldsDescription: i18n("admin.user_fields.description"),
@discourseComputed("buffered.field_type")
bufferedFieldType(fieldType) {
return UserField.fieldTypeById(fieldType);
},
@on("didInsertElement")
@observes("editing")
_focusOnEdit() {
if (this.editing) {
scheduleOnce("afterRender", this, "_focusName");
}
},
_focusName() {
$(".user-field-name").select();
},
@discourseComputed("userField.field_type")
fieldName(fieldType) {
return UserField.fieldTypeById(fieldType).get("name");
},
@discourseComputed(
"userField.editable",
"userField.required",
"userField.show_on_profile",
"userField.show_on_user_card"
)
flags(editable, required, showOnProfile, showOnUserCard) {
const ret = [];
if (editable) {
ret.push(I18n.t("admin.user_fields.editable.enabled"));
}
if (required) {
ret.push(I18n.t("admin.user_fields.required.enabled"));
}
if (showOnProfile) {
ret.push(I18n.t("admin.user_fields.show_on_profile.enabled"));
}
if (showOnUserCard) {
ret.push(I18n.t("admin.user_fields.show_on_user_card.enabled"));
}
return ret.join(", ");
},
actions: {
save() {
const buffered = this.buffered;
const attrs = buffered.getProperties(
"name",
"description",
"field_type",
"editable",
"required",
"show_on_profile",
"show_on_user_card",
"options"
);
this.userField
.save(attrs)
.then(() => {
this.set("editing", false);
this.commitBuffer();
})
.catch(popupAjaxError);
},
edit() {
this.set("editing", true);
},
cancel() {
const id = this.get("userField.id");
if (isEmpty(id)) {
this.destroyAction(this.userField);
} else {
this.rollbackBuffer();
this.set("editing", false);
}
}
}
});

View File

@ -1,107 +0,0 @@
import { isEmpty } from "@ember/utils";
import { empty } from "@ember/object/computed";
import { scheduleOnce } from "@ember/runloop";
import Component from "@ember/component";
import UserField from "admin/models/user-field";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { propertyEqual } from "discourse/lib/computed";
import { i18n } from "discourse/lib/computed";
import discourseComputed, {
observes,
on
} from "discourse-common/utils/decorators";
export default Component.extend(bufferedProperty("userField"), {
editing: empty("userField.id"),
classNameBindings: [":user-field"],
cantMoveUp: propertyEqual("userField", "firstField"),
cantMoveDown: propertyEqual("userField", "lastField"),
userFieldsDescription: i18n("admin.user_fields.description"),
@discourseComputed("buffered.field_type")
bufferedFieldType(fieldType) {
return UserField.fieldTypeById(fieldType);
},
@on("didInsertElement")
@observes("editing")
_focusOnEdit() {
if (this.editing) {
scheduleOnce("afterRender", this, "_focusName");
}
},
_focusName() {
$(".user-field-name").select();
},
@discourseComputed("userField.field_type")
fieldName(fieldType) {
return UserField.fieldTypeById(fieldType).get("name");
},
@discourseComputed(
"userField.editable",
"userField.required",
"userField.show_on_profile",
"userField.show_on_user_card"
)
flags(editable, required, showOnProfile, showOnUserCard) {
const ret = [];
if (editable) {
ret.push(I18n.t("admin.user_fields.editable.enabled"));
}
if (required) {
ret.push(I18n.t("admin.user_fields.required.enabled"));
}
if (showOnProfile) {
ret.push(I18n.t("admin.user_fields.show_on_profile.enabled"));
}
if (showOnUserCard) {
ret.push(I18n.t("admin.user_fields.show_on_user_card.enabled"));
}
return ret.join(", ");
},
actions: {
save() {
const buffered = this.buffered;
const attrs = buffered.getProperties(
"name",
"description",
"field_type",
"editable",
"required",
"show_on_profile",
"show_on_user_card",
"options"
);
this.userField
.save(attrs)
.then(() => {
this.set("editing", false);
this.commitBuffer();
})
.catch(popupAjaxError);
},
edit() {
this.set("editing", true);
},
cancel() {
const id = this.get("userField.id");
if (isEmpty(id)) {
this.destroyAction(this.userField);
} else {
this.rollbackBuffer();
this.set("editing", false);
}
}
}
});

View File

@ -0,0 +1,29 @@
import I18n from "I18n";
import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
export default Component.extend({
classNames: ["watched-word"],
watchedWord: null,
xIcon: iconHTML("times").htmlSafe(),
init() {
this._super(...arguments);
this.set("watchedWord", this.get("word.word"));
},
click() {
this.word
.destroy()
.then(() => {
this.action(this.word);
})
.catch(e => {
bootbox.alert(
I18n.t("generic_error_with_reason", {
error: `http: ${e.status} - ${e.body}`
})
);
});
}
});

View File

@ -1,28 +0,0 @@
import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
export default Component.extend({
classNames: ["watched-word"],
watchedWord: null,
xIcon: iconHTML("times").htmlSafe(),
init() {
this._super(...arguments);
this.set("watchedWord", this.get("word.word"));
},
click() {
this.word
.destroy()
.then(() => {
this.action(this.word);
})
.catch(e => {
bootbox.alert(
I18n.t("generic_error_with_reason", {
error: `http: ${e.status} - ${e.body}`
})
);
});
}
});

View File

@ -0,0 +1,47 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
export default Component.extend({
classNames: ["hook-event"],
typeName: alias("type.name"),
@discourseComputed("typeName")
name(typeName) {
return I18n.t(`admin.web_hooks.${typeName}_event.name`);
},
@discourseComputed("typeName")
details(typeName) {
return I18n.t(`admin.web_hooks.${typeName}_event.details`);
},
@discourseComputed("model.[]", "typeName")
eventTypeExists(eventTypes, typeName) {
return eventTypes.any(event => event.name === typeName);
},
@discourseComputed("eventTypeExists")
enabled: {
get(eventTypeExists) {
return eventTypeExists;
},
set(value, eventTypeExists) {
const type = this.type;
const model = this.model;
// add an association when not exists
if (value !== eventTypeExists) {
if (value) {
model.addObject(type);
} else {
model.removeObjects(
model.filter(eventType => eventType.name === type.name)
);
}
}
return value;
}
}
});

View File

@ -1,46 +0,0 @@
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
export default Component.extend({
classNames: ["hook-event"],
typeName: alias("type.name"),
@discourseComputed("typeName")
name(typeName) {
return I18n.t(`admin.web_hooks.${typeName}_event.name`);
},
@discourseComputed("typeName")
details(typeName) {
return I18n.t(`admin.web_hooks.${typeName}_event.details`);
},
@discourseComputed("model.[]", "typeName")
eventTypeExists(eventTypes, typeName) {
return eventTypes.any(event => event.name === typeName);
},
@discourseComputed("eventTypeExists")
enabled: {
get(eventTypeExists) {
return eventTypeExists;
},
set(value, eventTypeExists) {
const type = this.type;
const model = this.model;
// add an association when not exists
if (value !== eventTypeExists) {
if (value) {
model.addObject(type);
} else {
model.removeObjects(
model.filter(eventType => eventType.name === type.name)
);
}
}
return value;
}
}
});

View File

@ -0,0 +1,110 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter";
export default Component.extend({
tagName: "li",
expandDetails: null,
expandDetailsRequestKey: "request",
expandDetailsResponseKey: "response",
@discourseComputed("model.status")
statusColorClasses(status) {
if (!status) return "";
if (status >= 200 && status <= 299) {
return "text-successful";
} else {
return "text-danger";
}
},
@discourseComputed("model.created_at")
createdAt(createdAt) {
return moment(createdAt).format("YYYY-MM-DD HH:mm:ss");
},
@discourseComputed("model.duration")
completion(duration) {
const seconds = Math.floor(duration / 10.0) / 100.0;
return I18n.t("admin.web_hooks.events.completed_in", { count: seconds });
},
@discourseComputed("expandDetails")
expandRequestIcon(expandDetails) {
return expandDetails === this.expandDetailsRequestKey
? "ellipsis-h"
: "ellipsis-v";
},
@discourseComputed("expandDetails")
expandResponseIcon(expandDetails) {
return expandDetails === this.expandDetailsResponseKey
? "ellipsis-h"
: "ellipsis-v";
},
actions: {
redeliver() {
return bootbox.confirm(
I18n.t("admin.web_hooks.events.redeliver_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
result => {
if (result) {
ajax(
`/admin/api/web_hooks/${this.get(
"model.web_hook_id"
)}/events/${this.get("model.id")}/redeliver`,
{ type: "POST" }
)
.then(json => {
this.set("model", json.web_hook_event);
})
.catch(popupAjaxError);
}
}
);
},
toggleRequest() {
const expandDetailsKey = this.expandDetailsRequestKey;
if (this.expandDetails !== expandDetailsKey) {
let headers = _.extend(
{
"Request URL": this.get("model.request_url"),
"Request method": "POST"
},
ensureJSON(this.get("model.headers"))
);
this.setProperties({
headers: plainJSON(headers),
body: prettyJSON(this.get("model.payload")),
expandDetails: expandDetailsKey,
bodyLabel: I18n.t("admin.web_hooks.events.payload")
});
} else {
this.set("expandDetails", null);
}
},
toggleResponse() {
const expandDetailsKey = this.expandDetailsResponseKey;
if (this.expandDetails !== expandDetailsKey) {
this.setProperties({
headers: plainJSON(this.get("model.response_headers")),
body: this.get("model.response_body"),
expandDetails: expandDetailsKey,
bodyLabel: I18n.t("admin.web_hooks.events.body")
});
} else {
this.set("expandDetails", null);
}
}
}
});

View File

@ -1,109 +0,0 @@
import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter";
export default Component.extend({
tagName: "li",
expandDetails: null,
expandDetailsRequestKey: "request",
expandDetailsResponseKey: "response",
@discourseComputed("model.status")
statusColorClasses(status) {
if (!status) return "";
if (status >= 200 && status <= 299) {
return "text-successful";
} else {
return "text-danger";
}
},
@discourseComputed("model.created_at")
createdAt(createdAt) {
return moment(createdAt).format("YYYY-MM-DD HH:mm:ss");
},
@discourseComputed("model.duration")
completion(duration) {
const seconds = Math.floor(duration / 10.0) / 100.0;
return I18n.t("admin.web_hooks.events.completed_in", { count: seconds });
},
@discourseComputed("expandDetails")
expandRequestIcon(expandDetails) {
return expandDetails === this.expandDetailsRequestKey
? "ellipsis-h"
: "ellipsis-v";
},
@discourseComputed("expandDetails")
expandResponseIcon(expandDetails) {
return expandDetails === this.expandDetailsResponseKey
? "ellipsis-h"
: "ellipsis-v";
},
actions: {
redeliver() {
return bootbox.confirm(
I18n.t("admin.web_hooks.events.redeliver_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
result => {
if (result) {
ajax(
`/admin/api/web_hooks/${this.get(
"model.web_hook_id"
)}/events/${this.get("model.id")}/redeliver`,
{ type: "POST" }
)
.then(json => {
this.set("model", json.web_hook_event);
})
.catch(popupAjaxError);
}
}
);
},
toggleRequest() {
const expandDetailsKey = this.expandDetailsRequestKey;
if (this.expandDetails !== expandDetailsKey) {
let headers = _.extend(
{
"Request URL": this.get("model.request_url"),
"Request method": "POST"
},
ensureJSON(this.get("model.headers"))
);
this.setProperties({
headers: plainJSON(headers),
body: prettyJSON(this.get("model.payload")),
expandDetails: expandDetailsKey,
bodyLabel: I18n.t("admin.web_hooks.events.payload")
});
} else {
this.set("expandDetails", null);
}
},
toggleResponse() {
const expandDetailsKey = this.expandDetailsResponseKey;
if (this.expandDetails !== expandDetailsKey) {
this.setProperties({
headers: plainJSON(this.get("model.response_headers")),
body: this.get("model.response_body"),
expandDetails: expandDetailsKey,
bodyLabel: I18n.t("admin.web_hooks.events.body")
});
} else {
this.set("expandDetails", null);
}
}
}
});

View File

@ -0,0 +1,38 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
export default Component.extend({
classes: ["text-muted", "text-danger", "text-successful", "text-muted"],
icons: ["far-circle", "times-circle", "circle", "circle"],
circleIcon: null,
deliveryStatus: null,
@discourseComputed("deliveryStatuses", "model.last_delivery_status")
status(deliveryStatuses, lastDeliveryStatus) {
return deliveryStatuses.find(s => s.id === lastDeliveryStatus);
},
@discourseComputed("status.id", "icons")
icon(statusId, icons) {
return icons[statusId - 1];
},
@discourseComputed("status.id", "classes")
class(statusId, classes) {
return classes[statusId - 1];
},
didReceiveAttrs() {
this._super(...arguments);
this.set(
"circleIcon",
iconHTML(this.icon, { class: this.class }).htmlSafe()
);
this.set(
"deliveryStatus",
I18n.t(`admin.web_hooks.delivery_status.${this.get("status.name")}`)
);
}
});

View File

@ -1,37 +0,0 @@
import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
export default Component.extend({
classes: ["text-muted", "text-danger", "text-successful", "text-muted"],
icons: ["far-circle", "times-circle", "circle", "circle"],
circleIcon: null,
deliveryStatus: null,
@discourseComputed("deliveryStatuses", "model.last_delivery_status")
status(deliveryStatuses, lastDeliveryStatus) {
return deliveryStatuses.find(s => s.id === lastDeliveryStatus);
},
@discourseComputed("status.id", "icons")
icon(statusId, icons) {
return icons[statusId - 1];
},
@discourseComputed("status.id", "classes")
class(statusId, classes) {
return classes[statusId - 1];
},
didReceiveAttrs() {
this._super(...arguments);
this.set(
"circleIcon",
iconHTML(this.icon, { class: this.class }).htmlSafe()
);
this.set(
"deliveryStatus",
I18n.t(`admin.web_hooks.delivery_status.${this.get("status.name")}`)
);
}
});

View File

@ -0,0 +1,73 @@
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import { computed, action } from "@ember/object";
import loadScript, { loadCSS } from "discourse/lib/load-script";
import { observes } from "discourse-common/utils/decorators";
/**
An input field for a color.
@param hexValue is a reference to the color's hex value.
@param brightnessValue is a number from 0 to 255 representing the brightness of the color. See ColorSchemeColor.
@params valid is a boolean indicating if the input field is a valid color.
**/
export default Component.extend({
classNames: ["color-picker"],
onlyHex: true,
styleSelection: true,
maxlength: computed("onlyHex", function() {
return this.onlyHex ? 6 : null;
}),
@action
onHexInput(color) {
this.attrs.onChangeColor && this.attrs.onChangeColor(color || "");
},
@observes("hexValue", "brightnessValue", "valid")
hexValueChanged: function() {
const hex = this.hexValue;
let text = this.element.querySelector("input.hex-input");
this.attrs.onChangeColor && this.attrs.onChangeColor(hex);
if (this.valid) {
this.styleSelection &&
text.setAttribute(
"style",
"color: " +
(this.brightnessValue > 125 ? "black" : "white") +
"; background-color: #" +
hex +
";"
);
if (this.pickerLoaded) {
$(this.element.querySelector(".picker")).spectrum({
color: "#" + hex
});
}
} else {
this.styleSelection && text.setAttribute("style", "");
}
},
didInsertElement() {
loadScript("/javascripts/spectrum.js").then(() => {
loadCSS("/javascripts/spectrum.css").then(() => {
schedule("afterRender", () => {
$(this.element.querySelector(".picker"))
.spectrum({ color: "#" + this.hexValue })
.on("change.spectrum", (me, color) => {
this.set("hexValue", color.toHexString().replace("#", ""));
});
this.set("pickerLoaded", true);
});
});
});
schedule("afterRender", () => this.hexValueChanged());
}
});

View File

@ -1,58 +0,0 @@
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import loadScript, { loadCSS } from "discourse/lib/load-script";
import { observes } from "discourse-common/utils/decorators";
/**
An input field for a color.
@param hexValue is a reference to the color's hex value.
@param brightnessValue is a number from 0 to 255 representing the brightness of the color. See ColorSchemeColor.
@params valid is a boolean indicating if the input field is a valid color.
**/
export default Component.extend({
classNames: ["color-picker"],
@observes("hexValue", "brightnessValue", "valid")
hexValueChanged: function() {
var hex = this.hexValue;
let text = this.element.querySelector("input.hex-input");
if (this.valid) {
text.setAttribute(
"style",
"color: " +
(this.brightnessValue > 125 ? "black" : "white") +
"; background-color: #" +
hex +
";"
);
if (this.pickerLoaded) {
$(this.element.querySelector(".picker")).spectrum({
color: "#" + this.hexValue
});
}
} else {
text.setAttribute("style", "");
}
},
didInsertElement() {
loadScript("/javascripts/spectrum.js").then(() => {
loadCSS("/javascripts/spectrum.css").then(() => {
schedule("afterRender", () => {
$(this.element.querySelector(".picker"))
.spectrum({ color: "#" + this.hexValue })
.on("change.spectrum", (me, color) => {
this.set("hexValue", color.toHexString().replace("#", ""));
});
this.set("pickerLoaded", true);
});
});
});
schedule("afterRender", () => {
this.hexValueChanged();
});
}
});

View File

@ -0,0 +1,53 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { reads } from "@ember/object/computed";
import Component from "@ember/component";
export default Component.extend({
editorId: reads("fieldName"),
@discourseComputed("fieldName")
currentEditorMode(fieldName) {
return fieldName === "css" ? "scss" : fieldName;
},
@discourseComputed("fieldName", "styles.html", "styles.css")
resetDisabled(fieldName) {
return (
this.get(`styles.${fieldName}`) ===
this.get(`styles.default_${fieldName}`)
);
},
@discourseComputed("styles", "fieldName")
editorContents: {
get(styles, fieldName) {
return styles[fieldName];
},
set(value, styles, fieldName) {
styles.setField(fieldName, value);
return value;
}
},
actions: {
reset() {
bootbox.confirm(
I18n.t("admin.customize.email_style.reset_confirm", {
fieldName: I18n.t(`admin.customize.email_style.${this.fieldName}`)
}),
I18n.t("no_value"),
I18n.t("yes_value"),
result => {
if (result) {
this.styles.setField(
this.fieldName,
this.styles.get(`default_${this.fieldName}`)
);
this.notifyPropertyChange("editorContents");
}
}
);
}
}
});

View File

@ -1,52 +0,0 @@
import discourseComputed from "discourse-common/utils/decorators";
import { reads } from "@ember/object/computed";
import Component from "@ember/component";
export default Component.extend({
editorId: reads("fieldName"),
@discourseComputed("fieldName")
currentEditorMode(fieldName) {
return fieldName === "css" ? "scss" : fieldName;
},
@discourseComputed("fieldName", "styles.html", "styles.css")
resetDisabled(fieldName) {
return (
this.get(`styles.${fieldName}`) ===
this.get(`styles.default_${fieldName}`)
);
},
@discourseComputed("styles", "fieldName")
editorContents: {
get(styles, fieldName) {
return styles[fieldName];
},
set(value, styles, fieldName) {
styles.setField(fieldName, value);
return value;
}
},
actions: {
reset() {
bootbox.confirm(
I18n.t("admin.customize.email_style.reset_confirm", {
fieldName: I18n.t(`admin.customize.email_style.${this.fieldName}`)
}),
I18n.t("no_value"),
I18n.t("yes_value"),
result => {
if (result) {
this.styles.setField(
this.fieldName,
this.styles.get(`default_${this.fieldName}`)
);
this.notifyPropertyChange("editorContents");
}
}
);
}
}
});

View File

@ -0,0 +1,81 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { isEmpty } from "@ember/utils";
import { or } from "@ember/object/computed";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { on, observes } from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
import Category from "discourse/models/category";
export default Component.extend(bufferedProperty("host"), {
editToggled: false,
tagName: "tr",
categoryId: null,
editing: or("host.isNew", "editToggled"),
@on("didInsertElement")
@observes("editing")
_focusOnInput() {
schedule("afterRender", () => {
this.element.querySelector(".host-name").focus();
});
},
@discourseComputed("buffered.host", "host.isSaving")
cantSave(host, isSaving) {
return isSaving || isEmpty(host);
},
actions: {
edit() {
this.set("categoryId", this.get("host.category.id"));
this.set("editToggled", true);
},
save() {
if (this.cantSave) {
return;
}
const props = this.buffered.getProperties(
"host",
"path_whitelist",
"class_name"
);
props.category_id = this.categoryId;
const host = this.host;
host
.save(props)
.then(() => {
host.set("category", Category.findById(this.categoryId));
this.set("editToggled", false);
})
.catch(popupAjaxError);
},
delete() {
bootbox.confirm(I18n.t("admin.embedding.confirm_delete"), result => {
if (result) {
this.host.destroyRecord().then(() => {
this.deleteHost(this.host);
});
}
});
},
cancel() {
const host = this.host;
if (host.get("isNew")) {
this.deleteHost(host);
} else {
this.rollbackBuffer();
this.set("editToggled", false);
}
}
}
});

View File

@ -1,80 +0,0 @@
import discourseComputed from "discourse-common/utils/decorators";
import { isEmpty } from "@ember/utils";
import { or } from "@ember/object/computed";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { on, observes } from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
import Category from "discourse/models/category";
export default Component.extend(bufferedProperty("host"), {
editToggled: false,
tagName: "tr",
categoryId: null,
editing: or("host.isNew", "editToggled"),
@on("didInsertElement")
@observes("editing")
_focusOnInput() {
schedule("afterRender", () => {
this.element.querySelector(".host-name").focus();
});
},
@discourseComputed("buffered.host", "host.isSaving")
cantSave(host, isSaving) {
return isSaving || isEmpty(host);
},
actions: {
edit() {
this.set("categoryId", this.get("host.category.id"));
this.set("editToggled", true);
},
save() {
if (this.cantSave) {
return;
}
const props = this.buffered.getProperties(
"host",
"path_whitelist",
"class_name"
);
props.category_id = this.categoryId;
const host = this.host;
host
.save(props)
.then(() => {
host.set("category", Category.findById(this.categoryId));
this.set("editToggled", false);
})
.catch(popupAjaxError);
},
delete() {
bootbox.confirm(I18n.t("admin.embedding.confirm_delete"), result => {
if (result) {
this.host.destroyRecord().then(() => {
this.deleteHost(this.host);
});
}
});
},
cancel() {
const host = this.host;
if (host.get("isNew")) {
this.deleteHost(host);
} else {
this.rollbackBuffer();
this.set("editToggled", false);
}
}
}
});

View File

@ -0,0 +1,42 @@
import I18n from "I18n";
import Component from "@ember/component";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["inline-edit"],
checked: null,
checkedInternal: null,
init() {
this._super(...arguments);
this.set("checkedInternal", this.checked);
},
@observes("checked")
checkedChanged() {
this.set("checkedInternal", this.checked);
},
@discourseComputed("labelKey")
label(key) {
return I18n.t(key);
},
@discourseComputed("checked", "checkedInternal")
changed(checked, checkedInternal) {
return !!checked !== !!checkedInternal;
},
actions: {
cancelled() {
this.set("checkedInternal", this.checked);
},
finished() {
this.set("checked", this.checkedInternal);
this.action();
}
}
});

View File

@ -1,41 +0,0 @@
import Component from "@ember/component";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["inline-edit"],
checked: null,
checkedInternal: null,
init() {
this._super(...arguments);
this.set("checkedInternal", this.checked);
},
@observes("checked")
checkedChanged() {
this.set("checkedInternal", this.checked);
},
@discourseComputed("labelKey")
label(key) {
return I18n.t(key);
},
@discourseComputed("checked", "checkedInternal")
changed(checked, checkedInternal) {
return !!checked !== !!checkedInternal;
},
actions: {
cancelled() {
this.set("checkedInternal", this.checked);
},
finished() {
this.set("checked", this.checkedInternal);
this.action();
}
}
});

View File

@ -0,0 +1,114 @@
import I18n from "I18n";
import EmberObject from "@ember/object";
import { later } from "@ember/runloop";
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import { ajax } from "discourse/lib/ajax";
import AdminUser from "admin/models/admin-user";
import copyText from "discourse/lib/copy-text";
export default Component.extend({
classNames: ["ip-lookup"],
@discourseComputed("other_accounts.length", "totalOthersWithSameIP")
otherAccountsToDelete(otherAccountsLength, totalOthersWithSameIP) {
// can only delete up to 50 accounts at a time
const total = Math.min(50, totalOthersWithSameIP || 0);
const visible = Math.min(50, otherAccountsLength || 0);
return Math.max(visible, total);
},
actions: {
lookup() {
this.set("show", true);
if (!this.location) {
ajax("/admin/users/ip-info", { data: { ip: this.ip } }).then(location =>
this.set("location", EmberObject.create(location))
);
}
if (!this.other_accounts) {
this.set("otherAccountsLoading", true);
const data = {
ip: this.ip,
exclude: this.userId,
order: "trust_level DESC"
};
ajax("/admin/users/total-others-with-same-ip", { data }).then(result =>
this.set("totalOthersWithSameIP", result.total)
);
AdminUser.findAll("active", data).then(users => {
this.setProperties({
other_accounts: users,
otherAccountsLoading: false
});
});
}
},
hide() {
this.set("show", false);
},
copy() {
let text = `IP: ${this.ip}\n`;
const location = this.location;
if (location) {
if (location.hostname) {
text += `${I18n.t("ip_lookup.hostname")}: ${location.hostname}\n`;
}
text += I18n.t("ip_lookup.location");
if (location.location) {
text += `: ${location.location}\n`;
} else {
text += `: ${I18n.t("ip_lookup.location_not_found")}\n`;
}
if (location.organization) {
text += I18n.t("ip_lookup.organisation");
text += `: ${location.organization}\n`;
}
}
const $copyRange = $('<p id="copy-range"></p>');
$copyRange.html(text.trim().replace(/\n/g, "<br>"));
$(document.body).append($copyRange);
if (copyText(text, $copyRange[0])) {
this.set("copied", true);
later(() => this.set("copied", false), 2000);
}
$copyRange.remove();
},
deleteOtherAccounts() {
bootbox.confirm(
I18n.t("ip_lookup.confirm_delete_other_accounts"),
I18n.t("no_value"),
I18n.t("yes_value"),
confirmed => {
if (confirmed) {
this.setProperties({
other_accounts: null,
otherAccountsLoading: true,
totalOthersWithSameIP: null
});
ajax("/admin/users/delete-others-with-same-ip.json", {
type: "DELETE",
data: {
ip: this.ip,
exclude: this.userId,
order: "trust_level DESC"
}
}).then(() => this.send("lookup"));
}
}
);
}
}
});

View File

@ -1,113 +0,0 @@
import EmberObject from "@ember/object";
import { later } from "@ember/runloop";
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import { ajax } from "discourse/lib/ajax";
import AdminUser from "admin/models/admin-user";
import copyText from "discourse/lib/copy-text";
export default Component.extend({
classNames: ["ip-lookup"],
@discourseComputed("other_accounts.length", "totalOthersWithSameIP")
otherAccountsToDelete(otherAccountsLength, totalOthersWithSameIP) {
// can only delete up to 50 accounts at a time
const total = Math.min(50, totalOthersWithSameIP || 0);
const visible = Math.min(50, otherAccountsLength || 0);
return Math.max(visible, total);
},
actions: {
lookup() {
this.set("show", true);
if (!this.location) {
ajax("/admin/users/ip-info", { data: { ip: this.ip } }).then(location =>
this.set("location", EmberObject.create(location))
);
}
if (!this.other_accounts) {
this.set("otherAccountsLoading", true);
const data = {
ip: this.ip,
exclude: this.userId,
order: "trust_level DESC"
};
ajax("/admin/users/total-others-with-same-ip", { data }).then(result =>
this.set("totalOthersWithSameIP", result.total)
);
AdminUser.findAll("active", data).then(users => {
this.setProperties({
other_accounts: users,
otherAccountsLoading: false
});
});
}
},
hide() {
this.set("show", false);
},
copy() {
let text = `IP: ${this.ip}\n`;
const location = this.location;
if (location) {
if (location.hostname) {
text += `${I18n.t("ip_lookup.hostname")}: ${location.hostname}\n`;
}
text += I18n.t("ip_lookup.location");
if (location.location) {
text += `: ${location.location}\n`;
} else {
text += `: ${I18n.t("ip_lookup.location_not_found")}\n`;
}
if (location.organization) {
text += I18n.t("ip_lookup.organisation");
text += `: ${location.organization}\n`;
}
}
const $copyRange = $('<p id="copy-range"></p>');
$copyRange.html(text.trim().replace(/\n/g, "<br>"));
$(document.body).append($copyRange);
if (copyText(text, $copyRange[0])) {
this.set("copied", true);
later(() => this.set("copied", false), 2000);
}
$copyRange.remove();
},
deleteOtherAccounts() {
bootbox.confirm(
I18n.t("ip_lookup.confirm_delete_other_accounts"),
I18n.t("no_value"),
I18n.t("yes_value"),
confirmed => {
if (confirmed) {
this.setProperties({
other_accounts: null,
otherAccountsLoading: true,
totalOthersWithSameIP: null
});
ajax("/admin/users/delete-others-with-same-ip.json", {
type: "DELETE",
data: {
ip: this.ip,
exclude: this.userId,
order: "trust_level DESC"
}
}).then(() => this.send("lookup"));
}
}
);
}
}
});

View File

@ -0,0 +1,41 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { equal } from "@ember/object/computed";
import Component from "@ember/component";
import { afterRender } from "discourse-common/utils/decorators";
const ACTIONS = ["delete", "delete_replies", "edit", "none"];
export default Component.extend({
postId: null,
postAction: null,
postEdit: null,
@discourseComputed
penaltyActions() {
return ACTIONS.map(id => {
return { id, name: I18n.t(`admin.user.penalty_post_${id}`) };
});
},
editing: equal("postAction", "edit"),
actions: {
penaltyChanged(postAction) {
this.set("postAction", postAction);
// If we switch to edit mode, jump to the edit textarea
if (postAction === "edit") {
this._focusEditTextarea();
}
}
},
@afterRender
_focusEditTextarea() {
const elem = this.element;
const body = elem.closest(".modal-body");
body.scrollTo(0, body.clientHeight);
elem.querySelector(".post-editor").focus();
}
});

View File

@ -1,40 +0,0 @@
import discourseComputed from "discourse-common/utils/decorators";
import { equal } from "@ember/object/computed";
import Component from "@ember/component";
import { afterRender } from "discourse-common/utils/decorators";
const ACTIONS = ["delete", "delete_replies", "edit", "none"];
export default Component.extend({
postId: null,
postAction: null,
postEdit: null,
@discourseComputed
penaltyActions() {
return ACTIONS.map(id => {
return { id, name: I18n.t(`admin.user.penalty_post_${id}`) };
});
},
editing: equal("postAction", "edit"),
actions: {
penaltyChanged(postAction) {
this.set("postAction", postAction);
// If we switch to edit mode, jump to the edit textarea
if (postAction === "edit") {
this._focusEditTextarea();
}
}
},
@afterRender
_focusEditTextarea() {
const elem = this.element;
const body = elem.closest(".modal-body");
body.scrollTo(0, body.clientHeight);
elem.querySelector(".post-editor").focus();
}
});

View File

@ -0,0 +1,88 @@
import I18n from "I18n";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import { fmt } from "discourse/lib/computed";
import Permalink from "admin/models/permalink";
export default Component.extend({
classNames: ["permalink-form"],
formSubmitted: false,
permalinkType: "topic_id",
permalinkTypePlaceholder: fmt("permalinkType", "admin.permalink.%@"),
@discourseComputed
permalinkTypes() {
return [
{ id: "topic_id", name: I18n.t("admin.permalink.topic_id") },
{ id: "post_id", name: I18n.t("admin.permalink.post_id") },
{ id: "category_id", name: I18n.t("admin.permalink.category_id") },
{ id: "tag_name", name: I18n.t("admin.permalink.tag_name") },
{ id: "external_url", name: I18n.t("admin.permalink.external_url") }
];
},
didInsertElement() {
this._super(...arguments);
schedule("afterRender", () => {
$(this.element.querySelector(".external-url")).keydown(e => {
// enter key
if (e.keyCode === 13) {
this.send("submit");
}
});
});
},
focusPermalink() {
schedule("afterRender", () =>
this.element.querySelector(".permalink-url").focus()
);
},
actions: {
submit() {
if (!this.formSubmitted) {
this.set("formSubmitted", true);
Permalink.create({
url: this.url,
permalink_type: this.permalinkType,
permalink_type_value: this.permalink_type_value
})
.save()
.then(
result => {
this.setProperties({
url: "",
permalink_type_value: "",
formSubmitted: false
});
this.action(Permalink.create(result.permalink));
this.focusPermalink();
},
e => {
this.set("formSubmitted", false);
let error;
if (e.responseJSON && e.responseJSON.errors) {
error = I18n.t("generic_error_with_reason", {
error: e.responseJSON.errors.join(". ")
});
} else {
error = I18n.t("generic_error");
}
bootbox.alert(error, () => this.focusPermalink());
}
);
}
},
onChangePermalinkType(type) {
this.set("permalinkType", type);
}
}
});

View File

@ -1,86 +0,0 @@
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import { fmt } from "discourse/lib/computed";
import Permalink from "admin/models/permalink";
export default Component.extend({
classNames: ["permalink-form"],
formSubmitted: false,
permalinkType: "topic_id",
permalinkTypePlaceholder: fmt("permalinkType", "admin.permalink.%@"),
@discourseComputed
permalinkTypes() {
return [
{ id: "topic_id", name: I18n.t("admin.permalink.topic_id") },
{ id: "post_id", name: I18n.t("admin.permalink.post_id") },
{ id: "category_id", name: I18n.t("admin.permalink.category_id") },
{ id: "external_url", name: I18n.t("admin.permalink.external_url") }
];
},
didInsertElement() {
this._super(...arguments);
schedule("afterRender", () => {
$(this.element.querySelector(".external-url")).keydown(e => {
// enter key
if (e.keyCode === 13) {
this.send("submit");
}
});
});
},
focusPermalink() {
schedule("afterRender", () =>
this.element.querySelector(".permalink-url").focus()
);
},
actions: {
submit() {
if (!this.formSubmitted) {
this.set("formSubmitted", true);
Permalink.create({
url: this.url,
permalink_type: this.permalinkType,
permalink_type_value: this.permalink_type_value
})
.save()
.then(
result => {
this.setProperties({
url: "",
permalink_type_value: "",
formSubmitted: false
});
this.action(Permalink.create(result.permalink));
this.focusPermalink();
},
e => {
this.set("formSubmitted", false);
let error;
if (e.responseJSON && e.responseJSON.errors) {
error = I18n.t("generic_error_with_reason", {
error: e.responseJSON.errors.join(". ")
});
} else {
error = I18n.t("generic_error");
}
bootbox.alert(error, () => this.focusPermalink());
}
);
}
},
onChangePermalinkType(type) {
this.set("permalinkType", type);
}
}
});

View File

@ -0,0 +1,16 @@
import { action } from "@ember/object";
import FilterComponent from "admin/components/report-filters/filter";
export default FilterComponent.extend({
checked: false,
didReceiveAttrs() {
this._super(...arguments);
this.set("checked", !!this.filter.default);
},
@action
onChange() {
this.applyFilter(this.filter.id, !this.checked || undefined);
}
});

View File

@ -0,0 +1,12 @@
import { action } from "@ember/object";
import { readOnly } from "@ember/object/computed";
import FilterComponent from "admin/components/report-filters/filter";
export default FilterComponent.extend({
category: readOnly("filter.default"),
@action
onChange(categoryId) {
this.applyFilter(this.filter.id, categoryId || undefined);
}
});

View File

@ -1,16 +0,0 @@
import { readOnly } from "@ember/object/computed";
import FilterComponent from "admin/components/report-filters/filter";
export default FilterComponent.extend({
classNames: ["category-filter"],
layoutName: "admin/templates/components/report-filters/category",
category: readOnly("filter.default"),
actions: {
onChange(categoryId) {
this.applyFilter(this.get("filter.id"), categoryId || undefined);
}
}
});

View File

@ -1,7 +0,0 @@
import FilterComponent from "admin/components/report-filters/filter";
export default FilterComponent.extend({
classNames: ["file-extension-filter"],
layoutName: "admin/templates/components/report-filters/file-extension"
});

View File

@ -0,0 +1,9 @@
import Component from "@ember/component";
import { action } from "@ember/object";
export default Component.extend({
@action
onChange(value) {
this.applyFilter(this.filter.id, value);
}
});

View File

@ -1,8 +0,0 @@
import Component from "@ember/component";
export default Component.extend({
actions: {
onChange(value) {
this.applyFilter(this.get("filter.id"), value);
}
}
});

View File

@ -0,0 +1,18 @@
import { computed } from "@ember/object";
import FilterComponent from "admin/components/report-filters/filter";
export default FilterComponent.extend({
classNames: ["group-filter"],
@computed
get groupOptions() {
return (this.site.groups || []).map(group => {
return { name: group["name"], value: group["id"] };
});
},
@computed("filter.default")
get groupId() {
return this.filter.default ? parseInt(this.filter.default, 10) : null;
}
});

View File

@ -1,20 +0,0 @@
import FilterComponent from "admin/components/report-filters/filter";
import discourseComputed from "discourse-common/utils/decorators";
export default FilterComponent.extend({
classNames: ["group-filter"],
layoutName: "admin/templates/components/report-filters/group",
@discourseComputed()
groupOptions() {
return (this.site.groups || []).map(group => {
return { name: group["name"], value: group["id"] };
});
},
@discourseComputed("filter.default")
groupId(filterDefault) {
return filterDefault ? parseInt(filterDefault, 10) : null;
}
});

View File

@ -0,0 +1,3 @@
import FilterComponent from "admin/components/report-filters/filter";
export default FilterComponent.extend();

View File

@ -0,0 +1,140 @@
import getURL from "discourse-common/lib/get-url";
import I18n from "I18n";
import { later, schedule } from "@ember/runloop";
import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
import discourseComputed, { on } from "discourse-common/utils/decorators";
/*global Resumable:true */
/**
Example usage:
{{resumable-upload
target="/admin/backups/upload"
success=(action "successAction")
error=(action "errorAction")
uploadText="UPLOAD"
}}
**/
export default Component.extend({
tagName: "button",
classNames: ["btn", "ru"],
classNameBindings: ["isUploading"],
attributeBindings: ["translatedTitle:title"],
resumable: null,
isUploading: false,
progress: 0,
rerenderTriggers: ["isUploading", "progress"],
uploadingIcon: null,
progressBar: null,
@on("init")
_initialize() {
this.resumable = new Resumable({
target: getURL(this.target),
maxFiles: 1, // only 1 file at a time
headers: {
"X-CSRF-Token": document.querySelector("meta[name='csrf-token']")
.content
}
});
this.resumable.on("fileAdded", () => {
// automatically upload the selected file
this.resumable.upload();
// mark as uploading
later(() => {
this.set("isUploading", true);
this._updateIcon();
});
});
this.resumable.on("fileProgress", file => {
// update progress
later(() => {
this.set("progress", parseInt(file.progress() * 100, 10));
this._updateProgressBar();
});
});
this.resumable.on("fileSuccess", file => {
later(() => {
// mark as not uploading anymore
this._reset();
// fire an event to allow the parent route to reload its model
this.success(file.fileName);
});
});
this.resumable.on("fileError", (file, message) => {
later(() => {
// mark as not uploading anymore
this._reset();
// fire an event to allow the parent route to display the error message
this.error(file.fileName, message);
});
});
},
@on("didInsertElement")
_assignBrowse() {
schedule("afterRender", () => this.resumable.assignBrowse($(this.element)));
},
@on("willDestroyElement")
_teardown() {
if (this.resumable) {
this.resumable.cancel();
this.resumable = null;
}
},
@discourseComputed("title", "text")
translatedTitle(title, text) {
return title ? I18n.t(title) : text;
},
@discourseComputed("isUploading", "progress")
text(isUploading, progress) {
if (isUploading) {
return progress + " %";
} else {
return this.uploadText;
}
},
didReceiveAttrs() {
this._super(...arguments);
this._updateIcon();
},
click() {
if (this.isUploading) {
this.resumable.cancel();
later(() => this._reset());
return false;
} else {
return true;
}
},
_updateIcon() {
const icon = this.isUploading ? "times" : "upload";
this.set("uploadingIcon", `${iconHTML(icon)}`.htmlSafe());
},
_updateProgressBar() {
const pb = `${"width:" + this.progress + "%"}`.htmlSafe();
this.set("progressBar", pb);
},
_reset() {
this.setProperties({ isUploading: false, progress: 0 });
this._updateIcon();
this._updateProgressBar();
}
});

View File

@ -1,139 +0,0 @@
import { schedule } from "@ember/runloop";
import { later } from "@ember/runloop";
import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
import discourseComputed, { on } from "discourse-common/utils/decorators";
/*global Resumable:true */
/**
Example usage:
{{resumable-upload
target="/admin/backups/upload"
success=(action "successAction")
error=(action "errorAction")
uploadText="UPLOAD"
}}
**/
export default Component.extend({
tagName: "button",
classNames: ["btn", "ru"],
classNameBindings: ["isUploading"],
attributeBindings: ["translatedTitle:title"],
resumable: null,
isUploading: false,
progress: 0,
rerenderTriggers: ["isUploading", "progress"],
uploadingIcon: null,
progressBar: null,
@on("init")
_initialize() {
this.resumable = new Resumable({
target: Discourse.getURL(this.target),
maxFiles: 1, // only 1 file at a time
headers: {
"X-CSRF-Token": document.querySelector("meta[name='csrf-token']")
.content
}
});
this.resumable.on("fileAdded", () => {
// automatically upload the selected file
this.resumable.upload();
// mark as uploading
later(() => {
this.set("isUploading", true);
this._updateIcon();
});
});
this.resumable.on("fileProgress", file => {
// update progress
later(() => {
this.set("progress", parseInt(file.progress() * 100, 10));
this._updateProgressBar();
});
});
this.resumable.on("fileSuccess", file => {
later(() => {
// mark as not uploading anymore
this._reset();
// fire an event to allow the parent route to reload its model
this.success(file.fileName);
});
});
this.resumable.on("fileError", (file, message) => {
later(() => {
// mark as not uploading anymore
this._reset();
// fire an event to allow the parent route to display the error message
this.error(file.fileName, message);
});
});
},
@on("didInsertElement")
_assignBrowse() {
schedule("afterRender", () => this.resumable.assignBrowse($(this.element)));
},
@on("willDestroyElement")
_teardown() {
if (this.resumable) {
this.resumable.cancel();
this.resumable = null;
}
},
@discourseComputed("title", "text")
translatedTitle(title, text) {
return title ? I18n.t(title) : text;
},
@discourseComputed("isUploading", "progress")
text(isUploading, progress) {
if (isUploading) {
return progress + " %";
} else {
return this.uploadText;
}
},
didReceiveAttrs() {
this._super(...arguments);
this._updateIcon();
},
click() {
if (this.isUploading) {
this.resumable.cancel();
later(() => this._reset());
return false;
} else {
return true;
}
},
_updateIcon() {
const icon = this.isUploading ? "times" : "upload";
this.set("uploadingIcon", `${iconHTML(icon)}`.htmlSafe());
},
_updateProgressBar() {
const pb = `${"width:" + this.progress + "%"}`.htmlSafe();
this.set("progressBar", pb);
},
_reset() {
this.setProperties({ isUploading: false, progress: 0 });
this._updateIcon();
this._updateProgressBar();
}
});

View File

@ -1,14 +0,0 @@
import discourseComputed from "discourse-common/utils/decorators";
import { or } from "@ember/object/computed";
import Component from "@ember/component";
export default Component.extend({
classNames: ["controls"],
buttonDisabled: or("model.isSaving", "saveDisabled"),
@discourseComputed("model.isSaving")
savingText(saving) {
return saving ? "saving" : "save";
}
});

View File

@ -0,0 +1,97 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
/**
A form to create an IP address that will be blocked or whitelisted.
Example usage:
{{screened-ip-address-form action=(action "recordAdded")}}
where action is a callback on the controller or route that will get called after
the new record is successfully saved. It is called with the new ScreenedIpAddress record
as an argument.
**/
import ScreenedIpAddress from "admin/models/screened-ip-address";
import { on } from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["screened-ip-address-form"],
formSubmitted: false,
actionName: "block",
@discourseComputed
adminWhitelistEnabled() {
return Discourse.SiteSettings.use_admin_ip_whitelist;
},
@discourseComputed("adminWhitelistEnabled")
actionNames(adminWhitelistEnabled) {
if (adminWhitelistEnabled) {
return [
{ id: "block", name: I18n.t("admin.logs.screened_ips.actions.block") },
{
id: "do_nothing",
name: I18n.t("admin.logs.screened_ips.actions.do_nothing")
},
{
id: "allow_admin",
name: I18n.t("admin.logs.screened_ips.actions.allow_admin")
}
];
} else {
return [
{ id: "block", name: I18n.t("admin.logs.screened_ips.actions.block") },
{
id: "do_nothing",
name: I18n.t("admin.logs.screened_ips.actions.do_nothing")
}
];
}
},
actions: {
submit() {
if (!this.formSubmitted) {
this.set("formSubmitted", true);
const screenedIpAddress = ScreenedIpAddress.create({
ip_address: this.ip_address,
action_name: this.actionName
});
screenedIpAddress
.save()
.then(result => {
this.setProperties({ ip_address: "", formSubmitted: false });
this.action(ScreenedIpAddress.create(result.screened_ip_address));
schedule("afterRender", () =>
this.element.querySelector(".ip-address-input").focus()
);
})
.catch(e => {
this.set("formSubmitted", false);
const msg =
e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors
? I18n.t("generic_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". ")
})
: I18n.t("generic_error");
bootbox.alert(msg, () =>
this.element.querySelector(".ip-address-input").focus()
);
});
}
}
},
@on("didInsertElement")
_init() {
schedule("afterRender", () => {
$(this.element.querySelector(".ip-address-input")).keydown(e => {
if (e.keyCode === 13) {
this.send("submit");
}
});
});
}
});

View File

@ -1,96 +0,0 @@
import discourseComputed from "discourse-common/utils/decorators";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
/**
A form to create an IP address that will be blocked or whitelisted.
Example usage:
{{screened-ip-address-form action=(action "recordAdded")}}
where action is a callback on the controller or route that will get called after
the new record is successfully saved. It is called with the new ScreenedIpAddress record
as an argument.
**/
import ScreenedIpAddress from "admin/models/screened-ip-address";
import { on } from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["screened-ip-address-form"],
formSubmitted: false,
actionName: "block",
@discourseComputed
adminWhitelistEnabled() {
return Discourse.SiteSettings.use_admin_ip_whitelist;
},
@discourseComputed("adminWhitelistEnabled")
actionNames(adminWhitelistEnabled) {
if (adminWhitelistEnabled) {
return [
{ id: "block", name: I18n.t("admin.logs.screened_ips.actions.block") },
{
id: "do_nothing",
name: I18n.t("admin.logs.screened_ips.actions.do_nothing")
},
{
id: "allow_admin",
name: I18n.t("admin.logs.screened_ips.actions.allow_admin")
}
];
} else {
return [
{ id: "block", name: I18n.t("admin.logs.screened_ips.actions.block") },
{
id: "do_nothing",
name: I18n.t("admin.logs.screened_ips.actions.do_nothing")
}
];
}
},
actions: {
submit() {
if (!this.formSubmitted) {
this.set("formSubmitted", true);
const screenedIpAddress = ScreenedIpAddress.create({
ip_address: this.ip_address,
action_name: this.actionName
});
screenedIpAddress
.save()
.then(result => {
this.setProperties({ ip_address: "", formSubmitted: false });
this.action(ScreenedIpAddress.create(result.screened_ip_address));
schedule("afterRender", () =>
this.element.querySelector(".ip-address-input").focus()
);
})
.catch(e => {
this.set("formSubmitted", false);
const msg =
e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors
? I18n.t("generic_error_with_reason", {
error: e.jqXHR.responseJSON.errors.join(". ")
})
: I18n.t("generic_error");
bootbox.alert(msg, () =>
this.element.querySelector(".ip-address-input").focus()
);
});
}
}
},
@on("didInsertElement")
_init() {
schedule("afterRender", () => {
$(this.element.querySelector(".ip-address-input")).keydown(e => {
if (e.keyCode === 13) {
this.send("submit");
}
});
});
}
});

View File

@ -0,0 +1,105 @@
import I18n from "I18n";
import { isEmpty } from "@ember/utils";
import Component from "@ember/component";
import { on } from "discourse-common/utils/decorators";
import { set } from "@ember/object";
export default Component.extend({
classNameBindings: [":value-list", ":secret-value-list"],
inputDelimiter: null,
collection: null,
values: null,
validationMessage: null,
@on("didReceiveAttrs")
_setupCollection() {
const values = this.values;
this.set(
"collection",
this._splitValues(values, this.inputDelimiter || "\n")
);
},
actions: {
changeKey(index, newValue) {
if (this._checkInvalidInput(newValue)) return;
this._replaceValue(index, newValue, "key");
},
changeSecret(index, newValue) {
if (this._checkInvalidInput(newValue)) return;
this._replaceValue(index, newValue, "secret");
},
addValue() {
if (this._checkInvalidInput([this.newKey, this.newSecret])) return;
this._addValue(this.newKey, this.newSecret);
this.setProperties({ newKey: "", newSecret: "" });
},
removeValue(value) {
this._removeValue(value);
}
},
_checkInvalidInput(inputs) {
this.set("validationMessage", null);
for (let input of inputs) {
if (isEmpty(input) || input.includes("|")) {
this.set(
"validationMessage",
I18n.t("admin.site_settings.secret_list.invalid_input")
);
return true;
}
}
},
_addValue(value, secret) {
this.collection.addObject({ key: value, secret: secret });
this._saveValues();
},
_removeValue(value) {
const collection = this.collection;
collection.removeObject(value);
this._saveValues();
},
_replaceValue(index, newValue, keyName) {
let item = this.collection[index];
set(item, keyName, newValue);
this._saveValues();
},
_saveValues() {
this.set(
"values",
this.collection
.map(function(elem) {
return `${elem.key}|${elem.secret}`;
})
.join("\n")
);
},
_splitValues(values, delimiter) {
if (values && values.length) {
const keys = ["key", "secret"];
var res = [];
values.split(delimiter).forEach(function(str) {
var object = {};
str.split("|").forEach(function(a, i) {
object[keys[i]] = a;
});
res.push(object);
});
return res;
} else {
return [];
}
}
});

Some files were not shown because too many files have changed in this diff Show More