Merge master
This commit is contained in:
commit
607d00f780
@ -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/
|
||||
|
||||
@ -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
|
||||
|
||||
64
.github/workflows/ci.yml
vendored
64
.github/workflows/ci.yml
vendored
@ -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
13
.gitignore
vendored
@ -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
|
||||
|
||||
147
.rubocop.yml
147
.rubocop.yml
@ -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/**/*'
|
||||
|
||||
@ -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
|
||||
}
|
||||
};
|
||||
|
||||
86
.travis.yml
86
.travis.yml
@ -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
|
||||
"
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
45
Gemfile
@ -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
|
||||
|
||||
281
Gemfile.lock
281
Gemfile.lock
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
[](https://travis-ci.org/discourse/discourse)
|
||||
[](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 – 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.
|
||||
|
||||
25
app/assets/javascripts/activate-account.js
Normal file
25
app/assets/javascripts/activate-account.js
Normal 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);
|
||||
})();
|
||||
@ -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);
|
||||
})();
|
||||
122
app/assets/javascripts/admin/components/ace-editor.js
Normal file
122
app/assets/javascripts/admin/components/ace-editor.js
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
167
app/assets/javascripts/admin/components/admin-report-chart.js
Normal file
167
app/assets/javascripts/admin/components/admin-report-chart.js
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
413
app/assets/javascripts/admin/components/admin-report.js
Normal file
413
app/assets/javascripts/admin/components/admin-report.js
Normal 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);
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
108
app/assets/javascripts/admin/components/admin-user-field-item.js
Normal file
108
app/assets/javascripts/admin/components/admin-user-field-item.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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}`
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -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}`
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
110
app/assets/javascripts/admin/components/admin-web-hook-event.js
Normal file
110
app/assets/javascripts/admin/components/admin-web-hook-event.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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")}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -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")}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
73
app/assets/javascripts/admin/components/color-input.js
Normal file
73
app/assets/javascripts/admin/components/color-input.js
Normal 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());
|
||||
}
|
||||
});
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
81
app/assets/javascripts/admin/components/embeddable-host.js
Normal file
81
app/assets/javascripts/admin/components/embeddable-host.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
114
app/assets/javascripts/admin/components/ip-lookup.js
Normal file
114
app/assets/javascripts/admin/components/ip-lookup.js
Normal 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"));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
88
app/assets/javascripts/admin/components/permalink-form.js
Normal file
88
app/assets/javascripts/admin/components/permalink-form.js
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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"
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
@ -1,8 +0,0 @@
|
||||
import Component from "@ember/component";
|
||||
export default Component.extend({
|
||||
actions: {
|
||||
onChange(value) {
|
||||
this.applyFilter(this.get("filter.id"), value);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,3 @@
|
||||
import FilterComponent from "admin/components/report-filters/filter";
|
||||
|
||||
export default FilterComponent.extend();
|
||||
140
app/assets/javascripts/admin/components/resumable-upload.js
Normal file
140
app/assets/javascripts/admin/components/resumable-upload.js
Normal 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();
|
||||
}
|
||||
});
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
@ -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";
|
||||
}
|
||||
});
|
||||
@ -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");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -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");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
105
app/assets/javascripts/admin/components/secret-value-list.js
Normal file
105
app/assets/javascripts/admin/components/secret-value-list.js
Normal 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
Reference in New Issue
Block a user