mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-09-16 10:48:47 +00:00
Merge branch 'main' into bug6226
This commit is contained in:
@@ -4,8 +4,11 @@
|
||||
# shall be synchronized to the Github repo at github.com/SAP/sailing-analytics
|
||||
name: Merge main from trac@sapsailing.com:/home/trac/git
|
||||
on:
|
||||
schedule:
|
||||
- cron: "53 * * * *" # every hour, at :53 minutes
|
||||
# In the upstream repo, we currently don't want to fetch/merge any downstream branches;
|
||||
# this may change again when, e.g., the Wiki at https://wiki.sapsailing.com uses a
|
||||
# Git workspace that synchronizes with this upstream repo.
|
||||
#schedule:
|
||||
# - cron: "53 * * * *" # every hour, at :53 minutes
|
||||
workflow_dispatch: {}
|
||||
jobs:
|
||||
merge-master-from-sapsailing-com:
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Periodically merges the upstream Eclipse repo's main branch into our
|
||||
# downstream eclipse-main branch and opens (or reuses) a PR from eclipse-main
|
||||
# into main. The merge/push step uses GITHUB_TOKEN (so github-actions[bot] is
|
||||
# the last pusher), and the PR is opened by the eclipse-sailing-analytics-bot
|
||||
# user via ECLIPSE_BOT_PAT. Because neither identity is the human reviewer,
|
||||
# the "approval from someone other than the last pusher" branch protection
|
||||
# rule on main is satisfied by a maintainer's own approval.
|
||||
name: Merge upstream eclipse-sailing-analytics/sailing-analytics main into eclipse-main
|
||||
on:
|
||||
schedule:
|
||||
- cron: "37 5 * * *" # daily at 05:37 UTC
|
||||
workflow_dispatch: {}
|
||||
jobs:
|
||||
merge-upstream-main:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout eclipse-main
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: eclipse-main
|
||||
fetch-depth: 0 # full history so the merge has a real base
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
- name: Fetch upstream
|
||||
run: |
|
||||
git remote add upstream https://github.com/eclipse-sailing-analytics/sailing-analytics.git
|
||||
git fetch upstream main
|
||||
- name: Merge upstream/main into eclipse-main
|
||||
run: |
|
||||
git merge --no-ff upstream/main -m "Auto-merge upstream eclipse-sailing-analytics/sailing-analytics main into eclipse-main"
|
||||
- name: Push eclipse-main
|
||||
run: git push origin eclipse-main
|
||||
- name: Open PR eclipse-main -> main (as eclipse-sailing-analytics-bot)
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.ECLIPSE_BOT_PAT }}
|
||||
run: |
|
||||
existing=$(gh pr list --repo "$GITHUB_REPOSITORY" --head eclipse-main --base main --state open --json number --jq '.[0].number')
|
||||
if [ -n "$existing" ]; then
|
||||
echo "PR #$existing already open for eclipse-main -> main; skipping."
|
||||
exit 0
|
||||
fi
|
||||
gh pr create \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--base main \
|
||||
--head eclipse-main \
|
||||
--title "Merge upstream eclipse-sailing-analytics/sailing-analytics main into main" \
|
||||
--body "Automated PR opened by the daily sync workflow. The eclipse-main branch has been updated with the latest changes from upstream eclipse-sailing-analytics/sailing-analytics main. Use 'Create a merge commit' when merging (do not squash or rebase) so any '-s ours' merges on eclipse-main are respected."
|
||||
@@ -37,7 +37,6 @@ Based on the ``docker/docker-compose.yml`` definition you should end up with thr
|
||||
Try a request to [``http://127.0.0.1:8888/index.html``](http://127.0.0.1:8888/index.html) or [``http://127.0.0.1:8888/gwt/status``](http://127.0.0.1:8888/gwt/status) to see if things worked. The default login to your local administration console at [``http://127.0.0.1:8888/gwt/AdminConsole.html``](http://127.0.0.1:8888/gwt/AdminConsole.html) uses the user name ``admin`` with password ``admin``.
|
||||
|
||||
To use Java 25, use the ``docker-compose-25.yml`` file instead:
|
||||
|
||||
```
|
||||
wget "https://github.com/SAP/sailing-analytics/raw/refs/heads/main/docker/docker-compose-25.yml"
|
||||
docker-compose -f docker-compose-25.yml up
|
||||
@@ -156,7 +155,6 @@ The build runs integration tests against [geonames.org](https://geonames.org). U
|
||||
### Get the GitHub Actions build to work in your forked repository
|
||||
|
||||
Assign the IDs and secrets from the prerequisites to repository secrets in your forked repository as follows:
|
||||
|
||||
```
|
||||
AWS_S3_TEST_S3ACCESSID: {your-S3-test-bucket-upload-token-ID}
|
||||
AWS_S3_TEST_S3ACCESSKEY: {key-for-your-S3-token}
|
||||
|
||||
@@ -5,10 +5,7 @@ if [ $# -eq 0 ]; then
|
||||
echo
|
||||
echo "Constructs a Hudson job for the given bugid"
|
||||
echo "Example: $0 4221 [ {Bugzilla-API-Key} ]"
|
||||
echo "Builds a Hudson job for bug branch bug4221, and linking to the Bugzilla bug."
|
||||
echo "If a Bugzilla API Key is provided (may also be specified in the BUGZILLA_API_KEY environment"
|
||||
echo "variable), it is used to add the bug summary to the build job's description."
|
||||
echo "Get a Bugzilla API Key for your user account at https://bugzilla.sapsailing.com/bugzilla/userprefs.cgi?tab=apikey"
|
||||
echo "Builds a Hudson job for bug branch bug4221, and linking to the Github Issue."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
@@ -17,21 +14,15 @@ BUG_ID="$1"
|
||||
CONFIGFILE=$(mktemp mylocalconfigXXXX.xml)
|
||||
RESPONSE_HEADERS=$(mktemp responseheadersXXXX)
|
||||
HUDSON_BASE_URL=https://hudson.sapsailing.com
|
||||
GITHUB_ISSUES_BASE="https://github.com/eclipse-sailing-analytics/sailing-analytics/issues/"
|
||||
BUGZILLA_BASE=https://bugzilla.sapsailing.com/bugzilla
|
||||
COPY_TEMPLATE_JOB=CopyTemplate
|
||||
OS_FOR_GSED="darwin"
|
||||
if [ -n "$2" ]; then
|
||||
BUGZILLA_API_KEY="$2"
|
||||
fi
|
||||
if [ -n "${BUGZILLA_API_KEY}" ]; then
|
||||
echo "Trying to obtain bug summary/title from Bugzilla..."
|
||||
BUG_SUMMARY="$( curl -s -H 'Content-Type: application/json' -H 'Accept: application/json' ${BUGZILLA_BASE}'/rest/bug/'${BUG_ID}'?Bugzilla_api_key='${BUGZILLA_API_KEY}'&include_fields=summary' | jq -r '.bugs[0].summary' )"
|
||||
echo "Found: ${BUG_SUMMARY}"
|
||||
else
|
||||
BUG_SUMMARY=""
|
||||
fi
|
||||
read -p "Username: " USERNAME
|
||||
read -s -p "Password: " PASSWORD
|
||||
echo "Trying to obtain bug summary/title from Github..."
|
||||
BUG_SUMMARY="$( curl -s -H 'Accept: application/vnd.github+json' https://api.github.com/repos/eclipse-sailing-analytics/sailing-analytics/issues/${BUG_ID} | jq -r '.title' )"
|
||||
echo "Found: ${BUG_SUMMARY}"
|
||||
read -p "Hudson Username: " USERNAME
|
||||
read -s -p "Hudson Password: " PASSWORD
|
||||
echo
|
||||
COPY_TEMPLATE_CONFIG_URL="$HUDSON_BASE_URL/job/$COPY_TEMPLATE_JOB/config.xml"
|
||||
curl -s -X GET $COPY_TEMPLATE_CONFIG_URL -u "$USERNAME:$PASSWORD" -o "$CONFIGFILE"
|
||||
@@ -39,9 +30,9 @@ curl -s -X GET $COPY_TEMPLATE_CONFIG_URL -u "$USERNAME:$PASSWORD" -o "$CONFIGFIL
|
||||
# On macosx is gnu-sed needed
|
||||
if [[ "$OSTYPE" == *"$OS_FOR_GSED"* ]]; then
|
||||
echo "Using gsed"
|
||||
gsed -i'' -e 's|<description>..*</description>|<description>This is the CI job for \<a href=\"'$BUGZILLA_BASE'/show_bug.cgi?id='$BUG_ID'\"\>Bug '$BUG_ID'\</a\> ('"${BUG_SUMMARY}"'). See its latest \<a href=\"/userContent/measurements.html?job=bug'$BUG_ID'\"\>quality and performance measurements here.\</a\></description>|' -e 's|<disabled>true</disabled>|<disabled>false</disabled>|' "$CONFIGFILE"
|
||||
gsed -i'' -e 's|<description>..*</description>|<description>This is the CI job for \<a href=\"'${GITHUB_ISSUES_BASE}${BUG_ID}'\"\>Bug '$BUG_ID'\</a\> ('"${BUG_SUMMARY}"'). See its latest \<a href=\"/userContent/measurements.html?job=bug'$BUG_ID'\"\>quality and performance measurements here.\</a\></description>|' -e 's|<disabled>true</disabled>|<disabled>false</disabled>|' "$CONFIGFILE"
|
||||
else
|
||||
sed -i -e 's|<description>..*</description>|<description>This is the CI job for \<a href=\"'$BUGZILLA_BASE'/show_bug.cgi?id='$BUG_ID'\"\>Bug '$BUG_ID'\</a\> ('"${BUG_SUMMARY}"'). See its latest \<a href=\"/userContent/measurements.html?job=bug'$BUG_ID'\"\>quality and performance measurements here.\</a\></description>|' -e 's|<disabled>true</disabled>|<disabled>false</disabled>|' "$CONFIGFILE"
|
||||
sed -i -e 's|<description>..*</description>|<description>This is the CI job for \<a href=\"'${GITHUB_ISSUES_BASE}${BUG_ID}'\"\>Bug '$BUG_ID'\</a\> ('"${BUG_SUMMARY}"'). See its latest \<a href=\"/userContent/measurements.html?job=bug'$BUG_ID'\"\>quality and performance measurements here.\</a\></description>|' -e 's|<disabled>true</disabled>|<disabled>false</disabled>|' "$CONFIGFILE"
|
||||
fi
|
||||
|
||||
# On macosx is gnu-sed needed
|
||||
|
||||
+1
-1
@@ -9,5 +9,5 @@ RequiredBy=multi-user.target
|
||||
[Service]
|
||||
Type=exec
|
||||
RemainAfterExit=true
|
||||
ExecStart=su - wiki -c "cd /home/wiki && rackup -p 4567 /home/wiki/config.ru"
|
||||
ExecStart=su - wiki -c "cd /home/wiki && ./serve.sh"
|
||||
ExecStop=/bin/kill -SIGTERM "$MAINPID"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
ruby "3.2.8"
|
||||
|
||||
gem "gollum", "5.3.2"
|
||||
|
||||
gem "sinatra", "2.2.4"
|
||||
gem "sinatra-contrib", "2.2.4"
|
||||
|
||||
gem "rack", "2.2.21"
|
||||
gem "rack-protection", "2.2.4"
|
||||
gem "rack-session", "~> 1.0"
|
||||
|
||||
gem "webrick", "1.9.1"
|
||||
|
||||
gem "redis", "5.4.1"
|
||||
gem "redis-store", "1.11.0"
|
||||
gem "redis-rack", "2.1.2"
|
||||
|
||||
gem "rugged", "1.9.0"
|
||||
|
||||
gem "github-markup", "4.0.2"
|
||||
gem "kramdown", "2.5.1"
|
||||
gem "kramdown-parser-gfm", "1.1.0"
|
||||
|
||||
gem "mustache", "1.1.1"
|
||||
gem "mustache-sinatra", "2.0.0"
|
||||
|
||||
gem "rest-client", "2.1.0"
|
||||
gem 'rufo'
|
||||
@@ -0,0 +1,24 @@
|
||||
# Internal Wiki
|
||||
|
||||
We use a Ruby based tool called Gollum for our wiki (one wiki to rule them all 🙄).
|
||||
It provides a nice UI, indexing and versions all changes in Git.
|
||||
|
||||
We have added some authentication and access control as a sort of middleware.
|
||||
This is done via the GitHub OAuth process and some APIs.
|
||||
|
||||
## The Process
|
||||
Users navigate to `/login` (or are automatically redirected here if they are logged out and they try to access a path for modifying a resource) and are redirected to GitHub, starting the OAuth process. They have to authenticate with their chosen GitHub credentials and are then redirected back to the wiki.
|
||||
If a user tries to read a resource or its history then they can access anything, but they must have push access to the Sailing Analytics repository to modify a resource (e.g. rename, create, modify).
|
||||
|
||||
## Installation
|
||||
Run `bundle install` in the wiki home directory as the user who will start the wiki and make sure the Gemfile is present.
|
||||
|
||||
## Files
|
||||
* The `templates` dir is necessary for adding the logout button. A custom dir can be specified by modifying the `config.ru`.
|
||||
* The `app.rb` handles the auth and provides an allow list for user paths
|
||||
* `config.ru` handles configuration and setup as you might guess.
|
||||
* `Gemfile` all packages and versions.
|
||||
* `server.sh` launches the wiki via Bundle.
|
||||
|
||||
## Running
|
||||
First ensure the secrets specified at the start of app.rb (`ENV[<name>]`) are exported in a secrets file in the same directory. Then, as the user who owns the git repository that backs the Gollum wiki, run the `serve.sh` script.
|
||||
@@ -1,99 +1,223 @@
|
||||
require 'gollum/app'
|
||||
require 'digest/sha1'
|
||||
require "gollum/app"
|
||||
require "digest/sha1"
|
||||
require "logger"
|
||||
require "rest-client"
|
||||
require "rack/session/redis"
|
||||
require "base64"
|
||||
|
||||
|
||||
#__DIR__ = File.expand_path(File.dirname(__FILE__))
|
||||
#$: << __DIR__
|
||||
class App < Precious::App
|
||||
User = Struct.new(:name, :email, :password_hash, :can_write)
|
||||
before { authenticate! }
|
||||
before /edit/ do authorize_write! ; end
|
||||
before do
|
||||
session['gollum.author'] = {
|
||||
:name => "%s" % settings.loggedInUser,
|
||||
:email => "%s" % settings.loggedInUserEmail,
|
||||
}
|
||||
use Rack::Session::Pool, expire_after: 3600, httponly: true #, secure: true
|
||||
|
||||
# use Rack::Session::Redis,
|
||||
# :redis_server => "redis://127.0.0.1:6379/0",
|
||||
# :expires_in => 3600
|
||||
|
||||
LOGGER = Logger.new("/home/wiki/wiki_log.txt")
|
||||
CLIENT_ID = ENV["CLIENT_ID"]
|
||||
CLIENT_SECRET = ENV["CLIENT_SECRET"]
|
||||
REDIRECT_URL = "https://wiki.sapsailing.com/callback"
|
||||
ACCESS_TOKEN = ENV["ACCESS_TOKEN"]
|
||||
|
||||
REPO_OWNER = "SAP"
|
||||
REPO_NAME = "sailing-analytics"
|
||||
before { check! }
|
||||
before "/gollum/(edit|create|rename|delete)/*" do authorize_write end
|
||||
before do
|
||||
if session[:email] && session[:name]
|
||||
session["gollum.author"] = {
|
||||
:name => session[:name],
|
||||
:email => session[:email],
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
get "/logout" do
|
||||
if session[:access_token]
|
||||
revoke_access_token(token)
|
||||
end
|
||||
session.clear()
|
||||
"logged out"
|
||||
end
|
||||
|
||||
get "/login" do
|
||||
session[:oauth_state] = {
|
||||
:state => SecureRandom.hex(16),
|
||||
:expiry => Time.now + (60 * 5),
|
||||
}
|
||||
params = {
|
||||
client_id: CLIENT_ID,
|
||||
scope: "user:email",
|
||||
state: session[:oauth_state][:state],
|
||||
redirect_uri: REDIRECT_URL,
|
||||
}
|
||||
uri = URI::HTTPS.build(
|
||||
host: "github.com",
|
||||
path: "/login/oauth/authorize",
|
||||
query: URI.encode_www_form(params),
|
||||
)
|
||||
redirect uri.to_s()
|
||||
end
|
||||
|
||||
get "/callback" do
|
||||
if session[:logged_in]
|
||||
# Ensures cancel works in editor after login.
|
||||
if session[:prev]
|
||||
LOGGER.debug(session[:prev])
|
||||
prev = session[:prev].dup
|
||||
stripped_prev = prev.sub("/gollum/edit", "")
|
||||
redirect stripped_prev
|
||||
end
|
||||
end
|
||||
halt 400, "error" if params[:error]
|
||||
halt 400, "Missing code" unless params[:code]
|
||||
halt 403, "Old state" unless session[:oauth_state] && session[:oauth_state][:expiry] > Time.now
|
||||
halt 403, "Invalid OAuth state" unless session[:oauth_state] && session[:oauth_state][:state] == params[:state]
|
||||
|
||||
session.delete(:oauth_state)
|
||||
|
||||
result = JSON.parse(RestClient.post("https://github.com/login/oauth/access_token",
|
||||
{
|
||||
:client_id => CLIENT_ID,
|
||||
:client_secret => CLIENT_SECRET,
|
||||
:code => params[:code],
|
||||
:redirect_uri => REDIRECT_URL,
|
||||
},
|
||||
:accept => :json))
|
||||
scopes = result["scope"].split(",")
|
||||
if scopes.include?("user:email") && result["access_token"]
|
||||
access_token = result["access_token"]
|
||||
fetch_and_set_user_email(access_token)
|
||||
fetch_and_set_user_name_and_id(access_token)
|
||||
session[:logged_in] = true
|
||||
revoke_access_token(access_token)
|
||||
session.delete(:access_token)
|
||||
end
|
||||
|
||||
if session[:prev]
|
||||
redirect session[:prev].sub(/\/gollum\/(rename|delete)/, "")
|
||||
end
|
||||
|
||||
redirect "/"
|
||||
end
|
||||
|
||||
helpers do
|
||||
def authenticate!
|
||||
public_urls=IO.readlines 'public.txt'
|
||||
public_urls.each {|url|
|
||||
if self.env['PATH_INFO'] == url.slice(0, url.length-1)
|
||||
puts "Allowing " + url
|
||||
return
|
||||
end
|
||||
def public_path?(path)
|
||||
if path == "/"
|
||||
return true
|
||||
end
|
||||
public_starts = ["/Home", "/wiki", "/favicon.ico"]
|
||||
public_starts.any? { |link| path.start_with?(link) }
|
||||
end
|
||||
|
||||
if self.env['PATH_INFO'].start_with?('/wiki/images') ||
|
||||
self.env['PATH_INFO'].start_with?('/favicon.ico')
|
||||
return
|
||||
end
|
||||
}
|
||||
if self.env['PATH_INFO'].split('/')[1] == 'gollum' && self.env['PATH_INFO'].split('/')[2] == 'assets'
|
||||
def asset_path?(path)
|
||||
non_page_patterns = [%r{\A/gollum/(assets|commit|history|last_commit_info).*}, %r{\A/gollum/search}, %r{\A/gollum/latest_changes\z}]
|
||||
non_page_patterns.any? { |pattern| pattern.match(path) }
|
||||
end
|
||||
|
||||
def auth_path?(path)
|
||||
auth_paths = [%r{\A/gollum/(edit|create|rename|delete)/.*\z}, %r{\A/gollum/(overview|preview)}, %r{\A/gollum/create}]
|
||||
auth_paths.any? { |pattern| pattern.match(path) }
|
||||
end
|
||||
|
||||
def login_path?(path)
|
||||
%r{\A/(login|callback|logout|cancel)\z}.match?(path)
|
||||
end
|
||||
|
||||
def check!
|
||||
path = env["PATH_INFO"].dup
|
||||
LOGGER.debug(path)
|
||||
return if login_path?(path)
|
||||
return if asset_path?(path)
|
||||
isPublicPath = public_path?(path)
|
||||
isAuthPath = auth_path?(path)
|
||||
if isPublicPath || isAuthPath
|
||||
session[:prev] = path
|
||||
return
|
||||
end
|
||||
@_auth = Rack::Auth::Basic::Request.new(request.env)
|
||||
if self.env['PATH_INFO'].split('/').length >=2 &&
|
||||
(self.env['PATH_INFO'].split('/')[1] != 'wiki' &&
|
||||
self.env['PATH_INFO'].split('/')[2] != 'wiki' &&
|
||||
self.env['PATH_INFO'].split('/')[1] != 'home' &&
|
||||
self.env['PATH_INFO'].split('/')[2] != 'home' &&
|
||||
self.env['PATH_INFO'].split('/')[1] != 'Home' &&
|
||||
self.env['PATH_INFO'].split('/')[2] != 'Home' &&
|
||||
self.env['PATH_INFO'].split('/')[1] != 'Home.md' &&
|
||||
self.env['PATH_INFO'].split('/')[1] != 'search' &&
|
||||
self.env['PATH_INFO'].split('/')[2] != 'search' &&
|
||||
self.env['PATH_INFO'].split('/')[1] != 'edit' &&
|
||||
self.env['PATH_INFO'].split('/')[2] != 'edit' &&
|
||||
self.env['PATH_INFO'].split('/')[1] != 'preview' &&
|
||||
self.env['PATH_INFO'].split('/')[2] != 'preview' &&
|
||||
(self.env['PATH_INFO'].split('/')[1] != 'gollum' ||
|
||||
(self.env['PATH_INFO'].split('/')[2] != 'create' &&
|
||||
(self.env['PATH_INFO'].split('/')[2] != 'overview' || self.env['PATH_INFO'].split('/')[3] != 'wiki'))))
|
||||
throw(:halt, [403, 'Forbidden - You can not access anything outside wiki/ path.'])
|
||||
halt 404, "You cannot access anything outside wiki/ path."
|
||||
end
|
||||
|
||||
def authorize_write
|
||||
LOGGER.debug("Checking auth before writing")
|
||||
|
||||
if !session[:logged_in]
|
||||
if env["PATH_INFO"].dup.match(%r{/gollum/delete/.*})
|
||||
halt 401, "Unauthorized"
|
||||
end
|
||||
redirect "/login"
|
||||
end
|
||||
if @_auth.provided?
|
||||
end
|
||||
if @_auth.provided? && @_auth.basic? && @_auth.credentials && @user = detected_user(@_auth.credentials)
|
||||
Precious::App.set(:loggedInUser, @user.name)
|
||||
Precious::App.set(:loggedInUserEmail, @user.email)
|
||||
return @user
|
||||
else
|
||||
response['WWW-Authenticate'] = %(Basic realm="Gollum Wiki")
|
||||
throw(:halt, [401, "Not authorized\n"])
|
||||
halt 403, "Forbidden" unless user_can_write()
|
||||
end
|
||||
|
||||
def user_can_write()
|
||||
return false unless session[:logged_in] && session[:name]
|
||||
response = github_api_get("/repos/#{REPO_OWNER}/#{REPO_NAME}/collaborators/#{session[:name]}/permission",
|
||||
ACCESS_TOKEN)
|
||||
return false unless response
|
||||
LOGGER.debug("response received")
|
||||
result = JSON.parse(response)
|
||||
LOGGER.debug("checking permission")
|
||||
result.dig("user", "permissions", "push") == true
|
||||
end
|
||||
|
||||
def fetch_and_set_user_email(access_token)
|
||||
response = github_api_get("/user/emails", access_token)
|
||||
return unless response
|
||||
emails = JSON.parse(response)
|
||||
if emails[0] && emails[0]["email"]
|
||||
session[:email] = emails[0]["email"]
|
||||
LOGGER.debug("email is #{session[:email]}")
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_write!
|
||||
throw(:halt, [403, "Forbidden\n"]) unless @user.can_write
|
||||
end
|
||||
|
||||
def users
|
||||
@_users ||= settings.authorized_users.map {|u| User.new(*u) }
|
||||
end
|
||||
|
||||
def detected_user(credentials)
|
||||
users.detect do |u|
|
||||
[u.email, u.password_hash] ==
|
||||
[credentials[0], Digest::SHA1.hexdigest(credentials[1])]
|
||||
def fetch_and_set_user_name_and_id(access_token)
|
||||
response = github_api_get("/user", access_token)
|
||||
return unless response
|
||||
user_details = JSON.parse(response)
|
||||
if user_details["login"] && user_details["id"]
|
||||
session[:user_id] = user_details["id"]
|
||||
session[:name] = user_details["login"]
|
||||
LOGGER.debug("user #{session[:email]} logged in")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def commit_
|
||||
{
|
||||
:message => params[:message],
|
||||
# :name => @user.name,
|
||||
:email => @user.email
|
||||
}
|
||||
def github_api_get(path, access_token)
|
||||
uri = URI::HTTPS.build(
|
||||
host: "api.github.com",
|
||||
path: path,
|
||||
)
|
||||
RestClient.get(uri.to_s(),
|
||||
{
|
||||
:Authorization => "Bearer #{access_token}",
|
||||
})
|
||||
rescue RestClient::Unauthorized, RestClient::Forbidden
|
||||
LOGGER.warn("GitHub auth failed for #{path}")
|
||||
nil
|
||||
rescue RestClient::ExceptionWithResponse => e
|
||||
LOGGER.error("GitHub API error #{e.response.code} for #{path}")
|
||||
nil
|
||||
rescue StandardError => e
|
||||
LOGGER.error("GitHub request failed: #{e.message}")
|
||||
nil
|
||||
end
|
||||
|
||||
def revoke_access_token(token)
|
||||
uri = URI::HTTPS.build(
|
||||
host: "api.github.com",
|
||||
path: "/applications/#{CLIENT_ID}/token",
|
||||
)
|
||||
basicAuth = Base64.strict_encode64("#{CLIENT_ID}:#{CLIENT_SECRET}")
|
||||
RestClient::Request.execute(
|
||||
method: :delete,
|
||||
url: uri.to_s(),
|
||||
payload: {
|
||||
:access_token => token,
|
||||
}.to_json(),
|
||||
headers: {
|
||||
:Authorization => "Basic #{basicAuth}",
|
||||
:accept => "application/vnd.github.v3+json",
|
||||
},
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
##set author
|
||||
#class Precious::App
|
||||
# before do
|
||||
# session['gollum.author'] = {
|
||||
# :name => "%s" % settings.loggedInUser,
|
||||
# :email => "%s@example.com" % settings.loggedInUser,
|
||||
# }
|
||||
# end
|
||||
#End
|
||||
|
||||
@@ -28,11 +28,6 @@
|
||||
#use Gollum::Auth, users, options
|
||||
#
|
||||
## That's it. The rest is for gollum only.
|
||||
#gollum_path = "/home/wiki/gitwiki"
|
||||
#wiki_options = {:universal_toc => false}
|
||||
#Precious::App.set(:gollum_path, gollum_path)
|
||||
#Precious::App.set(:wiki_options, wiki_options)
|
||||
#run Precious::App
|
||||
|
||||
require 'rubygems'
|
||||
require 'gollum/app'
|
||||
@@ -48,7 +43,7 @@ Gollum::Page.send :remove_const, :FORMAT_NAMES if defined? Gollum::Page::FORMAT_
|
||||
#}
|
||||
|
||||
gollum_path = "/home/wiki/gitwiki"
|
||||
wiki_options = {universal_toc: false, ref: 'main'}
|
||||
wiki_options = {universal_toc: false, ref: 'main', template_dir: "./templates"}
|
||||
Precious::App.set(:gollum_path, gollum_path)
|
||||
Precious::App.set(:wiki_options, wiki_options)
|
||||
Precious::App.set(:authorized_users, YAML.load_file(File.expand_path('users.yml', File.expand_path(File.dirname(__FILE__)))))
|
||||
@@ -57,12 +52,3 @@ Precious::App.set(:loggedInUserEmail, "wiki@sapsailing.com");
|
||||
App.set(:default_markup, :markdown) # set your favorite markup language
|
||||
run App
|
||||
|
||||
#require 'rubygems'
|
||||
#require 'gollum/app'
|
||||
#
|
||||
#gollum_path = "/home/wiki/gitwiki"
|
||||
#wiki_options = {:universal_toc => false}
|
||||
#Precious::App.set(:gollum_path, gollum_path)
|
||||
#Precious::App.set(:default_markup, :markdown) # set your favorite markup language
|
||||
#Precious::App.set(:wiki_options, wiki_options)
|
||||
#run Precious::App
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
# kill any running gollums
|
||||
kill -9 `ps axlw | grep rackup | grep wiki | awk '{ print $3; }'`
|
||||
rm /home/wiki/wiki_log.txt
|
||||
# start gollum as a background process
|
||||
# you can pipe output to /dev/null instead, if you don't want a log
|
||||
cd /home/wiki
|
||||
. secrets
|
||||
#nohup rackup -p 4567 /home/wiki/config.ru 1>nohup.out 2>&1 &
|
||||
nohup bundle exec rackup -p 4567 /home/wiki/config.ru &
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
<div id="wiki-content" class="px-2 px-lg-0">
|
||||
<h1 class="header-title text-center text-md-left pt-4">
|
||||
{{page_header}}
|
||||
</h1>
|
||||
<div class="breadcrumb">{{{breadcrumb}}}</div>
|
||||
|
||||
<div class="{{#has_header}}has-header{{/has_header}}{{#has_footer}} has-footer{{/has_footer}}{{#has_sidebar}} has-sidebar has-{{bar_side}}bar{{/has_sidebar}}{{#has_toc}} has-toc{{/has_toc}}">
|
||||
{{#has_toc}}
|
||||
<div id="wiki-toc-main">
|
||||
{{{toc_content}}}
|
||||
</div>
|
||||
{{/has_toc}}
|
||||
<div id="wiki-body" class="gollum-{{format}}-content">
|
||||
{{#has_header}}
|
||||
<div id="wiki-header" class="gollum-{{header_format}}-content">
|
||||
<div id="header-content" class="markdown-body">
|
||||
{{{header_content}}}
|
||||
</div>
|
||||
</div>
|
||||
{{/has_header}}
|
||||
<div class="main-content clearfix container-lg">
|
||||
<div class="markdown-body {{#header_enum?}}header-enum{{/header_enum?}} {{#has_sidebar}}float-md-{{body_side}} col-md-9{{/has_sidebar}}" {{#header_enum?}}style="--header-enum-style:{{header_enum_style}};"{{/header_enum?}}>
|
||||
{{{rendered_metadata}}}
|
||||
{{{content}}}
|
||||
</div>
|
||||
{{#has_sidebar}}
|
||||
<div id="wiki-sidebar" class="Box Box--condensed float-md-{{body_side}} col-md-3">
|
||||
<div id="sidebar-content" class="gollum-{{sidebar_format}}-content markdown-body px-4">
|
||||
{{{sidebar_content}}}
|
||||
</div>
|
||||
</div>
|
||||
{{/has_sidebar}}
|
||||
</div>
|
||||
</div>
|
||||
{{#has_footer}}
|
||||
<div id="wiki-footer" class="gollum-{{footer_format}}-content my-2">
|
||||
<div id="footer-content" class="Box Box-condensed markdown-body px-4">
|
||||
{{{footer_content}}}
|
||||
</div>
|
||||
</div>
|
||||
{{/has_footer}}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="footer" class="pt-4">
|
||||
{{^historical}}
|
||||
{{^preview}}
|
||||
<a href="/logout">logout</a>
|
||||
<p id="last-edit"><div class="dotted-spinner hidden"></div> <a id="page-info-toggle" data-pagepath="{{escaped_url_path}}">When was this page last modified?</a></p>
|
||||
{{#allow_editing}}
|
||||
<p>
|
||||
<a id="delete-link" href="{{escaped_url_path}}" data-confirm="Are you sure you want to delete this page?"><span>Delete this Page</span></a>
|
||||
</p>
|
||||
{{/allow_editing}}
|
||||
{{/preview}}
|
||||
{{/historical}}
|
||||
{{#historical}}
|
||||
<p>This version of the page was edited by <b>{{author}}</b> at <time datetime="{{datetime}}" data-format="{{date_format}}">{{date}}</time>. <a href="{{full_url_path}}">View the most recent version.</a></p>
|
||||
{{/historical}}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
echo "Content-Type: text/html"
|
||||
echo ""
|
||||
# Read secrets:
|
||||
. ~/secrets
|
||||
# ==== CONFIG for OAuth App wiki.sapsailing.com: ====
|
||||
CLIENT_ID="${GITHUB_OAUTH_CLIENT_ID}"
|
||||
CLIENT_SECRET="${GITHUB_OAUTH_CLIENT_SECRET}"
|
||||
REDIRECT_URI="https://git.sapsailing.com/cgi-bin/github_oauth.sh"
|
||||
# ===================================================
|
||||
STATES_FILE=/tmp/github_oauth_states
|
||||
# Parse QUERY_STRING for code
|
||||
QUERY="$QUERY_STRING"
|
||||
CODE=""
|
||||
STATE_PARAM=""
|
||||
IFS='&' read -ra KV <<< "$QUERY"
|
||||
for pair in "${KV[@]}"; do
|
||||
key="${pair%%=*}"
|
||||
val="${pair#*=}"
|
||||
if [ "$key" = "code" ]; then
|
||||
CODE="$val"
|
||||
elif [ "$key" = "state" ]; then
|
||||
STATE_PARAM="$val"
|
||||
fi
|
||||
done
|
||||
# Checking for code parameter; if not found, start the flow
|
||||
if [ -z "$CODE" ]; then
|
||||
# No code → show login page
|
||||
STATE=$(openssl rand -hex 16)
|
||||
echo "${STATE}" >>"${STATES_FILE}"
|
||||
AUTH_URL="https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=read:user,user:email&state=${STATE}"
|
||||
cat <<EOF
|
||||
<html><head><title>GitHub OAuth Login</title></head>
|
||||
<body>
|
||||
<h2>Login with GitHub</h2>
|
||||
<p><a href="${AUTH_URL}">Authorize this app to access your GitHub account</a></p>
|
||||
</body></html>
|
||||
EOF
|
||||
exit 0
|
||||
else
|
||||
if grep -q "${STATE_PARAM}" "${STATES_FILE}"; then
|
||||
sed -i '/'${STATE_PARAM}'/d' "${STATES_FILE}"
|
||||
# Got a code → exchange it for an access token
|
||||
TOKEN_JSON=$(curl -s -X POST https://github.com/login/oauth/access_token \
|
||||
-H "Accept: application/json" \
|
||||
-d "client_id=${CLIENT_ID}" \
|
||||
-d "client_secret=${CLIENT_SECRET}" \
|
||||
-d "code=${CODE}" \
|
||||
-d "redirect_uri=${REDIRECT_URI}" )
|
||||
ACCESS_TOKEN=$(echo "$TOKEN_JSON" | jq -r '.access_token')
|
||||
if [ "$ACCESS_TOKEN" = "null" ] || [ -z "$ACCESS_TOKEN" ]; then
|
||||
cat <<EOF
|
||||
<html><body>
|
||||
<h3>Failed to obtain access token.</h3>
|
||||
<pre>${TOKEN_JSON}</pre>
|
||||
</body></html>
|
||||
EOF
|
||||
exit 1
|
||||
else
|
||||
# Use token to get user info
|
||||
USER_JSON=$(curl -s -H "Authorization: token ${ACCESS_TOKEN}" -H "Accept: application/vnd.github+json" https://api.github.com/user)
|
||||
LOGIN=$(echo "$USER_JSON" | jq -r '.login')
|
||||
cat <<EOF
|
||||
<html><head><title>GitHub OAuth Success</title></head>
|
||||
<body>
|
||||
<h2>Welcome, ${LOGIN}!</h2>
|
||||
<p>The state param ${STATE_PARAM} was found in our ${STATES_FILE}:</p>
|
||||
<pre>$(cat "${STATES_FILE}")</pre>
|
||||
<p>You have successfully authenticated with GitHub.</p>
|
||||
<p>Your token JSON was:</p>
|
||||
<pre>${TOKEN_JSON}</pre>
|
||||
<p><strong>Access Token:</strong> ${ACCESS_TOKEN}</p>
|
||||
<p>You can now use this token to make API calls on behalf of this user.</p>
|
||||
<pre>${USER_JSON}</pre>
|
||||
</body></html>
|
||||
EOF
|
||||
fi
|
||||
else
|
||||
# The ${STATE_PARAM} was not found in ${STATES_FILE}, so this may be an attack
|
||||
cat <<EOF
|
||||
<html><body>
|
||||
<h3>Failed to find state; attack?</h3>
|
||||
<pre>${STATE_PARAM}</pre>
|
||||
</body></html>
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
+2
-1
@@ -35,7 +35,7 @@ terminationCheck() {
|
||||
}
|
||||
if [[ "$#" -ne 4 ]]; then
|
||||
echo "4 arguments required. Please check comment description for further details."
|
||||
echo "Example usage: setup-central-reverse-proxy.sh 1.2.3.4 0OcJ1938QE5it875kjlQe7HnzQ6740jsnMEVzowjZrs= 18.170.25.225 /home/sailing/code"
|
||||
echo "Example usage: setup-central-reverse-proxy.sh 1.2.3.4 0OcJ1938QE5it875kjlQe7HnzQ6740jsnMEVzowjZrs= 54.229.94.254 /home/wiki/gitwiki"
|
||||
exit 2
|
||||
fi
|
||||
echo "Make sure the instance is in the same AZ as the existing reverse proxy, so the volumes can be switched over."
|
||||
@@ -65,6 +65,7 @@ yum group install -y "Development Tools"
|
||||
yum install -y ruby ruby-devel libicu libicu-devel zlib zlib-devel git cmake openssl-devel libyaml-devel
|
||||
gem install gollum -v 5.3.2
|
||||
gem update --system 3.5.7
|
||||
su - wiki -c "cd /home/wiki && bundle install"
|
||||
cd /home
|
||||
# copy bugzilla
|
||||
scp -o StrictHostKeyChecking=no root@sapsailing.com:/var/www/static/bugzilla-5.2.tar.gz /usr/local/src
|
||||
|
||||
-2
@@ -1,11 +1,9 @@
|
||||
package com.sap.sailing.declination.test;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
|
||||
import com.sap.sailing.declination.impl.NOAAImporterForTesting;
|
||||
|
||||
@Disabled("Disabled because NOAA server seems down; 2026-05-19")
|
||||
public class NOAADeclinationImportTest extends DeclinationImportTest<NOAAImporterForTesting> {
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
|
||||
-2
@@ -1,11 +1,9 @@
|
||||
package com.sap.sailing.declination.test;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
|
||||
import com.sap.sailing.declination.impl.NOAAImporter;
|
||||
|
||||
@Disabled("Disabled because NOAA server seems down; 2026-05-19")
|
||||
public class NOAASimpleDeclinationTest extends SimpleDeclinationTest<NOAAImporter> {
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
|
||||
+4
-4
@@ -66,11 +66,11 @@ public class BoatTableWrapper<S extends RefreshableSelectionModel<BoatDTO>> exte
|
||||
new EntityIdentityComparator<BoatDTO>() {
|
||||
@Override
|
||||
public boolean representSameEntity(BoatDTO dto1, BoatDTO dto2) {
|
||||
return dto1.getIdAsString().equals(dto2.getIdAsString());
|
||||
return Util.equalsWithNull(dto1.getIdAsString(), dto2.getIdAsString());
|
||||
}
|
||||
@Override
|
||||
public int hashCode(BoatDTO t) {
|
||||
return t.getIdAsString().hashCode();
|
||||
return t.getIdAsString() != null ? t.getIdAsString().hashCode() : 0;
|
||||
}
|
||||
});
|
||||
this.boatsRefresher = boatsRefresher;
|
||||
@@ -286,11 +286,11 @@ public class BoatTableWrapper<S extends RefreshableSelectionModel<BoatDTO>> exte
|
||||
boatsRefresher.addIfNotContainedElseReplace(updatedBoat, new EntityIdentityComparator<BoatDTO>() {
|
||||
@Override
|
||||
public boolean representSameEntity(BoatDTO dto1, BoatDTO dto2) {
|
||||
return dto1.getIdAsString().equals(dto2.getIdAsString());
|
||||
return Util.equalsWithNull(dto1.getIdAsString(), dto2.getIdAsString());
|
||||
}
|
||||
@Override
|
||||
public int hashCode(BoatDTO t) {
|
||||
return t.getIdAsString().hashCode();
|
||||
return t.getIdAsString() != null ? t.getIdAsString().hashCode() : 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+8
-8
@@ -93,11 +93,11 @@ public class CompetitorTableWrapper<S extends RefreshableSelectionModel<Competit
|
||||
new EntityIdentityComparator<CompetitorDTO>() {
|
||||
@Override
|
||||
public boolean representSameEntity(CompetitorDTO dto1, CompetitorDTO dto2) {
|
||||
return dto1.getIdAsString().equals(dto2.getIdAsString());
|
||||
return Util.equalsWithNull(dto1.getIdAsString(), dto2.getIdAsString());
|
||||
}
|
||||
@Override
|
||||
public int hashCode(CompetitorDTO t) {
|
||||
return t.getIdAsString().hashCode();
|
||||
return t.getIdAsString() != null ? t.getIdAsString().hashCode() : 0;
|
||||
}
|
||||
});
|
||||
this.competitorsRefresher = competitorsRefresher;
|
||||
@@ -462,22 +462,22 @@ public class CompetitorTableWrapper<S extends RefreshableSelectionModel<Competit
|
||||
competitorsRefresher.addIfNotContainedElseReplace(updatedCompetitor, new EntityIdentityComparator<CompetitorDTO>() {
|
||||
@Override
|
||||
public boolean representSameEntity(CompetitorDTO dto1, CompetitorDTO dto2) {
|
||||
return dto1.getIdAsString().equals(dto2.getIdAsString());
|
||||
return Util.equalsWithNull(dto1.getIdAsString(), dto2.getIdAsString());
|
||||
}
|
||||
@Override
|
||||
public int hashCode(CompetitorDTO t) {
|
||||
return t.getIdAsString().hashCode();
|
||||
return t.getIdAsString() != null ? t.getIdAsString().hashCode() : 0;
|
||||
}
|
||||
});
|
||||
if (boatsRefresher != null) {
|
||||
boatsRefresher.addIfNotContainedElseReplace(updatedCompetitor.getBoat(), new EntityIdentityComparator<BoatDTO>() {
|
||||
@Override
|
||||
public boolean representSameEntity(BoatDTO dto1, BoatDTO dto2) {
|
||||
return dto1.getIdAsString().equals(dto2.getIdAsString());
|
||||
return Util.equalsWithNull(dto1.getIdAsString(), dto2.getIdAsString());
|
||||
}
|
||||
@Override
|
||||
public int hashCode(BoatDTO t) {
|
||||
return t.getIdAsString().hashCode();
|
||||
return t.getIdAsString() != null ? t.getIdAsString().hashCode() : 0;
|
||||
}
|
||||
});
|
||||
boatsRefresher.callAllFill();
|
||||
@@ -542,11 +542,11 @@ public class CompetitorTableWrapper<S extends RefreshableSelectionModel<Competit
|
||||
competitorsRefresher.addIfNotContainedElseReplace(updatedCompetitor, new EntityIdentityComparator<CompetitorDTO>() {
|
||||
@Override
|
||||
public boolean representSameEntity(CompetitorDTO dto1, CompetitorDTO dto2) {
|
||||
return dto1.getIdAsString().equals(dto2.getIdAsString());
|
||||
return Util.equalsWithNull(dto1.getIdAsString(), dto2.getIdAsString());
|
||||
}
|
||||
@Override
|
||||
public int hashCode(CompetitorDTO t) {
|
||||
return t.getIdAsString().hashCode();
|
||||
return t.getIdAsString() != null ? t.getIdAsString().hashCode() : 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+4
-16
@@ -14,7 +14,6 @@ import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.google.gwt.cell.client.AbstractCell;
|
||||
@@ -40,7 +39,6 @@ import com.google.gwt.user.client.ui.Composite;
|
||||
import com.google.gwt.user.client.ui.Label;
|
||||
import com.google.gwt.user.client.ui.VerticalPanel;
|
||||
import com.google.gwt.view.client.ListDataProvider;
|
||||
import com.google.gwt.view.client.SelectionChangeEvent;
|
||||
import com.sap.sailing.domain.common.dto.CourseAreaDTO;
|
||||
import com.sap.sailing.gwt.common.client.help.HelpButton;
|
||||
import com.sap.sailing.gwt.common.client.help.HelpButtonResources;
|
||||
@@ -178,21 +176,11 @@ public class EventListComposite extends Composite {
|
||||
final Button create = buttonPanel.addCreateAction(stringMessages.actionAddEvent(), this::openCreateEventDialog);
|
||||
create.ensureDebugId("CreateEventButton");
|
||||
final Button remove = buttonPanel.addRemoveAction(stringMessages.remove(), refreshableEventSelectionModel, true,
|
||||
() -> removeEvents(refreshableEventSelectionModel.getSelectedSet()));
|
||||
remove.ensureDebugId("RemoveEventsButton");
|
||||
this.refreshableEventSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
|
||||
@Override
|
||||
public void onSelectionChange(SelectionChangeEvent event) {
|
||||
final Set<EventDTO> selectedEvents = refreshableEventSelectionModel.getSelectedSet();
|
||||
boolean canDeleteAll = true;
|
||||
for (EventDTO eventDTO : selectedEvents) {
|
||||
if (!userService.hasPermission(eventDTO, DefaultActions.DELETE)) {
|
||||
canDeleteAll = false;
|
||||
}
|
||||
}
|
||||
remove.setEnabled(!selectedEvents.isEmpty() && canDeleteAll);
|
||||
}
|
||||
() -> {
|
||||
final List<EventDTO> selected = new ArrayList<>(refreshableEventSelectionModel.getSelectedSet());
|
||||
removeEvents(selected);
|
||||
});
|
||||
remove.ensureDebugId("RemoveEventsButton");
|
||||
buttonPanel.addUnsecuredWidget(new HelpButton(HelpButtonResources.INSTANCE,
|
||||
stringMessages.videoGuide(), "https://sapsailing-documentation.s3-eu-west-1.amazonaws.com/adminconsole/CreatingYourFirstEvent.mp4"));
|
||||
panel.add(filterTextbox);
|
||||
|
||||
+7
-11
@@ -13,6 +13,7 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.gwt.cell.client.SafeHtmlCell;
|
||||
@@ -183,17 +184,17 @@ public class IgtimiDevicesPanel extends FlowPanel implements FilterablePanelProv
|
||||
devicesControlsPanel.add(busyIndicator);
|
||||
buttonPanel.addUnsecuredAction(stringMessages.refresh(), () -> refreshDevices());
|
||||
// setup controls
|
||||
final Button removeDeviceButton = buttonPanel.addRemoveAction(stringMessages.remove(), refreshableDevicesSelectionModel,
|
||||
buttonPanel.addRemoveAction(stringMessages.remove(), refreshableDevicesSelectionModel,
|
||||
/* with confirmation */ true, () -> {
|
||||
if (refreshableDevicesSelectionModel.getSelectedSet().size() > 0) {
|
||||
if (Window.confirm(stringMessages.doYouReallyWantToRemoveTheSelectedIgtimiDevices())) {
|
||||
for (IgtimiDeviceWithSecurityDTO device : refreshableDevicesSelectionModel.getSelectedSet()) {
|
||||
final List<IgtimiDeviceWithSecurityDTO> selected = new ArrayList<>(refreshableDevicesSelectionModel.getSelectedSet());
|
||||
for (IgtimiDeviceWithSecurityDTO device : selected) {
|
||||
removeDevice(device, filteredDevices);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
removeDeviceButton.setEnabled(false);
|
||||
devicesCaptionPanelContents.add(devicesControlsPanel);
|
||||
devicesCaptionPanelContents.add(devicesTable);
|
||||
add(devicesCaptionPanel);
|
||||
@@ -230,20 +231,19 @@ public class IgtimiDevicesPanel extends FlowPanel implements FilterablePanelProv
|
||||
dawControlsPanel.add(dawButtonPanel);
|
||||
dawButtonPanel.addUnsecuredAction(stringMessages.refresh(), () -> refreshDataAccessWindows());
|
||||
// setup controls
|
||||
final Button removeDAWButton = dawButtonPanel.addRemoveAction(stringMessages.remove(), refreshableDataAccessWindowsSelectionModel,
|
||||
dawButtonPanel.addRemoveAction(stringMessages.remove(), refreshableDataAccessWindowsSelectionModel,
|
||||
/* with confirmation */ true, () -> {
|
||||
if (refreshableDataAccessWindowsSelectionModel.getSelectedSet().size() > 0) {
|
||||
if (Window.confirm(stringMessages.doYouReallyWantToRemoveTheSelectedIgtimiDataAccessWindows())) {
|
||||
for (IgtimiDataAccessWindowWithSecurityDTO daw : refreshableDataAccessWindowsSelectionModel.getSelectedSet()) {
|
||||
final List<IgtimiDataAccessWindowWithSecurityDTO> selected = new ArrayList<>(refreshableDataAccessWindowsSelectionModel.getSelectedSet());
|
||||
for (IgtimiDataAccessWindowWithSecurityDTO daw : selected) {
|
||||
removeDataAccessWindow(daw, filteredDAWs);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
removeDAWButton.setEnabled(false);
|
||||
refreshableDevicesSelectionModel.addSelectionChangeHandler(
|
||||
e -> {
|
||||
removeDeviceButton.setEnabled(refreshableDevicesSelectionModel.getSelectedSet().size() > 0);
|
||||
final boolean exactlyOneDeviceSelected = refreshableDevicesSelectionModel.getSelectedSet().size() == 1;
|
||||
dawTable.setVisible(exactlyOneDeviceSelected);
|
||||
dawControlsPanel.setVisible(exactlyOneDeviceSelected);
|
||||
@@ -251,10 +251,6 @@ public class IgtimiDevicesPanel extends FlowPanel implements FilterablePanelProv
|
||||
filterDataAccessWindowPanel.search(refreshableDevicesSelectionModel.getSelectedSet().iterator().next().getSerialNumber());
|
||||
}
|
||||
});
|
||||
refreshableDataAccessWindowsSelectionModel.addSelectionChangeHandler(
|
||||
e -> {
|
||||
removeDAWButton.setEnabled(refreshableDataAccessWindowsSelectionModel.getSelectedSet().size() > 0);
|
||||
});
|
||||
dataAccessWindowsCaptionPanelContents.add(dawControlsPanel);
|
||||
dataAccessWindowsCaptionPanelContents.add(dawTable);
|
||||
add(dataAccessWindowsCaptionPanel);
|
||||
|
||||
+4
-9
@@ -168,7 +168,10 @@ public class LeaderboardConfigPanel extends AbstractLeaderboardConfigPanel
|
||||
leaderboardCreateAndRegattaReadPermission, this::createRegattaLeaderboardWithOtherTieBreakingLeaderboard);
|
||||
createRegattaLeaderboardWithOtherTieBreakingLeaderboardBtn.ensureDebugId("CreateRegattaLeaderboardWithOtherTieBreakingLeaderboardButton");
|
||||
leaderboardRemoveButton = buttonPanel.addRemoveAction(stringMessages.remove(), leaderboardSelectionModel, true,
|
||||
() -> removeLeaderboards(leaderboardSelectionModel.getSelectedSet()));
|
||||
() -> {
|
||||
final List<StrippedLeaderboardDTO> selectedLeaderboards = new ArrayList<>(leaderboardSelectionModel.getSelectedSet());
|
||||
removeLeaderboards(selectedLeaderboards);
|
||||
});
|
||||
leaderboardRemoveButton.ensureDebugId("LeaderboardsRemoveButton");
|
||||
buttonPanel.addUnsecuredWidget(new HelpButton(HelpButtonResources.INSTANCE,
|
||||
stringMessages.videoGuide(), "https://sapsailing-documentation.s3-eu-west-1.amazonaws.com/adminconsole/Advanced+Topics/Leaderboard+Group+explained.mp4"));
|
||||
@@ -865,14 +868,6 @@ public class LeaderboardConfigPanel extends AbstractLeaderboardConfigPanel
|
||||
|
||||
@Override
|
||||
protected void leaderboardSelectionChanged() {
|
||||
Set<StrippedLeaderboardDTO> selectedLeaderboards = leaderboardSelectionModel.getSelectedSet();
|
||||
boolean canDeleteAllSelected = true;
|
||||
for (StrippedLeaderboardDTO leaderboard : selectedLeaderboards) {
|
||||
if (!userService.hasPermission(leaderboard, DefaultActions.DELETE)) {
|
||||
canDeleteAllSelected = false;
|
||||
}
|
||||
}
|
||||
leaderboardRemoveButton.setEnabled(!selectedLeaderboards.isEmpty() && canDeleteAllSelected);
|
||||
final StrippedLeaderboardDTO selectedLeaderboard = getSelectedLeaderboard();
|
||||
if (leaderboardSelectionModel.getSelectedSet().size() == 1 && selectedLeaderboard != null) {
|
||||
raceColumnTable.getDataProvider().getList().clear();
|
||||
|
||||
+5
-9
@@ -610,7 +610,10 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel
|
||||
groupsTable.addColumnSortHandler(leaderboardGroupsListHandler);
|
||||
refreshableGroupsSelectionModel = leaderboardTableSelectionColumn.getSelectionModel();
|
||||
removeButton = buttonPanel.addRemoveAction(stringMessages.remove(), refreshableGroupsSelectionModel, true,
|
||||
() -> removeLeaderboardGroups(refreshableGroupsSelectionModel.getSelectedSet()));
|
||||
() -> {
|
||||
final List<LeaderboardGroupDTO> selectedGroups = new ArrayList<>(refreshableGroupsSelectionModel.getSelectedSet());
|
||||
removeLeaderboardGroups(selectedGroups);
|
||||
});
|
||||
removeButton.ensureDebugId("RemoveLeaderboardButton");
|
||||
refreshableGroupsSelectionModel.addSelectionChangeHandler(event -> groupSelectionChanged());
|
||||
groupsTable.setSelectionModel(refreshableGroupsSelectionModel, leaderboardTableSelectionColumn.getSelectionManager());
|
||||
@@ -835,15 +838,8 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel
|
||||
}
|
||||
|
||||
private void groupSelectionChanged() {
|
||||
Set<LeaderboardGroupDTO> selectedLeaderboardGroups = refreshableGroupsSelectionModel.getSelectedSet();
|
||||
final Set<LeaderboardGroupDTO> selectedLeaderboardGroups = refreshableGroupsSelectionModel.getSelectedSet();
|
||||
isSingleGroupSelected = selectedLeaderboardGroups.size() == 1;
|
||||
boolean canDeleteAllSelected = true;
|
||||
for (LeaderboardGroupDTO group : selectedLeaderboardGroups) {
|
||||
if (!userService.hasPermission(group, DefaultActions.DELETE)) {
|
||||
canDeleteAllSelected = false;
|
||||
}
|
||||
}
|
||||
removeButton.setEnabled(!selectedLeaderboardGroups.isEmpty() && canDeleteAllSelected);
|
||||
splitPanel.setVisible(isSingleGroupSelected);
|
||||
if (isSingleGroupSelected) {
|
||||
LeaderboardGroupDTO selectedGroup = selectedLeaderboardGroups.iterator().next();
|
||||
|
||||
+4
-24
@@ -32,12 +32,9 @@ import com.google.gwt.user.cellview.client.Header;
|
||||
import com.google.gwt.user.client.Command;
|
||||
import com.google.gwt.user.client.Window;
|
||||
import com.google.gwt.user.client.rpc.AsyncCallback;
|
||||
import com.google.gwt.user.client.ui.Button;
|
||||
import com.google.gwt.user.client.ui.FlowPanel;
|
||||
import com.google.gwt.user.client.ui.Label;
|
||||
import com.google.gwt.view.client.ListDataProvider;
|
||||
import com.google.gwt.view.client.SelectionChangeEvent;
|
||||
import com.google.gwt.view.client.SelectionChangeEvent.Handler;
|
||||
import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
|
||||
import com.sap.sailing.domain.common.media.MediaTrack;
|
||||
import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO;
|
||||
@@ -177,7 +174,7 @@ public class MediaPanel extends FlowPanel implements FilterablePanelProvider<Med
|
||||
}).center();
|
||||
}
|
||||
});
|
||||
final Button multiUrlChange = buttonAndFilterPanel.addUpdateAction(stringMessages.multiUrlChangeMediaTrack(),
|
||||
buttonAndFilterPanel.addUpdateAction(stringMessages.multiUrlChangeMediaTrack(),
|
||||
refreshableSelectionModel,
|
||||
new Command() {
|
||||
@Override
|
||||
@@ -195,33 +192,16 @@ public class MediaPanel extends FlowPanel implements FilterablePanelProvider<Med
|
||||
}
|
||||
}
|
||||
});
|
||||
final Button remove = buttonAndFilterPanel.addRemoveAction(stringMessages.remove(), refreshableSelectionModel,
|
||||
buttonAndFilterPanel.addRemoveAction(stringMessages.remove(), refreshableSelectionModel,
|
||||
/* with confirmation */ true, new Command() {
|
||||
@Override
|
||||
public void execute() {
|
||||
for (final MediaTrackWithSecurityDTO track : refreshableSelectionModel.getSelectedSet()) {
|
||||
final List<MediaTrackWithSecurityDTO> selected = new ArrayList<>(refreshableSelectionModel.getSelectedSet());
|
||||
for (final MediaTrackWithSecurityDTO track : selected) {
|
||||
removeMediaTrack(track);
|
||||
}
|
||||
}
|
||||
});
|
||||
refreshableSelectionModel.addSelectionChangeHandler(new Handler() {
|
||||
@Override
|
||||
public void onSelectionChange(final SelectionChangeEvent event) {
|
||||
final Set<MediaTrackWithSecurityDTO> selected = refreshableSelectionModel.getSelectedSet();
|
||||
boolean canDeleteAllSelected = true;
|
||||
boolean canUpdateAllSelected = true;
|
||||
for (final MediaTrackWithSecurityDTO track : selected) {
|
||||
if (!userService.hasPermission(track, DefaultActions.DELETE)) {
|
||||
canDeleteAllSelected = false;
|
||||
}
|
||||
if (!userService.hasPermission(track, DefaultActions.UPDATE)) {
|
||||
canUpdateAllSelected = false;
|
||||
}
|
||||
}
|
||||
remove.setEnabled(!selected.isEmpty() && canDeleteAllSelected);
|
||||
multiUrlChange.setEnabled(!selected.isEmpty() && canUpdateAllSelected);
|
||||
}
|
||||
});
|
||||
buttonAndFilterPanel.addUnsecuredWidget(lblFilterRaces);
|
||||
createMediaTracksTable(userService);
|
||||
filterableMediaTracks.getTextBox().ensureDebugId("MediaTracksFilterTextBox");
|
||||
|
||||
+4
-1
@@ -61,7 +61,10 @@ public class RaceLogTrackingEventManagementRaceImagesBarCell extends ImagesBarCe
|
||||
result.add(new ImageSpec(ACTION_SET_TRACKING_TIMES, stringMessages.setTrackingTimes(), makeImagePrototype(resources.setTrackingTimes())));
|
||||
final boolean trackerExists = object.getA().getRaceLogTrackingInfo(object.getB()).raceLogTrackerExists;
|
||||
final RaceLogTrackingState trackingState = object.getA().getRaceLogTrackingInfo(object.getB()).raceLogTrackingState;
|
||||
if (trackingState == RaceLogTrackingState.AWAITING_RACE_DEFINITION || (trackingState == RaceLogTrackingState.TRACKING && !trackerExists)) {
|
||||
// bug6251: also suppress "Start Tracking" when a tracked race is already linked to the slot (e.g. FINISHED after
|
||||
// stopping tracking) — starting would fail on the server because the tracked race would need to be removed first.
|
||||
final boolean trackedRaceLinked = object.getA().isTrackedRace(object.getB());
|
||||
if (!trackedRaceLinked && (trackingState == RaceLogTrackingState.AWAITING_RACE_DEFINITION || (trackingState == RaceLogTrackingState.TRACKING && !trackerExists))) {
|
||||
result.add(new ImageSpec(ACTION_START_TRACKING, stringMessages.startTracking(), makeImagePrototype(resources.startRaceLogTracking())));
|
||||
} else if (trackingState == RaceLogTrackingState.TRACKING && trackerExists) {
|
||||
result.add(new ImageSpec(ACTION_STOP_TRACKING, stringMessages.stopTracking(), makeImagePrototype(resources.stopRaceLogTracking())));
|
||||
|
||||
+3
-12
@@ -15,7 +15,6 @@ import com.google.gwt.user.client.ui.CaptionPanel;
|
||||
import com.google.gwt.user.client.ui.SimplePanel;
|
||||
import com.google.gwt.user.client.ui.VerticalPanel;
|
||||
import com.google.gwt.view.client.SelectionChangeEvent;
|
||||
import com.google.gwt.view.client.SelectionChangeEvent.Handler;
|
||||
import com.sap.sailing.domain.common.RegattaIdentifier;
|
||||
import com.sap.sailing.gwt.common.client.help.HelpButton;
|
||||
import com.sap.sailing.gwt.common.client.help.HelpButtonResources;
|
||||
@@ -29,7 +28,6 @@ import com.sap.sse.gwt.adminconsole.FilterablePanelProvider;
|
||||
import com.sap.sse.gwt.client.ErrorReporter;
|
||||
import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel;
|
||||
import com.sap.sse.gwt.client.panels.AbstractFilterablePanel;
|
||||
import com.sap.sse.security.shared.HasPermissions.DefaultActions;
|
||||
import com.sap.sse.security.ui.client.UserService;
|
||||
import com.sap.sse.security.ui.client.component.AccessControlledButtonPanel;
|
||||
|
||||
@@ -79,7 +77,7 @@ public class RegattaManagementPanel extends SimplePanel implements FilterablePan
|
||||
update.ensureDebugId("UpdateRegattaButton");
|
||||
final Button create = buttonPanel.addCreateAction(stringMessages.addRegatta(), this::openCreateRegattaDialog);
|
||||
create.ensureDebugId("AddRegattaButton");
|
||||
final Button remove = buttonPanel.addRemoveAction(stringMessages.remove(),
|
||||
buttonPanel.addRemoveAction(stringMessages.remove(),
|
||||
refreshableRegattaMultiSelectionModel, true, () -> {
|
||||
// unmodifiable collection can't be sent to the server.
|
||||
final Collection<RegattaIdentifier> regattas = createModifiableCollection();
|
||||
@@ -88,10 +86,10 @@ public class RegattaManagementPanel extends SimplePanel implements FilterablePan
|
||||
buttonPanel.addUnsecuredWidget(new HelpButton(HelpButtonResources.INSTANCE,
|
||||
stringMessages.videoGuide(), "https://sapsailing-documentation.s3-eu-west-1.amazonaws.com/adminconsole/Advanced+Topics/Setting+up+Events+with+multiple+Regattas+or+Classes.mp4"));
|
||||
regattasContentPanel.add(buttonPanel);
|
||||
refreshableRegattaMultiSelectionModel.addSelectionChangeHandler(new Handler() {
|
||||
refreshableRegattaMultiSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
|
||||
@Override
|
||||
public void onSelectionChange(SelectionChangeEvent event) {
|
||||
List<RegattaDTO> selectedRegattas = new ArrayList<>(
|
||||
final List<RegattaDTO> selectedRegattas = new ArrayList<>(
|
||||
refreshableRegattaMultiSelectionModel.getSelectedSet());
|
||||
final RegattaIdentifier selectedRegatta;
|
||||
if (selectedRegattas.size() == 1) {
|
||||
@@ -109,13 +107,6 @@ public class RegattaManagementPanel extends SimplePanel implements FilterablePan
|
||||
regattaDetailsComposite.setRegatta(null);
|
||||
regattaDetailsComposite.setVisible(false);
|
||||
}
|
||||
boolean canDeleteAllSelected = true;
|
||||
for (RegattaDTO regatta : refreshableRegattaMultiSelectionModel.getSelectedSet()) {
|
||||
if (!userService.hasPermission(regatta, DefaultActions.DELETE)) {
|
||||
canDeleteAllSelected = false;
|
||||
}
|
||||
}
|
||||
remove.setEnabled(!selectedRegattas.isEmpty() && canDeleteAllSelected);
|
||||
}
|
||||
});
|
||||
regattasContentPanel.add(regattaListComposite);
|
||||
|
||||
+5
-1
@@ -1,6 +1,7 @@
|
||||
package com.sap.sailing.gwt.ui.adminconsole;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -56,7 +57,10 @@ public class ResultImportUrlsListComposite extends Composite {
|
||||
add.ensureDebugId("AddUrlButton");
|
||||
add.setEnabled(false);
|
||||
final Button remove = buttonPanel.addRemoveAction(stringMessages.remove(), table.getSelectionModel(),
|
||||
/* withConfirmation */ true, () -> removeUrls(table.getSelectionModel().getSelectedSet()));
|
||||
/* withConfirmation */ true, () -> {
|
||||
final Set<UrlDTO> selected = new HashSet<>(table.getSelectionModel().getSelectedSet());
|
||||
removeUrls(selected);
|
||||
});
|
||||
remove.ensureDebugId("RemoveUrlButton");
|
||||
final Button refresh = buttonPanel.addUnsecuredAction(stringMessages.refresh(), this::updateTable);
|
||||
refresh.ensureDebugId("RefreshUrlButton");
|
||||
|
||||
+5
@@ -602,6 +602,11 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC
|
||||
} else if (!getTrackingState(race).isForTracking()) {
|
||||
allCanStart = false;
|
||||
allCanStop = false;
|
||||
// bug6251: a linked tracked race (e.g. FINISHED) has no active tracker but cannot be tracked again
|
||||
// until the existing tracked race is removed first — disable "Start Tracking" in that case.
|
||||
} else if (race.getA().isTrackedRace(race.getB())) {
|
||||
allCanStart = false;
|
||||
allCanStop = false;
|
||||
} else {
|
||||
allCanStop = false;
|
||||
}
|
||||
|
||||
+3
-2
@@ -100,8 +100,9 @@ public class CourseTemplatePanel extends FlowPanel implements FilterablePanelPro
|
||||
buttonAndFilterPanel.addCreateAction(stringMessages.add(),
|
||||
() -> openEditCourseTemplateDialog(new CourseTemplateDTO(), userService, true));
|
||||
buttonAndFilterPanel.addRemoveAction(stringMessages.remove(), refreshableSelectionModel, true,
|
||||
() -> removeCourseTemplates(refreshableSelectionModel.getSelectedSet().stream()
|
||||
.map(courseTemplateDTO -> courseTemplateDTO.getUuid()).collect(Collectors.toList())));
|
||||
() -> {final List<UUID> uuids = refreshableSelectionModel.getSelectedSet().stream().map(courseTemplateDTO -> courseTemplateDTO.getUuid()).collect(Collectors.toList());
|
||||
removeCourseTemplates(uuids);
|
||||
});
|
||||
buttonAndFilterPanel.addUnsecuredWidget(lblFilterRaces);
|
||||
buttonAndFilterPanel.addUnsecuredWidget(filterableCourseTemplatePanel);
|
||||
filterableCourseTemplatePanel
|
||||
|
||||
+3
-2
@@ -110,8 +110,9 @@ public class MarkPropertiesPanel extends FlowPanel implements FilterablePanelPro
|
||||
buttonAndFilterPanel.addCreateAction(stringMessages.add(),
|
||||
() -> openEditMarkPropertiesDialog(new MarkPropertiesDTO()));
|
||||
buttonAndFilterPanel.addRemoveAction(stringMessages.remove(), refreshableSelectionModel, true,
|
||||
() -> removeMarkProperties(refreshableSelectionModel.getSelectedSet().stream()
|
||||
.map(markPropertiesDTO -> markPropertiesDTO.getUuid()).collect(Collectors.toList())));
|
||||
() -> { final List<UUID> uuids = refreshableSelectionModel.getSelectedSet().stream().map(markPropertiesDTO -> markPropertiesDTO.getUuid()).collect(Collectors.toList());
|
||||
removeMarkProperties(uuids);
|
||||
});
|
||||
buttonAndFilterPanel.addUnsecuredWidget(lblFilterRaces);
|
||||
filterableMarkProperties.getTextBox().ensureDebugId("MarkPropertiesFilterTextBox");
|
||||
buttonAndFilterPanel.addUnsecuredWidget(filterableMarkProperties);
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@ package com.sap.sailing.gwt.ui.shared;
|
||||
import java.util.Date;
|
||||
|
||||
import com.sap.sailing.domain.common.security.SecuredDomainType;
|
||||
import com.sap.sse.common.Named;
|
||||
import com.sap.sse.common.TimePoint;
|
||||
import com.sap.sse.security.shared.HasPermissions;
|
||||
import com.sap.sse.security.shared.QualifiedObjectIdentifier;
|
||||
@@ -12,7 +13,7 @@ import com.sap.sse.security.shared.dto.OwnershipDTO;
|
||||
import com.sap.sse.security.shared.dto.SecuredDTO;
|
||||
import com.sap.sse.security.shared.dto.SecurityInformationDTO;
|
||||
|
||||
public class IgtimiDataAccessWindowWithSecurityDTO implements SecuredDTO {
|
||||
public class IgtimiDataAccessWindowWithSecurityDTO implements SecuredDTO, Named {
|
||||
private static final long serialVersionUID = 176992188692729118L;
|
||||
private SecurityInformationDTO securityInformation = new SecurityInformationDTO();
|
||||
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
package com.sap.sailing.gwt.ui.shared;
|
||||
|
||||
import com.sap.sailing.domain.common.security.SecuredDomainType;
|
||||
import com.sap.sse.common.Named;
|
||||
import com.sap.sse.common.Position;
|
||||
import com.sap.sse.common.TimePoint;
|
||||
import com.sap.sse.common.Util.Pair;
|
||||
@@ -12,7 +13,7 @@ import com.sap.sse.security.shared.dto.OwnershipDTO;
|
||||
import com.sap.sse.security.shared.dto.SecuredDTO;
|
||||
import com.sap.sse.security.shared.dto.SecurityInformationDTO;
|
||||
|
||||
public class IgtimiDeviceWithSecurityDTO implements SecuredDTO {
|
||||
public class IgtimiDeviceWithSecurityDTO implements SecuredDTO, Named {
|
||||
private static final long serialVersionUID = 176992188692729118L;
|
||||
private SecurityInformationDTO securityInformation = new SecurityInformationDTO();
|
||||
|
||||
|
||||
+10
-2
@@ -33,6 +33,8 @@ public class UserGroupRoleDefinitionPanelPO extends PageArea {
|
||||
private static final String TABLE_ROLE_NAME_COLUMN = "Role Name";
|
||||
@FindBy(how = BySeleniumId.class, using = "AddGroupUserButton")
|
||||
private WebElement addRoleButton;
|
||||
@FindBy(how = BySeleniumId.class, using = "RemoveRoleButton")
|
||||
private WebElement removeRoleButton;
|
||||
@FindBy(how = BySeleniumId.class, using = "RoleSuggestion")
|
||||
private WebElement roleNameInput;
|
||||
@FindBy(how = BySeleniumId.class, using = "GroupRoleDefinitionDTOTable")
|
||||
@@ -85,8 +87,14 @@ public class UserGroupRoleDefinitionPanelPO extends PageArea {
|
||||
}
|
||||
|
||||
public void removeRole(String name) {
|
||||
final RoleEntryPO findRole = findRole(name);
|
||||
findRole.deleteRole();
|
||||
final RoleEntryPO role = findRole(name);
|
||||
role.select();
|
||||
removeRoleButton.click();
|
||||
waitForAlertContainingMessageAndAccept("The following element(s) will be removed");
|
||||
}
|
||||
|
||||
public void removeRoleViaActionButton(final String name) {
|
||||
findRole(name).deleteRole();
|
||||
waitForAlertContainingMessageAndAccept("Do you really want to remove role");
|
||||
}
|
||||
|
||||
|
||||
+13
@@ -87,6 +87,19 @@ public class TestUserGroupCreation extends AbstractSeleniumTest {
|
||||
|
||||
@SeleniumTestCase
|
||||
public void testRoleRemoval() {
|
||||
final UserGroupManagementPanelPO userGroupManagementPanel = goToUserGroupDefinitionsPanel();
|
||||
createGroup(userGroupManagementPanel);
|
||||
userGroupManagementPanel.selectGroup(TEST_GROUP_NAME);
|
||||
final UserGroupRoleDefinitionPanelPO userRolesPO = userGroupManagementPanel.getUserGroupRoles();
|
||||
createRole(userRolesPO);
|
||||
userGroupManagementPanel.selectGroup(TEST_GROUP_NAME);
|
||||
userRolesPO.removeRoleViaActionButton(TEST_ROLE);
|
||||
userGroupManagementPanel.selectGroup(TEST_GROUP_NAME);
|
||||
assertNull(userRolesPO.findRole(TEST_ROLE));
|
||||
}
|
||||
|
||||
@SeleniumTestCase
|
||||
public void testRoleRemovalViaGlobalRemoveButton() {
|
||||
final UserGroupManagementPanelPO userGroupManagementPanel = goToUserGroupDefinitionsPanel();
|
||||
createGroup(userGroupManagementPanel);
|
||||
userGroupManagementPanel.selectGroup(TEST_GROUP_NAME);
|
||||
|
||||
+23
-6
@@ -120,7 +120,12 @@ public class RefreshableMultiSelectionModel<T> extends MultiSelectionModelWithSe
|
||||
* {@link AbstractSelectionModel#fireEvent(com.google.gwt.event.shared.GwtEvent)}.
|
||||
*
|
||||
* @param newObjects
|
||||
* the new objects to refresh the {@link RefreshableMultiSelectionModel selection model}
|
||||
* the new objects to refresh the {@link RefreshableMultiSelectionModel selection model}. When this
|
||||
* selection model is used together with {@link com.sap.sse.gwt.client.panels.AbstractFilterablePanel},
|
||||
* this must be the <em>unfiltered</em> list (i.e. the {@code all} {@link ListDataProvider}, not the
|
||||
* {@code filtered} one), so that items hidden by the current filter are not incorrectly deselected.
|
||||
* {@link HasDataAdapter} ensures this by passing {@code listDataProvider.getList()} — which is the
|
||||
* {@code all} provider registered in the {@code AbstractFilterablePanel} constructor.
|
||||
*/
|
||||
@Override
|
||||
public void refreshSelectionModel(Iterable<T> newObjects) {
|
||||
@@ -129,14 +134,26 @@ public class RefreshableMultiSelectionModel<T> extends MultiSelectionModelWithSe
|
||||
try {
|
||||
if (!isEmpty()) {
|
||||
for (T it : newObjects) {
|
||||
if (isSelected(it)) {
|
||||
if (isSelected(it)) {
|
||||
setSelected(it, true); // this updates matching elements in the selection model
|
||||
}
|
||||
}
|
||||
// elements that were selected before and that don't have a corresponding element in newObjects
|
||||
// will just be left alone; they will probably remain in selectedSet, and they were probably not in
|
||||
// newObjects because a filter removed them. But when they re-appear, e.g., because the filter is
|
||||
// removed, the elements will naturally be selected again.
|
||||
// Deselect items that are no longer present in newObjects (e.g. because they were deleted).
|
||||
// newObjects comes from getAllListDataProvider() (the unfiltered list), so absence here means
|
||||
// true deletion, not just a filter hiding the item.
|
||||
// getSelectedElements() already returns a snapshot copy, so no additional copy is needed here.
|
||||
for (final T selected : getSelectedElements()) {
|
||||
boolean foundInNew = false;
|
||||
for (final T candidate : newObjects) {
|
||||
if (comp != null ? comp.representSameEntity(selected, candidate) : selected.equals(candidate)) {
|
||||
foundInNew = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!foundInNew) {
|
||||
super.setSelected(selected, false);
|
||||
}
|
||||
}
|
||||
SelectionChangeEvent.fire(this);
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -272,6 +272,7 @@ public abstract class AbstractFilterablePanel<T> extends HorizontalPanel {
|
||||
* Removes an object and applies the search filter.
|
||||
*/
|
||||
public void remove(T object) {
|
||||
select(object, false);
|
||||
all.getList().remove(object);
|
||||
filter();
|
||||
}
|
||||
@@ -287,11 +288,13 @@ public abstract class AbstractFilterablePanel<T> extends HorizontalPanel {
|
||||
* <code>removeAll(all)</code> with <code>all</code> being a copy of what you get when calling {@link #getAll()}.
|
||||
*/
|
||||
public void removeAll() {
|
||||
deselectAll();
|
||||
all.getList().clear();
|
||||
filter();
|
||||
}
|
||||
|
||||
public void removeAll(Iterable<T> objects) {
|
||||
objects.forEach(o->select(o, false)); // clear those objects removed from the selection
|
||||
Util.removeAll(objects, all.getList());
|
||||
filter();
|
||||
}
|
||||
|
||||
+113
-30
@@ -3,19 +3,25 @@ package com.sap.sse.security.ui.client.component;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.gwt.dom.client.Style.Unit;
|
||||
import com.google.gwt.event.dom.client.ClickHandler;
|
||||
import com.google.gwt.user.client.Command;
|
||||
import com.google.gwt.user.client.Window;
|
||||
import com.google.gwt.user.client.ui.Button;
|
||||
import com.google.gwt.user.client.ui.Composite;
|
||||
import com.google.gwt.user.client.ui.HorizontalPanel;
|
||||
import com.google.gwt.user.client.ui.TextBox;
|
||||
import com.google.gwt.user.client.ui.Widget;
|
||||
import com.google.gwt.view.client.MultiSelectionModel;
|
||||
import com.google.gwt.view.client.SetSelectionModel;
|
||||
import com.sap.sse.common.Named;
|
||||
import com.sap.sse.security.shared.HasPermissions;
|
||||
import com.sap.sse.security.shared.HasPermissions.DefaultActions;
|
||||
import com.sap.sse.security.shared.dto.SecuredDTO;
|
||||
import com.sap.sse.security.ui.client.UserService;
|
||||
import com.sap.sse.security.ui.client.i18n.StringMessages;
|
||||
|
||||
@@ -30,6 +36,7 @@ public class AccessControlledButtonPanel extends Composite {
|
||||
private final HorizontalPanel panel = new HorizontalPanel();
|
||||
private final Map<Button, Supplier<Boolean>> buttonToPermissions = new HashMap<>();
|
||||
|
||||
private final UserService userService;
|
||||
private final Supplier<Boolean> createPermissionCheck, createPermissionCheckWithoutServerCreateObjectCheck,
|
||||
removePermissionCheck, updatePermissionCheck;
|
||||
private final BiConsumer<Button, Supplier<Boolean>> visibilityUpdater = (btn, check) -> btn.setVisible(check.get());
|
||||
@@ -44,6 +51,7 @@ public class AccessControlledButtonPanel extends Composite {
|
||||
* the {@link HasPermissions} representing the type of objects to be secured by this panel
|
||||
*/
|
||||
public AccessControlledButtonPanel(final UserService userService, final HasPermissions type) {
|
||||
this.userService = userService;
|
||||
this.createPermissionCheck = () -> userService.hasCurrentUserPermissionToCreateObjectOfType(type);
|
||||
this.createPermissionCheckWithoutServerCreateObjectCheck = () -> userService
|
||||
.hasCurrentUserPermissionToCreateObjectOfTypeWithoutServerCreateObjectPermissionCheck(type);
|
||||
@@ -104,39 +112,103 @@ public class AccessControlledButtonPanel extends Composite {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a secured action button, which is only visible if the current user has any
|
||||
* {@link UserService#hasCurrentUserPermissionToDeleteAnyObjectOfType(HasPermissions) delete permission} for the
|
||||
* {@link HasPermissions type} provided in this {@link AccessControlledButtonPanel}'s constructor.
|
||||
* Like {@link #addRemoveAction(String, SetSelectionModel, boolean, Command)} but for rows that are not
|
||||
* {@link SecuredDTO} instances themselves — use this when the permission to remove entries is governed by a parent
|
||||
* secured object rather than per-row permissions. The button is {@link Button#setEnabled(boolean) enabled} when the
|
||||
* selection is non-empty and the current user has the specified {@code permissionAction} on the object supplied by
|
||||
* {@code parentSecuredObject}; it shows the selected count in its label.
|
||||
*
|
||||
* @param text
|
||||
* the {@link String text} to show on the button
|
||||
* @param callback
|
||||
* the {@link Command callback} to execute on button click, if permission is granted
|
||||
* @return the created {@link Button} instance
|
||||
*/
|
||||
public Button addRemoveAction(final String text, final Command callback) {
|
||||
return addAction(text, removePermissionCheck, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a secured action button, which is only visible if the current user has any
|
||||
* {@link UserService#hasCurrentUserPermissionToDeleteAnyObjectOfType(HasPermissions) delete permission} for the
|
||||
* {@link HasPermissions type} provided in this {@link AccessControlledButtonPanel}'s constructor.
|
||||
* <p>Example: removing a role or user from a {@code UserGroup} is semantically an UPDATE to that group, so the
|
||||
* caller should pass {@link DefaultActions#UPDATE} as {@code permissionAction} even though the button is labelled
|
||||
* "Remove".
|
||||
*
|
||||
* @param text
|
||||
* the {@link String text} to show on the button
|
||||
* @param selectionModel
|
||||
* the {@link SetSelectionModel<T> selection model} of the table; used to enable/disable the remove
|
||||
* button when the selection becomes non-empty/empty, respectively and to display the number of elements
|
||||
* selected in case the selection contains more than one element
|
||||
* the {@link MultiSelectionModel} of the sub-table; drives the count shown in the button label and the
|
||||
* enabled state
|
||||
* @param parentSecuredObject
|
||||
* supplies the parent {@link SecuredDTO} whose permission gates the button; may return {@code null} when
|
||||
* nothing is selected, which disables the button
|
||||
* @param permissionAction
|
||||
* the {@link DefaultActions action} to check on the parent secured object (e.g.
|
||||
* {@link DefaultActions#UPDATE} or {@link DefaultActions#DELETE})
|
||||
* @param callback
|
||||
* the {@link Command callback} to execute on button click, if permission is granted
|
||||
* @return the created {@link Button} instance
|
||||
*/
|
||||
public <T> Button addCountingActionWithParentPermission(final String text, final MultiSelectionModel<T> selectionModel,
|
||||
final Supplier<SecuredDTO> parentSecuredObject, final DefaultActions permissionAction, final Command callback) {
|
||||
final Button button = resolveButtonVisibility(removePermissionCheck,
|
||||
new Button(text, wrap(removePermissionCheck, callback)));
|
||||
selectionModel.addSelectionChangeHandler(event -> {
|
||||
final int count = selectionModel.getSelectedSet().size();
|
||||
button.setText(count > 0 ? text + " (" + count + ")" : text);
|
||||
button.setEnabled(count > 0 && userService.hasPermission(parentSecuredObject.get(), permissionAction));
|
||||
});
|
||||
button.setEnabled(false);
|
||||
return button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #addRemoveAction(String, SetSelectionModel, boolean, Command)} but for rows that are not
|
||||
* {@link SecuredDTO} instances themselves — use this when the permission to remove entries is governed by a parent
|
||||
* secured object rather than per-row permissions. A confirmation dialog is always shown before the {@code callback}
|
||||
* is invoked, with the selected elements listed using the provided {@code nameMapper}.
|
||||
*
|
||||
* @param text
|
||||
* the {@link String text} to show on the button
|
||||
* @param selectionModel
|
||||
* the {@link SetSelectionModel} of the sub-table; drives the count shown in the button label and the
|
||||
* enabled state
|
||||
* @param nameMapper
|
||||
* maps each selected element to the {@link String} name to display in the confirmation message
|
||||
* @param parentSecuredObject
|
||||
* supplies the parent {@link SecuredDTO} whose permission gates the button; may return {@code null} when
|
||||
* nothing is selected, which disables the button
|
||||
* @param permissionAction
|
||||
* the {@link DefaultActions action} to check on the parent secured object (e.g.
|
||||
* {@link DefaultActions#UPDATE} or {@link DefaultActions#DELETE})
|
||||
* @param callback
|
||||
* the {@link Command callback} to execute on button click, if permission is granted and confirmed
|
||||
* @return the created {@link Button} instance
|
||||
*/
|
||||
public <T> Button addRemoveActionWithParentPermission(final String text, final SetSelectionModel<T> selectionModel,
|
||||
final Function<T, String> nameMapper, final Supplier<SecuredDTO> parentSecuredObject,
|
||||
final DefaultActions permissionAction, final Command callback) {
|
||||
final Command confirmingCallback = () -> {
|
||||
final String names = selectionModel.getSelectedSet().stream().map(nameMapper)
|
||||
.collect(Collectors.joining("\n"));
|
||||
if (Window.confirm(StringMessages.INSTANCE.doYouReallyWantToRemoveSelectedElements(names))) {
|
||||
callback.execute();
|
||||
}
|
||||
};
|
||||
final Button button = resolveButtonVisibility(removePermissionCheck,
|
||||
new Button(text, wrap(removePermissionCheck, confirmingCallback)));
|
||||
selectionModel.addSelectionChangeHandler(event -> {
|
||||
final int count = selectionModel.getSelectedSet().size();
|
||||
button.setText(count > 0 ? text + " (" + count + ")" : text);
|
||||
button.setEnabled(count > 0 && userService.hasPermission(parentSecuredObject.get(), permissionAction));
|
||||
});
|
||||
button.setEnabled(false);
|
||||
return button;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param text
|
||||
* the {@link String text} to show on the button
|
||||
* @param selectionModel
|
||||
* the {@link SetSelectionModel} of the table; used to track the selected elements, display the count of
|
||||
* selected elements in the button text, and drive the enabled state of the button
|
||||
* @param withConfirmation
|
||||
* the {@link Boolean} flag indicates whether to show confirmation or not
|
||||
* when {@code true}, a confirmation dialog is shown before the {@code callback} is executed
|
||||
* @param callback
|
||||
* the {@link Command callback} to execute on button click, if permission is granted
|
||||
*
|
||||
* @return the created {@link SelectedElementsCountingButton} instance with optional confirmation
|
||||
*/
|
||||
public <T extends Named> Button addRemoveAction(final String text, final SetSelectionModel<T> selectionModel,
|
||||
public <T extends Named & SecuredDTO> Button addRemoveAction(final String text, final SetSelectionModel<T> selectionModel,
|
||||
boolean withConfirmation, final Command callback) {
|
||||
if (selectionModel == null) {
|
||||
throw new IllegalArgumentException("Selection model for a remove action must not be null");
|
||||
@@ -146,6 +218,11 @@ public class AccessControlledButtonPanel extends Composite {
|
||||
? new SelectedElementsCountingButton<T>(text, selectionModel, StringMessages.INSTANCE::doYouReallyWantToRemoveSelectedElements,
|
||||
handler)
|
||||
: new SelectedElementsCountingButton<T>(text, selectionModel, handler);
|
||||
selectionModel.addSelectionChangeHandler(event -> {
|
||||
final boolean canActOnAllSelected = selectionModel.getSelectedSet().stream()
|
||||
.allMatch(item -> userService.hasPermission(item, DefaultActions.DELETE));
|
||||
button.setEnabled(!selectionModel.getSelectedSet().isEmpty() && canActOnAllSelected);
|
||||
});
|
||||
return resolveButtonVisibility(removePermissionCheck, button);
|
||||
}
|
||||
|
||||
@@ -165,28 +242,34 @@ public class AccessControlledButtonPanel extends Composite {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a secured action button, which is only visible if the current user has any
|
||||
* {@link UserService#hasCurrentUserPermissionToDeleteAnyObjectOfType(HasPermissions) update permission} for the
|
||||
* {@link HasPermissions type} provided in this {@link AccessControlledButtonPanel}'s constructor.
|
||||
* Adds a secured action button, which is only {@link Button#setVisible(boolean) visible} if the current user has
|
||||
* any {@link UserService#hasCurrentUserPermissionToUpdateAnyObjectOfType(HasPermissions) update permission} for the
|
||||
* {@link HasPermissions type} provided in this {@link AccessControlledButtonPanel}'s constructor, and which is only
|
||||
* {@link Button#setEnabled(boolean) enabled} when the selection is non-empty and the current user has the
|
||||
* {@link DefaultActions#UPDATE update permission} on every individually selected object.
|
||||
*
|
||||
* @param text
|
||||
* the {@link String text} to show on the button
|
||||
* @param selectionModel
|
||||
* the {@link SetSelectionModel<T> selection model} of the table; used to enable/disable the remove
|
||||
* button when the selection becomes non-empty/empty, respectively and to display the number of elements
|
||||
* selected in case the selection contains more than one element
|
||||
* the {@link SetSelectionModel} of the table; used to track the selected elements, display the count of
|
||||
* selected elements in the button text, and drive the enabled state of the button
|
||||
* @param callback
|
||||
* the {@link Command callback} to execute on button click, if permission is granted
|
||||
*
|
||||
* @return the created {@link SelectedElementsCountingButton} instance with optional confirmation
|
||||
* @return the created {@link SelectedElementsCountingButton} instance
|
||||
*/
|
||||
public <T extends Named> Button addUpdateAction(final String text, final SetSelectionModel<T> selectionModel,
|
||||
public <T extends Named & SecuredDTO> Button addUpdateAction(final String text, final SetSelectionModel<T> selectionModel,
|
||||
final Command callback) {
|
||||
if (selectionModel == null) {
|
||||
throw new IllegalArgumentException("Selection model for an update action must not be null");
|
||||
}
|
||||
final ClickHandler handler = wrap(updatePermissionCheck, callback);
|
||||
final Button button = new SelectedElementsCountingButton<T>(text, selectionModel, handler);
|
||||
selectionModel.addSelectionChangeHandler(event -> {
|
||||
final boolean canActOnAllSelected = selectionModel.getSelectedSet().stream()
|
||||
.allMatch(item -> userService.hasPermission(item, DefaultActions.UPDATE));
|
||||
button.setEnabled(!selectionModel.getSelectedSet().isEmpty() && canActOnAllSelected);
|
||||
});
|
||||
return resolveButtonVisibility(updatePermissionCheck, button);
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -30,6 +30,7 @@ import com.google.gwt.user.client.ui.Button;
|
||||
import com.google.gwt.user.client.ui.Label;
|
||||
import com.google.gwt.user.client.ui.VerticalPanel;
|
||||
import com.google.gwt.view.client.ListDataProvider;
|
||||
import com.google.gwt.view.client.SetSelectionModel;
|
||||
import com.sap.sse.common.Util;
|
||||
import com.sap.sse.common.util.NaturalComparator;
|
||||
import com.sap.sse.gwt.client.ErrorReporter;
|
||||
@@ -101,18 +102,17 @@ public class RoleDefinitionsPanel extends VerticalPanel {
|
||||
roleDefinitionsTable.ensureDebugId("RolesCellTable");
|
||||
filterablePanelRoleDefinitions.getTextBox().ensureDebugId("RolesFilterTextBox");
|
||||
refreshableRoleDefinitionMultiSelectionModel = (RefreshableMultiSelectionModel<? super RoleDefinitionDTO>) roleDefinitionsTable.getSelectionModel();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
final SetSelectionModel<RoleDefinitionDTO> roleSelectionModel = (SetSelectionModel<RoleDefinitionDTO>) roleDefinitionsTable.getSelectionModel();
|
||||
final AccessControlledButtonPanel buttonPanel = new AccessControlledButtonPanel(userService, ROLE_DEFINITION);
|
||||
buttonPanel.addUnsecuredAction(stringMessages.refresh(), this::updateRoleDefinitions);
|
||||
final Button createButton = buttonPanel.addCreateActionWithoutServerCreateObjectPermissionCheck(stringMessages.add(),
|
||||
this::createRoleDefinition);
|
||||
createButton.ensureDebugId("CreateRoleButton");
|
||||
final Button removeButton = buttonPanel.addRemoveAction(stringMessages.remove(), () -> {
|
||||
final String roles = String.join(", ", Util.map(getSelectedRoleDefinitions(), RoleDefinitionDTO::getName));
|
||||
if (Window.confirm(stringMessages.doYouReallyWantToRemoveRole(roles))) {
|
||||
final Set<RoleDefinitionDTO> selectedRoles = new HashSet<>(getSelectedRoleDefinitions());
|
||||
filterablePanelRoleDefinitions.removeAll(selectedRoles);
|
||||
}
|
||||
final Button removeButton = buttonPanel.addRemoveAction(stringMessages.remove(), roleSelectionModel, true,
|
||||
() -> {
|
||||
final Set<RoleDefinitionDTO> selectedRoles = new HashSet<>(getSelectedRoleDefinitions());
|
||||
filterablePanelRoleDefinitions.removeAll(selectedRoles);
|
||||
});
|
||||
removeButton.ensureDebugId("RemoveRoleButton");
|
||||
add(buttonPanel);
|
||||
|
||||
+10
-13
@@ -26,6 +26,7 @@ import com.sap.sse.gwt.client.celltable.TableWrapper;
|
||||
import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel;
|
||||
import com.sap.sse.security.shared.dto.StrippedRoleDefinitionDTO;
|
||||
import com.sap.sse.security.shared.dto.UserGroupDTO;
|
||||
import com.sap.sse.security.shared.dto.SecuredDTO;
|
||||
import com.sap.sse.security.ui.client.UserManagementWriteServiceAsync;
|
||||
import com.sap.sse.security.ui.client.UserService;
|
||||
import com.sap.sse.security.ui.client.component.AccessControlledButtonPanel;
|
||||
@@ -122,14 +123,14 @@ public class GroupRoleDefinitionPanel extends Composite
|
||||
}
|
||||
});
|
||||
addButton.ensureDebugId("AddGroupUserButton");
|
||||
final Button removeButton = buttonPanel.addUpdateAction(stringMessages.removeRole(), () -> {
|
||||
Pair<StrippedRoleDefinitionDTO, Boolean> selectedRole = roleDefinitionTableWrapper.getSelectionModel()
|
||||
.getSelectedObject();
|
||||
if (selectedRole == null) {
|
||||
Window.alert(stringMessages.youHaveToSelectAUserGroup());
|
||||
} else if (Window.confirm(stringMessages.doYouReallyWantToRemoveRole(selectedRole.getA().getName()))) {
|
||||
UserGroupDTO selectedObject = TableWrapper.getSingleSelectedObjectOrNull(userGroupSelectionModel);
|
||||
if (selectedObject != null) {
|
||||
// Removing a role from a group is semantically an UPDATE to the UserGroup, not a per-role DELETE.
|
||||
final Button removeButton = buttonPanel.addRemoveActionWithParentPermission(stringMessages.removeRole(),
|
||||
roleDefinitionTableWrapper.getSelectionModel(),
|
||||
pair -> pair.getA().getName(),
|
||||
() -> (SecuredDTO) TableWrapper.getSingleSelectedObjectOrNull(userGroupSelectionModel), UPDATE, () -> {
|
||||
final UserGroupDTO selectedObject = TableWrapper.getSingleSelectedObjectOrNull(userGroupSelectionModel);
|
||||
if (selectedObject != null) {
|
||||
for (final Pair<StrippedRoleDefinitionDTO, Boolean> selectedRole : roleDefinitionTableWrapper.getSelectionModel().getSelectedElements()) {
|
||||
userManagementService.removeRoleDefinitionFromUserGroup(selectedObject.getId().toString(),
|
||||
selectedRole.getA().getId().toString(), new AsyncCallback<Void>() {
|
||||
@Override
|
||||
@@ -143,14 +144,10 @@ public class GroupRoleDefinitionPanel extends Composite
|
||||
updateUserGroups();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Window.alert(stringMessages.pleaseSelect());
|
||||
}
|
||||
}
|
||||
});
|
||||
roleDefinitionTableWrapper.getSelectionModel().addSelectionChangeHandler(event -> removeButton
|
||||
.setEnabled(!roleDefinitionTableWrapper.getSelectionModel().getSelectedSet().isEmpty()));
|
||||
removeButton.setEnabled(false);
|
||||
removeButton.ensureDebugId("RemoveRoleButton");
|
||||
buttonPanel.insertWidgetAtPosition(suggestRole, 0);
|
||||
return buttonPanel;
|
||||
}
|
||||
|
||||
+3
-3
@@ -17,7 +17,7 @@ import com.sap.sse.gwt.client.ErrorReporter;
|
||||
import com.sap.sse.gwt.client.celltable.AbstractSortableTextColumn;
|
||||
import com.sap.sse.gwt.client.celltable.CellTableWithCheckboxResources;
|
||||
import com.sap.sse.gwt.client.celltable.EntityIdentityComparator;
|
||||
import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel;
|
||||
import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel;
|
||||
import com.sap.sse.gwt.client.celltable.TableWrapper;
|
||||
import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel;
|
||||
import com.sap.sse.security.shared.dto.StrippedRoleDefinitionDTO;
|
||||
@@ -31,7 +31,7 @@ import com.sap.sse.security.ui.client.i18n.StringMessages;
|
||||
* Name and whether the role is enabled for all users. There is also an options to delete the group.
|
||||
*/
|
||||
public class RoleDefinitionTableWrapper extends
|
||||
TableWrapper<Pair<StrippedRoleDefinitionDTO, Boolean>, RefreshableSingleSelectionModel<Pair<StrippedRoleDefinitionDTO, Boolean>>, StringMessages, CellTableWithCheckboxResources> {
|
||||
TableWrapper<Pair<StrippedRoleDefinitionDTO, Boolean>, RefreshableMultiSelectionModel<Pair<StrippedRoleDefinitionDTO, Boolean>>, StringMessages, CellTableWithCheckboxResources> {
|
||||
|
||||
private final LabeledAbstractFilterablePanel<Pair<StrippedRoleDefinitionDTO, Boolean>> filterField;
|
||||
private final MultiSelectionModel<UserGroupDTO> userGroupSelectionModel;
|
||||
@@ -39,7 +39,7 @@ public class RoleDefinitionTableWrapper extends
|
||||
public RoleDefinitionTableWrapper(UserService userService, StringMessages stringMessages,
|
||||
ErrorReporter errorReporter, boolean enablePager, CellTableWithCheckboxResources tableResources,
|
||||
Runnable refresher, MultiSelectionModel<UserGroupDTO> userGroupSelectionModel) {
|
||||
super(stringMessages, errorReporter, false, enablePager,
|
||||
super(stringMessages, errorReporter, /* multiSelection */ true, enablePager,
|
||||
new EntityIdentityComparator<Pair<StrippedRoleDefinitionDTO, Boolean>>() {
|
||||
@Override
|
||||
public boolean representSameEntity(Pair<StrippedRoleDefinitionDTO, Boolean> dto1,
|
||||
|
||||
+8
-10
@@ -22,9 +22,11 @@ import com.google.gwt.view.client.SelectionChangeEvent.Handler;
|
||||
import com.sap.sse.common.Util;
|
||||
import com.sap.sse.gwt.client.ErrorReporter;
|
||||
import com.sap.sse.gwt.client.celltable.CellTableWithCheckboxResources;
|
||||
import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel;
|
||||
import com.sap.sse.gwt.client.celltable.TableWrapper;
|
||||
import com.sap.sse.security.shared.dto.StrippedUserDTO;
|
||||
import com.sap.sse.security.shared.dto.UserGroupDTO;
|
||||
import com.sap.sse.security.shared.dto.SecuredDTO;
|
||||
import com.sap.sse.security.ui.client.UserManagementWriteServiceAsync;
|
||||
import com.sap.sse.security.ui.client.UserService;
|
||||
import com.sap.sse.security.ui.client.component.AccessControlledButtonPanel;
|
||||
@@ -97,16 +99,15 @@ public class UserGroupDetailPanel extends Composite
|
||||
});
|
||||
addButton.ensureDebugId("AddUserButton");
|
||||
// add remove button
|
||||
final Button removeButton = buttonPanel.addUpdateAction(stringMessages.actionRemove(), () -> {
|
||||
// Removing a user from a group is semantically an UPDATE to the UserGroup, not a per-user DELETE.
|
||||
buttonPanel.addCountingActionWithParentPermission(stringMessages.actionRemove(),
|
||||
tenantUsersTable.getSelectionModel(),
|
||||
() -> (SecuredDTO) TableWrapper.getSingleSelectedObjectOrNull(userGroupSelectionModel), UPDATE, () -> {
|
||||
final Set<UserGroupDTO> selectedUserGroups = userGroupSelectionModel.getSelectedSet();
|
||||
if (selectedUserGroups != null && selectedUserGroups.size() == 1) {
|
||||
final UserGroupDTO selectedUserGroup = selectedUserGroups.iterator().next();
|
||||
Set<StrippedUserDTO> users = tenantUsersTable.getSelectionModel().getSelectedSet();
|
||||
if (selectedUserGroups == null || selectedUserGroups.isEmpty()) {
|
||||
Window.alert(stringMessages.youHaveToSelectAUserGroup());
|
||||
return;
|
||||
}
|
||||
for (StrippedUserDTO user : users) {
|
||||
final RefreshableMultiSelectionModel<StrippedUserDTO> usersSelectionModel = tenantUsersTable.getSelectionModel();
|
||||
for (final StrippedUserDTO user : usersSelectionModel.getSelectedElements()) {
|
||||
final String username = user.getName();
|
||||
userManagementService.removeUserFromUserGroup(selectedUserGroup.getId().toString(), username,
|
||||
new AsyncCallback<Void>() {
|
||||
@@ -134,9 +135,6 @@ public class UserGroupDetailPanel extends Composite
|
||||
}
|
||||
}
|
||||
});
|
||||
tenantUsersTable.getSelectionModel().addSelectionChangeHandler(
|
||||
event -> removeButton.setEnabled(!tenantUsersTable.getSelectionModel().getSelectedSet().isEmpty()));
|
||||
removeButton.setEnabled(false);
|
||||
return buttonPanel;
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -84,11 +84,12 @@ extends TableWrapper<UserDTO, S, StringMessages, TR> {
|
||||
new EntityIdentityComparator<UserDTO>() {
|
||||
@Override
|
||||
public boolean representSameEntity(UserDTO dto1, UserDTO dto2) {
|
||||
return dto1.getId().toString().equals(dto2.getId().toString());
|
||||
return Util.equalsWithNull(dto1.getId() != null ? dto1.getId().toString() : null,
|
||||
dto2.getId() != null ? dto2.getId().toString() : null);
|
||||
}
|
||||
@Override
|
||||
public int hashCode(UserDTO t) {
|
||||
return t.getId().hashCode();
|
||||
return t.getId() != null ? t.getId().hashCode() : 0;
|
||||
}
|
||||
}, tableResources);
|
||||
this.userService = userService;
|
||||
|
||||
@@ -249,8 +249,7 @@ Install the GWT Browser Plugin for the GWT Development mode. As of 2016-08-31 Fi
|
||||
|
||||
### Create Hudson Job
|
||||
If you want a hudson job to run when you push your branch then you can run a script in `configuration` called `createHudsonJobForBug.sh`. For you bug branch titled `bug<bug number>`, create a build job, which will create a release, by running the script like so: `./createHudsonJobForBug.sh <bug number>`.
|
||||
If you'd like the script to include the bug's summary in its description, set your BUGZILLA_API_KEY environment variable to an API key you obtain from [https://bugzilla.sapsailing.com/bugzilla/userprefs.cgi?tab=apikey](https://bugzilla.sapsailing.com/bugzilla/userprefs.cgi?tab=apikey) or pass the API key as the second argument, after the bug ID, as in
|
||||
`./configuration/createHudsonJobForBug.sh <bug number> {Bugzilla-API-Key}`
|
||||
The script will include the issue's summary in its description.
|
||||
If on Windows, you may need to disable any web shields in antivirus software, to allow `curl` to function. If on Mac, you may need to install gnu-sed (``gsed``) via Homebrew.
|
||||
|
||||
### Issues when playing around with AWS
|
||||
@@ -263,4 +262,5 @@ Solution: This was occurring because the website didn't have any content in the
|
||||
|
||||
### Extra Reading
|
||||
Check out [refactoring patterns](https://refactoring.guru/) as a sort of cheatsheet for the aforementioned design patterns books.
|
||||
The [Pragmatic Programmer](https://en.wikipedia.org/wiki/The_Pragmatic_Programmer) is a great read too.
|
||||
We also have an [onboarding glossary](https://wiki.sapsailing.com/wiki/howto/glossary).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Setup locally hosted 360° Videos
|
||||
# Setup locally hosted 360°
|
||||
[[_TOC_]]
|
||||
|
||||
## Installing the Webserver
|
||||
@@ -54,7 +54,7 @@ If you are on Linux you can:<br>
|
||||
* Following a refresh in the browser (F5) the video file should appear in the list:
|
||||

|
||||
<br><br>
|
||||
* Click on it to verify it is reachable, note that depending on the browser 360° videos will look distorted.
|
||||
* Click on it to verify it is reachable, note that depending on the browser 360° videos will look distorted.
|
||||

|
||||
<br><br>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user