Merge master

This commit is contained in:
Neil Lalonde 2020-11-30 16:41:58 -05:00
commit fef0a0c429
No known key found for this signature in database
GPG Key ID: FF871CA9037D0A91
5170 changed files with 185010 additions and 190655 deletions

View File

@ -8,10 +8,9 @@ lib/javascripts/locale/
lib/javascripts/messageformat.js
lib/highlight_js/
plugins/**/lib/javascripts/locale
public/javascripts/
public/
vendor/
test/javascripts/test_helper.js
test/javascripts/fixtures
test/javascripts/helpers/assertions.js
app/assets/javascripts/discourse/tests/test_helper.js
app/assets/javascripts/discourse/tests/fixtures
node_modules/
dist/

View File

@ -1,7 +1,18 @@
{
"extends": "eslint-config-discourse",
"plugins": ["discourse-ember"],
"rules": {
"discourse-ember/global-ember": 2
"discourse-ember/global-ember": 2,
"no-duplicate-imports": 2
},
"globals": {
"moduleFor": "off",
"moduleForComponent": "off",
"testStart": "off",
"testDone": "off",
"sinon": "off",
"currentURL": "off",
"invisible": "off",
"visible": "off",
"count": "off",
}
}

View File

@ -28,3 +28,27 @@ c4644c61d97c823b7dd940ffaf0967a104f4b58c
# DEV: Fix indentation for routes.rb
985900818ff985b04def6aa4c5d99c1aa6dbd45c
# Add rubocop to our build.
5012d46cbd3bcf79b7351f7d2d41003496a796c5
# Make rubocop happy again.
ad5082d969ab1f60b5c5b1e89a616117906289f8
# DEV: apply new coding standards (#10592)
52672b9eabccb1184d85dc7f08062d5a7c18cb73
# DEV: apply coding standards to plugins (#10594)
bf88410126f73aab47b7e694e3c5b46453cec1b6
# REFACTOR: Support bundling our `admin` section as an ember addon
ce3fe2f4c4ddf166949ee3cec3d9ecbf9108ab52
# REFACTOR: Move qunit tests to a different directory structure
bc97c79a35d8acd283d4d8b79aa079bce9d127c6
# REFACTOR: Move javascript tests inside discourse app
23f24bfb510edb25b18b6a0d5485270c88df9b24
# DEV: Tidy up imports. (#11364)
1c2358ba162eb9f9ba9095c9afe30cf51dd85e04

38
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,38 @@
version: 2
updates:
- package-ecosystem: bundler
directory: "/"
schedule:
interval: daily
time: "08:00"
timezone: Australia/Sydney
open-pull-requests-limit: 10
versioning-strategy: lockfile-only
allow:
- dependency-type: direct
- dependency-type: indirect
ignore:
- dependency-name: aws-partitions
versions:
- "> 1.329.0"
- "< 2"
- dependency-name: aws-sdk-core
versions:
- "> 3.99.1"
- "< 4"
- dependency-name: aws-sdk-kms
versions:
- "> 1.31.0"
- "< 2"
- dependency-name: aws-sdk-s3
versions:
- "> 1.66.0"
- "< 2"
- dependency-name: aws-sdk-sns
versions:
- "> 1.25.1"
- "< 2"
- dependency-name: aws-sigv4
versions:
- "> 1.2.0"
- "< 2"

1
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1 @@
<!-- NOTE: All pull requests should have tests (rspec in Ruby, qunit in Javascript). If your code does not include test coverage, please include an explanation of why it was omitted. -->

View File

@ -65,7 +65,7 @@ jobs:
if: env.BUILD_TYPE != 'LINT'
run: |
sudo apt-get update
sudo apt-get -yqq install postgresql-client libpq-dev gifsicle jpegoptim optipng jhead
sudo apt-get -yqq install postgresql-client libpq-dev jpegoptim optipng jhead
wget -qO- https://raw.githubusercontent.com/discourse/discourse_docker/master/image/base/install-pngquant | sudo sh
- name: Update imagemagick
@ -91,9 +91,10 @@ jobs:
gem install bundler -v 2.1.4 --no-doc
bundle config deployment 'true'
bundle config without 'development'
bundle config path vendor/bundle
- name: Bundler cache
uses: actions/cache@v1
uses: actions/cache@v2
id: bundler-cache
with:
path: vendor/bundle
@ -109,7 +110,7 @@ jobs:
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn cache
uses: actions/cache@v1
uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir.outputs.dir }}
@ -136,21 +137,56 @@ jobs:
bin/rake parallel:create
bin/rake parallel:migrate
- name: Rubocop
if: env.BUILD_TYPE == 'LINT'
- name: Rubocop (core and core plugins)
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'
run: bundle exec rubocop .
- name: ESLint
if: env.BUILD_TYPE == 'LINT'
run: |
yarn eslint app/assets/javascripts test/javascripts
yarn eslint --global I18n --ext .es6 plugins
- name: Rubocop (all plugins)
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'PLUGINS'
run: bundle exec rubocop plugins
- name: Prettier
if: env.BUILD_TYPE == 'LINT'
- name: ESLint (core)
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'
run: yarn eslint --ext .js,.js.es6 --no-error-on-unmatched-pattern app/assets/javascripts
- name: ESLint (core plugins)
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'
run: yarn eslint --ext .js,.js.es6 --no-error-on-unmatched-pattern plugins/**/{test,assets}/javascripts
- name: ESLint (all plugins)
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'PLUGINS'
run: yarn eslint --ext .js,.js.es6 --no-error-on-unmatched-pattern plugins/**/{test,assets}/javascripts
- name: Prettier (core and core plugins)
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'
run: |
yarn prettier -v
yarn prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.js" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6" "plugins/**/*.scss" "plugins/**/*.es6"
yarn prettier --list-different \
"app/assets/stylesheets/**/*.scss" \
"app/assets/javascripts/**/*.{js,es6}" \
"plugins/**/assets/stylesheets/**/*.scss" \
"plugins/**/assets/javascripts/**/*.{js,es6}"
- name: Prettier (all plugins)
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'PLUGINS'
run: |
yarn prettier -v
yarn prettier --list-different \
"plugins/**/assets/stylesheets/**/*.scss" \
"plugins/**/assets/javascripts/**/*.{js,es6}"
- name: Ember template lint (core and core plugins)
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'
run: |
yarn ember-template-lint \
app/assets/javascripts \
plugins/**/assets/javascripts
- name: Ember template lint (all plugins)
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'PLUGINS'
run: |
yarn ember-template-lint \
plugins/**/assets/javascripts
- name: Core English locale
if: env.BUILD_TYPE == 'LINT' && env.TARGET == 'CORE'

11
.gitignore vendored
View File

@ -53,8 +53,8 @@ bootsnap-compile-cache/
!/plugins/discourse-nginx-performance-report
!/plugins/discourse-narrative-bot
!/plugins/discourse-presence
!/plugins/styleguide
!/plugins/discourse-local-dates
!/plugins/discourse-unsupported-browser
/plugins/*/auto_generated/
/spec/fixtures/plugins/my_plugin/auto_generated
@ -88,6 +88,7 @@ config/multisite.yml
config/multisite1.yml
config/fog_credentials.yml
/public/fonts
/public/uploads
/public/backups
/public/stylesheet-cache/*
@ -136,8 +137,16 @@ node_modules
# ignore generated api documentation files
openapi/*
# ignore VSCode config files
.vscode
# ignore direnv
.envrc
# ember-cli generated
dist
# Copyright Deposits
copyright
yarn-error.log

12
.licensed.yml Normal file
View File

@ -0,0 +1,12 @@
sources:
yarn: true
bundler: true
allowed:
- mit
- apache-2.0
- bsd-2-clause
- bsd-3-clause
- cc0-1.0
- isc
- other
- none

View File

@ -1,6 +1,25 @@
app/assets/stylesheets/vendor/
plugins/**/assets/stylesheets/vendor/
plugins/**/assets/javascripts/vendor/
package.json
config/locales/**/*.yml
!config/locales/**/*.en*.yml
script/import_scripts/**/*.yml
app/assets/javascripts/env.js
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
lib/javascripts/locale/
lib/javascripts/messageformat.js
lib/highlight_js/
plugins/**/lib/javascripts/locale
public/
vendor/
app/assets/javascripts/discourse/tests/test_helper.js
app/assets/javascripts/discourse/tests/fixtures
node_modules/
dist/
**/*.rb

1
.prettierrc Normal file
View File

@ -0,0 +1 @@
{}

View File

@ -50,6 +50,6 @@ module.exports = {
"simple-unless": true,
"style-concatenation": true,
"table-groups": true,
"link-href-attributes": false
}
"link-href-attributes": false,
},
};

View File

@ -1,99 +0,0 @@
[main]
host = https://www.transifex.com
lang_map = el_GR: el, es_ES: es, fr_FR: fr, hu_HU: hu, ko_KR: ko, pt_PT: pt, sk_SK: sk, vi_VN: vi
[discourse-org.core-client-yml]
file_filter = config/locales/client.<lang>.yml
source_file = config/locales/client.en.yml
source_lang = en
type = YML
[discourse-org.core-server-yml]
file_filter = config/locales/server.<lang>.yml
source_file = config/locales/server.en.yml
source_lang = en
type = YML
[discourse-org.pollclientenyml]
file_filter = plugins/poll/config/locales/client.<lang>.yml
source_file = plugins/poll/config/locales/client.en.yml
source_lang = en
type = YML
[discourse-org.pollserverenyml]
file_filter = plugins/poll/config/locales/server.<lang>.yml
source_file = plugins/poll/config/locales/server.en.yml
source_lang = en
type = YML
[discourse-org.narrativeclientenyml]
file_filter = plugins/discourse-narrative-bot/config/locales/client.<lang>.yml
source_file = plugins/discourse-narrative-bot/config/locales/client.en.yml
source_lang = en
type = YML
[discourse-org.narrativeserverenyml]
file_filter = plugins/discourse-narrative-bot/config/locales/server.<lang>.yml
source_file = plugins/discourse-narrative-bot/config/locales/server.en.yml
source_lang = en
type = YML
[discourse-org.discourse-presenceclientenyml]
file_filter = plugins/discourse-presence/config/locales/client.<lang>.yml
source_file = plugins/discourse-presence/config/locales/client.en.yml
source_lang = en
type = YML
[discourse-org.discourse-presenceserverenyml]
file_filter = plugins/discourse-presence/config/locales/server.<lang>.yml
source_file = plugins/discourse-presence/config/locales/server.en.yml
source_lang = en
type = YML
[discourse-org.coreplugindetailsclientyml]
file_filter = plugins/discourse-details/config/locales/client.<lang>.yml
source_file = plugins/discourse-details/config/locales/client.en.yml
source_lang = en
type = YML
[discourse-org.coreplugindetailsserveryml]
file_filter = plugins/discourse-details/config/locales/server.<lang>.yml
source_file = plugins/discourse-details/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
source_lang = en
type = YML
[discourse-org.core-plugin-local-dates-server-yml]
file_filter = plugins/discourse-local-dates/config/locales/server.<lang>.yml
source_file = plugins/discourse-local-dates/config/locales/server.en.yml
source_lang = en
type = YML
[discourse-org.403html]
file_filter = public/403.<lang>.html
source_file = public/403.html
source_lang = en
type = HTML
[discourse-org.422html]
file_filter = public/422.<lang>.html
source_file = public/422.html
source_lang = en
type = HTML
[discourse-org.500html]
file_filter = public/500.<lang>.html
source_file = public/500.html
source_lang = en
type = HTML
[discourse-org.503html]
file_filter = public/503.<lang>.html
source_file = public/503.html
source_lang = en
type = HTML

View File

@ -17,7 +17,7 @@ locales_changes = git.modified_files.grep(%r{config/locales})
has_non_en_locales_changes = locales_changes.grep_v(%r{config/locales/(?:client|server)\.(?:en|en_US)\.yml}).any?
if locales_changes.any? && has_non_en_locales_changes
fail("Please submit your non-English translation updates via [Transifex](https://www.transifex.com/discourse/discourse-org/). You can read more on how to contribute translations [here](https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882).")
fail("Please submit your non-English translation updates via [Crowdin](https://translate.discourse.org/). You can read more on how to contribute translations [here](https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882).")
end
files = (git.added_files + git.modified_files)

39
Gemfile
View File

@ -18,13 +18,13 @@ 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.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 'actionmailer', '6.0.3.3'
gem 'actionpack', '6.0.3.3'
gem 'actionview', '6.0.3.3'
gem 'activemodel', '6.0.3.3'
gem 'activerecord', '6.0.3.3'
gem 'activesupport', '6.0.3.3'
gem 'railties', '6.0.3.3'
gem 'sprockets-rails'
end
@ -66,9 +66,10 @@ gem 'http_accept_language', require: false
# Ember related gems need to be pinned cause they control client side
# behavior, we will push these versions up when upgrading ember
gem 'ember-rails', '0.18.5'
gem 'discourse-ember-rails', '0.18.6', require: 'ember-rails'
gem 'discourse-ember-source', '~> 3.12.2'
gem 'ember-handlebars-template', '0.8.0'
gem 'discourse-fonts'
gem 'barber'
@ -76,10 +77,9 @@ gem 'message_bus'
gem 'rails_multisite'
gem 'fast_xs', platform: :mri
gem 'fast_xs', platform: :ruby
# may move to xorcist post: https://github.com/fny/xorcist/issues/4
gem 'fast_xor', platform: :mri
gem 'xorcist'
gem 'fastimage'
@ -101,7 +101,6 @@ gem 'css_parser', require: false
gem 'omniauth'
gem 'omniauth-facebook'
gem 'omniauth-twitter'
gem 'omniauth-instagram'
gem 'omniauth-github'
gem 'omniauth-oauth2', require: false
@ -125,10 +124,9 @@ gem 'mini_scheduler'
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
gem 'highline', require: false
gem 'rack', '2.2.2'
gem 'rack'
gem 'rack-protection' # security
gem 'cbor', require: false
@ -160,10 +158,6 @@ group :test, :development do
gem 'rb-fsevent', require: RUBY_PLATFORM =~ /darwin/i ? 'rb-fsevent' : false
# TODO determine if we can update this to 0.10, API changes happened
# we would like to upgrade it if possible
gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false
gem 'rspec-rails'
gem 'shoulda-matchers', require: false
@ -187,7 +181,7 @@ end
# this is an optional gem, it provides a high performance replacement
# to String#blank? a method that is called quite frequently in current
# ActiveRecord, this may change in the future
gem 'fast_blank', platform: :mri
gem 'fast_blank', platform: :ruby
# this provides a very efficient lru cache
gem 'lru_redux'
@ -201,7 +195,7 @@ gem 'htmlentities', require: false
gem 'flamegraph', require: false
gem 'rack-mini-profiler', require: ['enable_rails_patches']
gem 'unicorn', require: false, platform: :mri
gem 'unicorn', require: false, platform: :ruby
gem 'puma', require: false
gem 'rbtrace', require: false, platform: :mri
gem 'gc_tracer', require: false, platform: :mri
@ -225,6 +219,7 @@ gem 'sassc', '2.0.1', require: false
gem "sassc-rails"
gem 'rotp', require: false
gem 'rqrcode'
gem 'rubyzip', require: false
@ -232,7 +227,7 @@ gem 'rubyzip', require: false
gem 'sshkey', require: false
gem 'rchardet', require: false
gem 'lz4-ruby', require: false, platform: :mri
gem 'lz4-ruby', require: false, platform: :ruby
if ENV["IMPORT"] == "1"
gem 'mysql2'

View File

@ -1,38 +1,38 @@
GEM
remote: https://rubygems.org/
specs:
actionmailer (6.0.3.1)
actionpack (= 6.0.3.1)
actionview (= 6.0.3.1)
activejob (= 6.0.3.1)
actionmailer (6.0.3.3)
actionpack (= 6.0.3.3)
actionview (= 6.0.3.3)
activejob (= 6.0.3.3)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.0.3.1)
actionview (= 6.0.3.1)
activesupport (= 6.0.3.1)
actionpack (6.0.3.3)
actionview (= 6.0.3.3)
activesupport (= 6.0.3.3)
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.3.1)
activesupport (= 6.0.3.1)
actionview (6.0.3.3)
activesupport (= 6.0.3.3)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
actionview_precompiler (0.2.2)
actionview_precompiler (0.2.3)
actionview (>= 6.0.a)
active_model_serializers (0.8.4)
activemodel (>= 3.0)
activejob (6.0.3.1)
activesupport (= 6.0.3.1)
activejob (6.0.3.3)
activesupport (= 6.0.3.3)
globalid (>= 0.3.6)
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)
activemodel (6.0.3.3)
activesupport (= 6.0.3.3)
activerecord (6.0.3.3)
activemodel (= 6.0.3.3)
activesupport (= 6.0.3.3)
activesupport (6.0.3.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@ -45,34 +45,34 @@ GEM
rake (>= 10.4, < 14.0)
ast (2.4.1)
aws-eventstream (1.1.0)
aws-partitions (1.329.0)
aws-sdk-core (3.99.1)
aws-partitions (1.390.0)
aws-sdk-core (3.109.2)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.31.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sdk-kms (1.39.0)
aws-sdk-core (~> 3, >= 3.109.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.66.0)
aws-sdk-core (~> 3, >= 3.96.1)
aws-sdk-s3 (1.83.2)
aws-sdk-core (~> 3, >= 3.109.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sdk-sns (1.25.1)
aws-sdk-core (~> 3, >= 3.99.0)
aws-sdk-sns (1.35.0)
aws-sdk-core (~> 3, >= 3.109.0)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.2.0)
aws-sigv4 (1.2.2)
aws-eventstream (~> 1, >= 1.0.2)
barber (0.12.2)
ember-source (>= 1.0, < 3.1)
execjs (>= 1.2, < 3)
better_errors (2.7.1)
better_errors (2.9.1)
coderay (>= 1.0.0)
erubi (>= 1.0.0)
rack (>= 0.9.0)
binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1)
bootsnap (1.4.6)
bootsnap (1.5.1)
msgpack (~> 1.0)
builder (3.2.4)
bullet (6.1.0)
@ -81,24 +81,31 @@ GEM
byebug (11.1.3)
cbor (0.5.9.6)
certified (1.0.0)
chunky_png (1.3.11)
chunky_png (1.3.14)
coderay (1.1.3)
colored2 (3.1.2)
concurrent-ruby (1.1.6)
concurrent-ruby (1.1.7)
connection_pool (2.2.3)
cose (1.0.0)
cose (1.2.0)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 0.4.0)
openssl-signature_algorithm (~> 1.0)
cppjieba_rb (0.3.3)
crack (0.4.3)
safe_yaml (~> 1.0.0)
crack (0.4.4)
crass (1.0.6)
css_parser (1.7.1)
addressable
debug_inspector (0.0.3)
diff-lcs (1.4.1)
diffy (3.3.0)
discourse-ember-source (3.12.2.0)
diff-lcs (1.4.4)
diffy (3.4.0)
discourse-ember-rails (0.18.6)
active_model_serializers
ember-data-source (>= 1.0.0.beta.5)
ember-handlebars-template (>= 0.1.1, < 1.0)
ember-source (>= 1.1.0)
jquery-rails (>= 1.0.17)
railties (>= 3.1)
discourse-ember-source (3.12.2.2)
discourse-fonts (0.0.5)
discourse_image_optim (0.26.2)
exifr (~> 1.2, >= 1.2.2)
fspath (~> 3.0)
@ -112,28 +119,19 @@ GEM
ember-handlebars-template (0.8.0)
barber (>= 0.11.0)
sprockets (>= 3.3, < 4.1)
ember-rails (0.18.5)
active_model_serializers
ember-data-source (>= 1.0.0.beta.5)
ember-handlebars-template (>= 0.1.1, < 1.0)
ember-source (>= 1.1.0)
jquery-rails (>= 1.0.17)
railties (>= 3.1)
ember-source (2.18.2)
erubi (1.9.0)
excon (0.75.0)
erubi (1.10.0)
excon (0.78.0)
execjs (2.7.0)
exifr (1.3.6)
exifr (1.3.9)
fabrication (2.21.1)
fakeweb (1.3.0)
faraday (1.0.1)
faraday (1.1.0)
multipart-post (>= 1.2, < 3)
ruby2_keywords
fast_blank (1.0.0)
fast_xor (1.1.3)
rake
rake-compiler
fast_xs (0.8.0)
fastimage (2.1.7)
fastimage (2.2.0)
ffi (1.13.1)
flamegraph (0.9.5)
fspath (3.1.2)
@ -143,11 +141,11 @@ GEM
guess_html_encoding (0.0.11)
hashdiff (1.0.1)
hashie (4.1.0)
highline (1.7.10)
highline (2.0.3)
hkdf (0.3.0)
htmlentities (4.3.4)
http_accept_language (2.1.1)
i18n (1.8.3)
i18n (1.8.5)
concurrent-ruby (~> 1.0)
image_size (1.5.0)
in_threads (1.5.4)
@ -156,13 +154,13 @@ GEM
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.3.0)
json (2.3.1)
json-schema (2.8.1)
addressable (>= 2.4)
jwt (2.2.1)
jwt (2.2.2)
kgio (2.11.3)
libv8 (8.4.255.0)
listen (3.2.1)
listen (3.3.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
lograge (0.11.2)
@ -173,8 +171,8 @@ GEM
logstash-event (1.2.02)
logstash-logger (0.26.1)
logstash-event (~> 1.2)
logster (2.9.0)
loofah (2.6.0)
logster (2.9.4)
loofah (2.8.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lru_redux (1.1.0)
@ -183,28 +181,28 @@ GEM
mini_mime (>= 0.1.1)
maxminddb (0.1.22)
memory_profiler (0.9.14)
message_bus (3.3.1)
message_bus (3.3.4)
rack (>= 1.1.3)
method_source (1.0.0)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
mini_racer (0.3.1)
libv8 (~> 8.4.255)
mini_scheduler (0.12.2)
mini_scheduler (0.12.3)
sidekiq
mini_sql (0.2.5)
mini_sql (0.3)
mini_suffix (0.3.0)
ffi (~> 1.9)
minitest (5.14.1)
minitest (5.14.2)
mocha (1.11.2)
mock_redis (0.24.0)
mock_redis (0.26.0)
msgpack (1.3.3)
multi_json (1.14.1)
multi_json (1.15.0)
multi_xml (0.6.0)
multipart-post (2.1.1)
mustache (1.1.1)
nio4r (2.5.2)
nokogiri (1.10.9)
nio4r (2.5.4)
nokogiri (1.10.10)
mini_portile2 (~> 2.4.0)
nokogumbo (2.0.2)
nokogiri (~> 1.8, >= 1.8.4)
@ -215,11 +213,11 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
oj (3.10.6)
oj (3.10.16)
omniauth (1.9.1)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
omniauth-facebook (6.0.0)
omniauth-facebook (8.0.0)
omniauth-oauth2 (~> 1.2)
omniauth-github (1.4.0)
omniauth (~> 1.5)
@ -228,31 +226,28 @@ GEM
jwt (>= 2.0)
omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.6)
omniauth-instagram (1.3.0)
omniauth (~> 1)
omniauth-oauth2 (~> 1)
omniauth-oauth (1.1.0)
oauth
omniauth (~> 1.0)
omniauth-oauth2 (1.6.0)
oauth2 (~> 1.1)
omniauth-oauth2 (1.7.0)
oauth2 (~> 1.4)
omniauth (~> 1.9)
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
onebox (1.9.29)
onebox (2.1.7)
addressable (~> 2.7.0)
htmlentities (~> 4.3)
multi_json (~> 1.11)
mustache
nokogiri (~> 1.7)
sanitize
openssl-signature_algorithm (0.4.0)
openssl-signature_algorithm (1.0.0)
optimist (3.0.1)
parallel (1.19.2)
parallel_tests (3.0.0)
parallel (1.20.1)
parallel_tests (3.4.0)
parallel
parser (2.7.1.4)
parser (2.7.2.0)
ast (~> 2.4.1)
pg (1.2.3)
progress (3.5.2)
@ -264,14 +259,14 @@ GEM
pry (~> 0.13.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.5)
puma (4.3.5)
public_suffix (4.0.6)
puma (5.0.4)
nio4r (~> 2.0)
r2 (0.2.7)
rack (2.2.2)
rack-mini-profiler (2.0.2)
rack (2.2.3)
rack-mini-profiler (2.2.0)
rack (>= 1.2.0)
rack-protection (2.0.8.1)
rack-protection (2.1.0)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
@ -280,60 +275,58 @@ GEM
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
rails_failover (0.5.5)
rails_failover (0.6.2)
activerecord (~> 6.0)
concurrent-ruby
railties (~> 6.0)
rails_multisite (2.3.0)
rails_multisite (2.5.0)
activerecord (> 5.0, < 7)
railties (> 5.0, < 7)
railties (6.0.3.1)
actionpack (= 6.0.3.1)
activesupport (= 6.0.3.1)
railties (6.0.3.3)
actionpack (= 6.0.3.3)
activesupport (= 6.0.3.3)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
rainbow (3.0.0)
raindrops (0.19.1)
rake (13.0.1)
rake-compiler (1.1.0)
rake
rb-fsevent (0.10.4)
rb-inotify (0.10.1)
ffi (~> 1.0)
rbtrace (0.4.13)
rbtrace (0.4.14)
ffi (>= 1.0.6)
msgpack (>= 0.4.3)
optimist (>= 3.0.0)
rchardet (1.8.0)
redis (4.2.1)
redis-namespace (1.7.0)
redis (4.2.5)
redis-namespace (1.8.0)
redis (>= 3.0.4)
regexp_parser (1.7.1)
regexp_parser (2.0.0)
request_store (1.5.0)
rack (>= 1.4)
rexml (3.2.4)
rinku (2.0.6)
rotp (5.1.0)
addressable (~> 2.5)
rotp (6.2.0)
rqrcode (1.1.2)
chunky_png (~> 1.0)
rqrcode_core (~> 0.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.2)
rspec-support (~> 3.9.3)
rspec-expectations (3.9.2)
rspec (3.10.0)
rspec-core (~> 3.10.0)
rspec-expectations (~> 3.10.0)
rspec-mocks (~> 3.10.0)
rspec-core (3.10.0)
rspec-support (~> 3.10.0)
rspec-expectations (3.10.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-html-matchers (0.9.2)
rspec-support (~> 3.10.0)
rspec-html-matchers (0.9.4)
nokogiri (~> 1)
rspec (>= 3.0.0.a, < 4)
rspec-mocks (3.9.1)
rspec-mocks (3.10.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-support (~> 3.10.0)
rspec-rails (4.0.1)
actionpack (>= 4.2)
activesupport (>= 4.2)
@ -342,35 +335,36 @@ GEM
rspec-expectations (~> 3.9)
rspec-mocks (~> 3.9)
rspec-support (~> 3.9)
rspec-support (3.9.3)
rspec-support (3.10.0)
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.86.0)
rubocop (1.4.2)
parallel (~> 1.10)
parser (>= 2.7.0.1)
parser (>= 2.7.1.5)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.7)
regexp_parser (>= 1.8)
rexml
rubocop-ast (>= 0.0.3, < 1.0)
rubocop-ast (>= 1.1.1)
ruby-progressbar (~> 1.7)
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)
rubocop-rspec (>= 1.39.0)
rubocop-rspec (1.40.0)
rubocop (>= 0.68.1)
ruby-prof (1.4.1)
rubocop-ast (1.2.0)
parser (>= 2.7.1.5)
rubocop-discourse (2.4.1)
rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0)
rubocop-rspec (2.0.0)
rubocop (~> 1.0)
rubocop-ast (>= 1.1.0)
ruby-prof (1.4.2)
ruby-progressbar (1.10.1)
ruby-readability (0.7.0)
guess_html_encoding (>= 0.0.4)
nokogiri (>= 1.6.0)
ruby2_keywords (0.0.2)
rubyzip (2.3.0)
safe_yaml (1.0.5)
sanitize (5.2.1)
crass (~> 1.0.2)
nokogiri (>= 1.8.0)
@ -387,31 +381,32 @@ GEM
seed-fu (2.3.9)
activerecord (>= 3.1)
activesupport (>= 3.1)
shoulda-matchers (4.3.0)
shoulda-matchers (4.4.1)
activesupport (>= 4.2.0)
sidekiq (6.0.7)
sidekiq (6.1.2)
connection_pool (>= 2.2.2)
rack (~> 2.0)
rack-protection (>= 2.0.0)
redis (>= 4.1.0)
simplecov (0.18.5)
redis (>= 4.2.0)
simplecov (0.20.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov-html (0.12.2)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.2)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
sprockets-rails (3.2.2)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sshkey (2.0.0)
stackprof (0.2.15)
test-prof (0.11.3)
stackprof (0.2.16)
test-prof (0.12.2)
thor (1.0.1)
thread_safe (0.3.6)
tilt (2.0.10)
tzinfo (1.2.7)
tzinfo (1.2.8)
thread_safe (~> 0.1)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
@ -419,32 +414,33 @@ GEM
unf_ext
unf_ext (0.0.7.7)
unicode-display_width (1.7.0)
unicorn (5.5.5)
unicorn (5.7.0)
kgio (~> 2.6)
raindrops (~> 0.7)
uniform_notifier (1.13.0)
webmock (3.8.3)
webmock (3.10.0)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webpush (1.0.0)
webpush (1.1.0)
hkdf (~> 0.2)
jwt (~> 2.0)
xorcist (1.1.2)
yaml-lint (0.0.10)
zeitwerk (2.3.0)
zeitwerk (2.4.1)
PLATFORMS
ruby
DEPENDENCIES
actionmailer (= 6.0.3.1)
actionpack (= 6.0.3.1)
actionview (= 6.0.3.1)
actionmailer (= 6.0.3.3)
actionpack (= 6.0.3.3)
actionview (= 6.0.3.3)
actionview_precompiler
active_model_serializers (~> 0.8.3)
activemodel (= 6.0.3.1)
activerecord (= 6.0.3.1)
activesupport (= 6.0.3.1)
activemodel (= 6.0.3.3)
activerecord (= 6.0.3.3)
activesupport (= 6.0.3.3)
addressable
annotate
aws-sdk-s3
@ -462,22 +458,22 @@ DEPENDENCIES
cppjieba_rb
css_parser
diffy
discourse-ember-rails (= 0.18.6)
discourse-ember-source (~> 3.12.2)
discourse-fonts
discourse_image_optim
email_reply_trimmer
ember-handlebars-template (= 0.8.0)
ember-rails (= 0.18.5)
excon
execjs
fabrication
fakeweb
fast_blank
fast_xor
fast_xs
fastimage
flamegraph
gc_tracer
highline (~> 1.7.0)
highline
htmlentities
http_accept_language
json
@ -508,7 +504,6 @@ DEPENDENCIES
omniauth-facebook
omniauth-github
omniauth-google-oauth2
omniauth-instagram
omniauth-oauth2
omniauth-twitter
onebox
@ -518,15 +513,14 @@ DEPENDENCIES
pry-rails
puma
r2
rack (= 2.2.2)
rack
rack-mini-profiler
rack-protection
rails_failover
rails_multisite
railties (= 6.0.3.1)
railties (= 6.0.3.3)
rake
rb-fsevent
rb-inotify (~> 0.9)
rbtrace
rchardet
redis
@ -560,6 +554,7 @@ DEPENDENCIES
unicorn
webmock
webpush
xorcist
yaml-lint
BUNDLED WITH

View File

@ -17,8 +17,9 @@ To learn more about the philosophy and goals of the project, [visit **discourse.
<a href="https://bbs.boingboing.net"><img alt="Boing Boing" src="https://user-images.githubusercontent.com/1681963/52239245-04ad8280-289c-11e9-9c88-8c173d4a0422.png" width="720px"></a>
<a href="https://twittercommunity.com/"><img src="https://user-images.githubusercontent.com/1681963/52239250-04ad8280-289c-11e9-9e42-574f6eaab9d7.png" width="720px"></a>
<a href="https://discuss.howtogeek.com"><img src="https://user-images.githubusercontent.com/1681963/52239247-04ad8280-289c-11e9-9706-fd66bc0749dc.png" width="720px"></a>
<a href="https://talk.turtlerockstudios.com/"><img src="https://user-images.githubusercontent.com/1681963/52239249-04ad8280-289c-11e9-9155-f0ccc5decc50.png" width="720px"></a>
<a href="https://discuss.atom.io/"><img src="https://user-images.githubusercontent.com/1681963/89088039-6735f080-d364-11ea-93a6-5629ea8738fe.png" width="720px"></a>
<a href="https://forums.gearboxsoftware.com/"><img src="https://user-images.githubusercontent.com/1681963/89088042-68ffb400-d364-11ea-93be-161ea04d8b29.png" width="720px"></a>
<img src="https://user-images.githubusercontent.com/1681963/52239118-b304f800-289b-11e9-9904-16450680d9ec.jpg" alt="Mobile" width="414">
@ -61,6 +62,7 @@ Discourse supports the **latest, stable releases** of all major browsers and pla
- [Ember.js](https://github.com/emberjs/ember.js) &mdash; Our front end is an Ember.js app that communicates with the Rails API.
- [PostgreSQL](https://www.postgresql.org/) &mdash; Our main data store is in Postgres.
- [Redis](https://redis.io/) &mdash; We use Redis as a cache and for transient data.
- [BrowserStack](https://www.browserstack.com/) &mdash; We use BrowserStack to test on real devices and browsers.
Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https://github.com/discourse/discourse/blob/master/Gemfile).
@ -90,7 +92,6 @@ We take security very seriously at Discourse; all our code is 100% open source a
The original Discourse code contributors can be found in [**AUTHORS.MD**](docs/AUTHORS.md). For a complete list of the many individuals that contributed to the design and implementation of Discourse, please refer to [the official Discourse blog](https://blog.discourse.org/2013/02/the-discourse-team/) and [GitHub's list of contributors](https://github.com/discourse/discourse/contributors).
## Copyright / License
Copyright 2014 - 2020 Civilized Discourse Construction Kit, Inc.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 882 B

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 937 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 984 B

After

Width:  |  Height:  |  Size: 898 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 895 B

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 947 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 613 B

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 845 B

After

Width:  |  Height:  |  Size: 759 B

View File

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

View File

@ -1,11 +0,0 @@
import RESTAdapter from "discourse/adapters/rest";
export default RESTAdapter.extend({
basePath() {
return "/admin/api/";
},
apiNameFor() {
return "key";
}
});

View File

@ -1,11 +0,0 @@
import RestAdapter from "discourse/adapters/rest";
export default function buildPluginAdapter(pluginName) {
return RestAdapter.extend({
pathFor(store, type, findArgs) {
return (
"/admin/plugins/" + pluginName + this._super(store, type, findArgs)
);
}
});
}

View File

@ -1,7 +0,0 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
basePath() {
return "/admin/customize/";
}
});

View File

@ -1,7 +0,0 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
pathFor() {
return "/admin/customize/email_style";
}
});

View File

@ -1,7 +0,0 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
pathFor() {
return "/admin/customize/embedding";
}
});

View File

@ -1,7 +0,0 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
basePath() {
return "/admin/logs/";
}
});

View File

@ -1,5 +0,0 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
jsonMode: true
});

View File

@ -1,26 +0,0 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
basePath() {
return "/admin/";
},
afterFindAll(results) {
let map = {};
results.forEach(theme => {
map[theme.id] = theme;
});
results.forEach(theme => {
let mapped = theme.get("child_themes") || [];
mapped = mapped.map(t => map[t.id]);
theme.set("childThemes", mapped);
let mappedParents = theme.get("parent_themes") || [];
mappedParents = mappedParents.map(t => map[t.id]);
theme.set("parentThemes", mappedParents);
});
return results;
},
jsonMode: true
});

View File

@ -1,7 +0,0 @@
import RESTAdapter from "discourse/adapters/rest";
export default RESTAdapter.extend({
basePath() {
return "/admin/api/";
}
});

View File

@ -1,7 +0,0 @@
import RESTAdapter from "discourse/adapters/rest";
export default RESTAdapter.extend({
basePath() {
return "/admin/api/";
}
});

View File

@ -0,0 +1,13 @@
import RESTAdapter from "discourse/adapters/rest";
export default RESTAdapter.extend({
jsonMode: true,
basePath() {
return "/admin/api/";
},
apiNameFor() {
return "key";
},
});

View File

@ -0,0 +1,11 @@
import RestAdapter from "discourse/adapters/rest";
export default function buildPluginAdapter(pluginName) {
return RestAdapter.extend({
pathFor(store, type, findArgs) {
return (
"/admin/plugins/" + pluginName + this._super(store, type, findArgs)
);
},
});
}

View File

@ -0,0 +1,7 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
basePath() {
return "/admin/customize/";
},
});

View File

@ -0,0 +1,7 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
pathFor() {
return "/admin/customize/email_style";
},
});

View File

@ -0,0 +1,7 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
pathFor() {
return "/admin/customize/embedding";
},
});

View File

@ -0,0 +1,7 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
basePath() {
return "/admin/logs/";
},
});

View File

@ -0,0 +1,5 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
jsonMode: true,
});

View File

@ -0,0 +1,26 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
basePath() {
return "/admin/";
},
afterFindAll(results) {
let map = {};
results.forEach((theme) => {
map[theme.id] = theme;
});
results.forEach((theme) => {
let mapped = theme.get("child_themes") || [];
mapped = mapped.map((t) => map[t.id]);
theme.set("childThemes", mapped);
let mappedParents = theme.get("parent_themes") || [];
mappedParents = mappedParents.map((t) => map[t.id]);
theme.set("parentThemes", mappedParents);
});
return results;
},
jsonMode: true,
});

View File

@ -0,0 +1,7 @@
import RESTAdapter from "discourse/adapters/rest";
export default RESTAdapter.extend({
basePath() {
return "/admin/api/";
},
});

View File

@ -0,0 +1,7 @@
import RESTAdapter from "discourse/adapters/rest";
export default RESTAdapter.extend({
basePath() {
return "/admin/api/";
},
});

View File

@ -0,0 +1,134 @@
import Component from "@ember/component";
import loadScript from "discourse/lib/load-script";
import getURL from "discourse-common/lib/get-url";
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("placeholder")
placeholderChanged() {
if (this._editor) {
this._editor.setOptions({
placeholder: this.placeholder,
});
}
},
@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) => {
loadedAce.config.set("loadWorkerFromBlob", false);
loadedAce.config.set("workerPath", getURL("/javascripts/ace")); // Do not use CDN for workers
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", placeholder: this.placeholder });
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,76 @@
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

@ -0,0 +1,24 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "",
buffer: "",
editing: false,
init() {
this._super(...arguments);
this.set("editing", false);
},
actions: {
edit() {
this.set("buffer", this.value);
this.toggleProperty("editing");
},
save() {
// Action has to toggle 'editing' property.
this.action(this.buffer);
},
},
});

View File

@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
classNames: ["row"],
});

View File

@ -0,0 +1,57 @@
import Component from "@ember/component";
import loadScript from "discourse/lib/load-script";
export default Component.extend({
tagName: "canvas",
type: "line",
refreshChart() {
const ctx = this.element.getContext("2d");
const model = this.model;
const rawData = this.get("model.data");
var data = {
labels: rawData.map((r) => r.x),
datasets: [
{
data: rawData.map((r) => r.y),
label: model.get("title"),
backgroundColor: `rgba(200,220,240,${this.type === "bar" ? 1 : 0.3})`,
borderColor: "#08C",
},
],
};
const config = {
type: this.type,
data: data,
options: {
responsive: true,
tooltips: {
callbacks: {
title: (context) =>
moment(context[0].xLabel, "YYYY-MM-DD").format("LL"),
},
},
scales: {
yAxes: [
{
display: true,
ticks: {
stepSize: 1,
},
},
],
},
},
};
this._chart = new window.Chart(ctx, config);
},
didInsertElement() {
loadScript("/javascripts/Chart.min.js").then(() =>
this.refreshChart.apply(this)
);
},
});

View File

@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "",
});

View File

@ -0,0 +1,239 @@
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,
options: null,
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 = this._applyChartGrouping(
model,
makeArray(model.get("chartData") || model.get("data"), "weekly"),
this.options
);
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, this.options)
);
});
},
_buildChartConfig(data, options) {
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: {
unit: this._unitForGrouping(options),
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50,
},
},
],
},
},
};
},
_resetChart() {
if (this._chart) {
this._chart.destroy();
this._chart = null;
}
},
_applyChartGrouping(model, data, options) {
if (!options.chartGrouping || options.chartGrouping === "daily") {
return data;
}
if (
options.chartGrouping === "weekly" ||
options.chartGrouping === "monthly"
) {
const isoKind = options.chartGrouping === "weekly" ? "isoWeek" : "month";
const kind = options.chartGrouping === "weekly" ? "week" : "month";
const startMoment = moment(model.start_date, "YYYY-MM-DD");
let currentIndex = 0;
let currentStart = startMoment.clone().startOf(isoKind);
let currentEnd = startMoment.clone().endOf(isoKind);
const transformedData = [
{
x: currentStart.format("YYYY-MM-DD"),
y: 0,
},
];
data.forEach((d) => {
let date = moment(d.x, "YYYY-MM-DD");
if (!date.isBetween(currentStart, currentEnd)) {
currentIndex += 1;
currentStart = currentStart.add(1, kind).startOf(isoKind);
currentEnd = currentEnd.add(1, kind).endOf(isoKind);
}
if (transformedData[currentIndex]) {
transformedData[currentIndex].y += d.y;
} else {
transformedData[currentIndex] = {
x: d.x,
y: d.y,
};
}
});
return transformedData;
}
// ensure we return something if grouping is unknown
return data;
},
_unitForGrouping(options) {
switch (options.chartGrouping) {
case "monthly":
return "month";
case "weekly":
return "week";
default:
return "day";
}
},
});

View File

@ -0,0 +1,6 @@
import Component from "@ember/component";
export default Component.extend({
classNames: ["admin-report-counters"],
attributeBindings: ["model.description:title"],
});

View File

@ -0,0 +1,11 @@
import { match } from "@ember/object/computed";
import Component from "@ember/component";
export default Component.extend({
allTime: true,
tagName: "tr",
reverseColors: match(
"report.type",
/^(time_to_first_response|topics_with_no_response)$/
),
classNameBindings: ["reverseColors"],
});

View File

@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
classNames: ["admin-report-inline-table"],
});

View File

@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "tr",
});

View File

@ -0,0 +1,159 @@
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

@ -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

@ -0,0 +1,20 @@
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
export default Component.extend({
tagName: "td",
classNames: ["admin-report-table-cell"],
classNameBindings: ["type", "property"],
options: null,
@discourseComputed("label", "data", "options")
computedLabel(label, data, options) {
return label.compute(data, options || {});
},
type: alias("label.type"),
property: alias("label.mainProperty"),
formatedValue: alias("computedLabel.formatedValue"),
value: alias("computedLabel.value"),
});

View File

@ -0,0 +1,19 @@
import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component";
export default Component.extend({
tagName: "th",
classNames: ["admin-report-table-header"],
classNameBindings: ["label.mainProperty", "label.type", "isCurrentSort"],
attributeBindings: ["label.title:title"],
@discourseComputed("currentSortLabel.sortProperty", "label.sortProperty")
isCurrentSort(currentSortField, labelSortField) {
return currentSortField === labelSortField;
},
@discourseComputed("currentSortDirection")
sortIcon(currentSortDirection) {
return currentSortDirection === 1 ? "caret-up" : "caret-down";
},
});

View File

@ -0,0 +1,6 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "tr",
classNames: ["admin-report-table-row"],
options: null,
});

View File

@ -0,0 +1,174 @@
import discourseComputed from "discourse-common/utils/decorators";
import { makeArray } from "discourse-common/lib/helpers";
import { alias } from "@ember/object/computed";
import Component from "@ember/component";
const PAGES_LIMIT = 8;
export default Component.extend({
classNameBindings: ["sortable", "twoColumns"],
classNames: ["admin-report-table"],
sortable: false,
sortDirection: 1,
perPage: alias("options.perPage"),
page: 0,
@discourseComputed("model.computedLabels.length")
twoColumns(labelsLength) {
return labelsLength === 2;
},
@discourseComputed(
"totalsForSample",
"options.total",
"model.dates_filtering"
)
showTotalForSample(totalsForSample, total, datesFiltering) {
// check if we have at least one cell which contains a value
const sum = totalsForSample
.map((t) => t.value)
.compact()
.reduce((s, v) => s + v, 0);
return sum >= 1 && total && datesFiltering;
},
@discourseComputed("model.total", "options.total", "twoColumns")
showTotal(reportTotal, total, twoColumns) {
return reportTotal && total && twoColumns;
},
@discourseComputed(
"model.{average,data}",
"totalsForSample.1.value",
"twoColumns"
)
showAverage(model, sampleTotalValue, hasTwoColumns) {
return (
model.average &&
model.data.length > 0 &&
sampleTotalValue &&
hasTwoColumns
);
},
@discourseComputed("totalsForSample.1.value", "model.data.length")
averageForSample(totals, count) {
return (totals / count).toFixed(0);
},
@discourseComputed("model.data.length")
showSortingUI(dataLength) {
return dataLength >= 5;
},
@discourseComputed("totalsForSampleRow", "model.computedLabels")
totalsForSample(row, labels) {
return labels.map((label) => {
const computedLabel = label.compute(row);
computedLabel.type = label.type;
computedLabel.property = label.mainProperty;
return computedLabel;
});
},
@discourseComputed("model.data", "model.computedLabels")
totalsForSampleRow(rows, labels) {
if (!rows || !rows.length) {
return {};
}
let totalsRow = {};
labels.forEach((label) => {
const reducer = (sum, row) => {
const computedLabel = label.compute(row);
const value = computedLabel.value;
if (!["seconds", "number", "percent"].includes(label.type)) {
return;
} else {
return sum + Math.round(value || 0);
}
};
const total = rows.reduce(reducer, 0);
totalsRow[label.mainProperty] =
label.type === "percent" ? Math.round(total / rows.length) : total;
});
return totalsRow;
},
@discourseComputed("sortLabel", "sortDirection", "model.data.[]")
sortedData(sortLabel, sortDirection, data) {
data = makeArray(data);
if (sortLabel) {
const compare = (label, direction) => {
return (a, b) => {
const aValue = label.compute(a, { useSortProperty: true }).value;
const bValue = label.compute(b, { useSortProperty: true }).value;
const result = aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
return result * direction;
};
};
return data.sort(compare(sortLabel, sortDirection));
}
return data;
},
@discourseComputed("sortedData.[]", "perPage", "page")
paginatedData(data, perPage, page) {
if (perPage < data.length) {
const start = perPage * page;
return data.slice(start, start + perPage);
}
return data;
},
@discourseComputed("model.data", "perPage", "page")
pages(data, perPage, page) {
if (!data || data.length <= perPage) {
return [];
}
const pagesIndexes = [];
for (let i = 0; i < Math.ceil(data.length / perPage); i++) {
pagesIndexes.push(i);
}
let pages = pagesIndexes.map((v) => {
return {
page: v + 1,
index: v,
class: v === page ? "is-current" : null,
};
});
if (pages.length > PAGES_LIMIT) {
const before = Math.max(0, page - PAGES_LIMIT / 2);
const after = Math.max(PAGES_LIMIT, page + PAGES_LIMIT / 2);
pages = pages.slice(before, after);
}
return pages;
},
actions: {
changePage(page) {
this.set("page", page);
},
sortByLabel(label) {
if (this.sortLabel === label) {
this.set("sortDirection", this.sortDirection === 1 ? -1 : 1);
} else {
this.set("sortLabel", label);
}
},
},
});

View File

@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "tr",
});

View File

@ -0,0 +1,453 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { makeArray } from "discourse-common/lib/helpers";
import { alias, or, and, equal, notEmpty } 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: [
"isHidden:hidden",
"isHidden::is-visible",
"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"),
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")
isChartMode(currentMode) {
return currentMode === "chart";
},
@action
changeGrouping(grouping) {
this.send("refreshReport", {
chartGrouping: grouping,
});
},
@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;
},
@discourseComputed("reportOptions.chartGrouping")
chartGroupings(chartGrouping) {
chartGrouping = chartGrouping || "daily";
return ["daily", "weekly", "monthly"].map((id) => {
return {
id,
label: `admin.dashboard.reports.${id}`,
class: `chart-grouping ${chartGrouping === id ? "active" : "inactive"}`,
};
});
},
@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 = {}) {
if (!this.attrs.onRefresh) {
return;
}
this.attrs.onRefresh({
type: this.get("model.type"),
chartGrouping: options.chartGrouping,
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);
this.send("refreshReport", {
chartGrouping: null,
});
},
_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") || {}, {
chartGrouping: this.get("reportOptions.chartGrouping"),
})
);
}
},
_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,115 @@
import I18n from "I18n";
import { next } from "@ember/runloop";
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
import { fmt } from "discourse/lib/computed";
export default Component.extend({
@discourseComputed("theme.targets", "onlyOverridden", "showAdvanced")
visibleTargets(targets, onlyOverridden, showAdvanced) {
return targets.filter((target) => {
if (target.advanced && !showAdvanced) {
return false;
}
if (!onlyOverridden) {
return true;
}
return target.edited;
});
},
@discourseComputed("currentTargetName", "onlyOverridden", "theme.fields")
visibleFields(targetName, onlyOverridden, fields) {
fields = fields[targetName];
if (onlyOverridden) {
fields = fields.filter((field) => field.edited);
}
return fields;
},
@discourseComputed("currentTargetName", "fieldName")
activeSectionMode(targetName, fieldName) {
if (["settings", "translations"].includes(targetName)) {
return "yaml";
}
if (["extra_scss"].includes(targetName)) {
return "scss";
}
if (["color_definitions"].includes(fieldName)) {
return "scss";
}
return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html";
},
@discourseComputed("currentTargetName", "fieldName")
placeholder(targetName, fieldName) {
return fieldName && fieldName === "color_definitions"
? I18n.t("admin.customize.theme.color_definitions.placeholder")
: "";
},
@discourseComputed("fieldName", "currentTargetName", "theme")
activeSection: {
get(fieldName, target, model) {
return model.getField(target, fieldName);
},
set(value, fieldName, target, model) {
model.setField(target, fieldName, value);
return value;
},
},
editorId: fmt("fieldName", "currentTargetName", "%@|%@"),
@discourseComputed("maximized")
maximizeIcon(maximized) {
return maximized ? "discourse-compress" : "discourse-expand";
},
@discourseComputed("currentTargetName", "theme.targets")
showAddField(currentTargetName, targets) {
return targets.find((t) => t.name === currentTargetName).customNames;
},
@discourseComputed(
"currentTargetName",
"fieldName",
"theme.theme_fields.@each.error"
)
error(target, fieldName) {
return this.theme.getError(target, fieldName);
},
actions: {
toggleShowAdvanced() {
this.toggleProperty("showAdvanced");
},
toggleAddField() {
this.toggleProperty("addingField");
},
cancelAddField() {
this.set("addingField", false);
},
addField(name) {
if (!name) {
return;
}
name = name.replace(/[^a-zA-Z0-9-_/]/g, "");
this.theme.setField(this.currentTargetName, name, "");
this.setProperties({ newFieldName: "", addingField: false });
this.fieldAdded(this.currentTargetName, name);
},
toggleMaximize: function () {
this.toggleProperty("maximized");
next(() => this.appEvents.trigger("ace:resize"));
},
onlyOverriddenChanged(value) {
this.onlyOverriddenChanged(value);
},
},
});

View File

@ -0,0 +1,107 @@
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, 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,30 @@
import I18n from "I18n";
import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
import bootbox from "bootbox";
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

@ -0,0 +1,113 @@
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";
import bootbox from "bootbox";
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 = Object.assign(
{
"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

@ -0,0 +1,12 @@
import Component from "@ember/component";
export default Component.extend({
didInsertElement() {
this._super(...arguments);
$("body").addClass("admin-interface");
},
willDestroyElement() {
this._super(...arguments);
$("body").removeClass("admin-interface");
},
});

View File

@ -0,0 +1,4 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "",
});

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

@ -0,0 +1,54 @@
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { reads } from "@ember/object/computed";
import Component from "@ember/component";
import bootbox from "bootbox";
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");
}
}
);
},
},
});

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