diff --git a/.travis.yml b/.travis.yml index 5bd5c74e8..3235d3788 100644 --- a/.travis.yml +++ b/.travis.yml @@ -83,7 +83,7 @@ jobs: - cd webapp - npm start & - cd $TRAVIS_BUILD_DIR - - celery -A backend.tasks.celery_worker.celery worker --pool=solo -l debug & + - celery -A backend.tasks.celery_worker.celery worker --pool=solo -l WARNING & # Wait for all other systems to be setup - sleep 30 - pytest tests/integration_tests --cov=./ diff --git a/backend/blueprints/spa_api/errors/errors.py b/backend/blueprints/spa_api/errors/errors.py index 66eb0a428..f4976248a 100644 --- a/backend/blueprints/spa_api/errors/errors.py +++ b/backend/blueprints/spa_api/errors/errors.py @@ -91,6 +91,13 @@ class TagError(CalculatedError): message = "Exception with tags" +class TagKeyError(TagError): + status_code = 400 + + def __init__(self, tag_key, exception): + self.message = f'Unable to decode tag_key; [{tag_key}] ' + str(exception) + + class TagNotFound(TagError): status_code = 404 message = "Tag not found" diff --git a/backend/blueprints/spa_api/service_layers/replay/tag.py b/backend/blueprints/spa_api/service_layers/replay/tag.py index 462a10b97..a15528f82 100644 --- a/backend/blueprints/spa_api/service_layers/replay/tag.py +++ b/backend/blueprints/spa_api/service_layers/replay/tag.py @@ -1,37 +1,41 @@ import base64 +import urllib from typing import List, Dict from backend.blueprints.spa_api.service_layers.utils import with_session -from backend.utils.logging import ErrorLogger -from backend.blueprints.spa_api.errors.errors import TagNotFound, PlayerNotFound, TagError -from backend.database.objects import Tag as DBTag, Player +from backend.blueprints.spa_api.errors.errors import TagNotFound, TagError, TagKeyError +from backend.database.objects import Tag as DBTag from backend.database.wrapper.tag_wrapper import TagWrapper, DBTagNotFound from backend.utils.safe_flask_globals import get_current_user_id class Tag: - def __init__(self, name: str, owner: str, db_tag: DBTag = None): + def __init__(self, name: str, owner: str, privateKey: str = None, db_tag: DBTag = None): super().__init__() self.name = name - self.owner_id = owner + self.ownerId = owner + self.privateKey = privateKey self.db_tag = db_tag def to_JSON(self, with_id=False): if with_id: return { "name": self.name, - "owner_id": self.owner_id, + "ownerId": self.ownerId, + "privateKey": self.privateKey, "tag_id": self.db_tag.id } return { "name": self.name, - "owner_id": self.owner_id + "ownerId": self.ownerId, + "privateKey": self.privateKey, } @staticmethod def create_from_dbtag(tag: DBTag): - return Tag(tag.name, tag.owner, db_tag=tag) + private_key = None if tag.private_id is None else Tag.encode_tag(tag.id, tag.private_id) + return Tag(tag.name, tag.owner, privateKey=private_key, db_tag=tag) @staticmethod @with_session @@ -43,13 +47,13 @@ def add_private_key(name: str, private_key: str, session=None, player_id=None): @staticmethod @with_session - def create(name: str, session=None, player_id=None, private_key=None) -> 'Tag': + def create(name: str, session=None, player_id=None, private_id=None) -> 'Tag': """ Creates a new instance of Tag, add one to the db if it does not exist. :param name: Tag name :param session: Database session :param player_id - :param private_key + :param private_id :return: """ # Check if tag exists @@ -59,7 +63,7 @@ def create(name: str, session=None, player_id=None, private_key=None) -> 'Tag': return tag except DBTagNotFound: pass - dbtag = TagWrapper.create_tag(session, get_current_user_id(player_id=player_id), name, private_key=private_key) + dbtag = TagWrapper.create_tag(session, get_current_user_id(player_id=player_id), name, private_id=private_id) tag = Tag.create_from_dbtag(dbtag) return tag @@ -128,33 +132,30 @@ def get_encoded_private_key(name: str, session=None) -> str: @staticmethod def encode_tag(tag_id: int, private_id: str) -> str: merged = str(tag_id + 1000) + ":" + private_id - return base64.b85encode(merged.encode(encoding="utf-8")).decode('utf-8') + # b85 encodes it to make it smaller / unreadable followed by a url encode to make it friendly for urls. + return urllib.parse.quote(base64.b85encode(merged.encode(encoding="utf-8")).decode('utf-8')) @staticmethod def decode_tag(encoded_key: str): - decoded_key_bytes = base64.b85decode(encoded_key) - decoded_key = decoded_key_bytes.decode(encoding="utf-8") + decoded_key_bytes = base64.b85decode(urllib.parse.unquote(encoded_key).encode('utf-8')) + + try: + decoded_key = decoded_key_bytes.decode(encoding="utf-8") + except UnicodeDecodeError as e: + raise TagKeyError(encoded_key, e) + first_index = decoded_key.find(':') tag_id = int(decoded_key[0: first_index]) - 1000 decoded_private_id = decoded_key[first_index + 1:] return tag_id, decoded_private_id + @with_session -def apply_tags_to_game(query_params: Dict[str, any]=None, game_id=None, session=None): +def apply_tags_to_game(query_params: Dict[str, any] = None, game_id=None, session=None): if query_params is None: return None - if 'tags' not in query_params and 'private_tag_keys' not in query_params: - return None - tags = query_params['tags'] if 'tags' in query_params else [] + private_ids = query_params['private_tag_keys'] if 'private_tag_keys' in query_params else [] - if len(tags) > 0: - player_id = query_params['player_id'] - if session.query(Player).filter(Player.platformid == player_id).first() is None: - ErrorLogger.log_error(PlayerNotFound()) - else: - for tag in tags: - created_tag = Tag.create(tag, session=session, player_id=player_id) - TagWrapper.add_tag_to_game(session, game_id, created_tag.db_tag) for private_id in private_ids: tag_id, private_key = Tag.decode_tag(private_id) diff --git a/backend/blueprints/spa_api/spa_api.py b/backend/blueprints/spa_api/spa_api.py index 6e01cf1e5..896b0ec7c 100644 --- a/backend/blueprints/spa_api/spa_api.py +++ b/backend/blueprints/spa_api/spa_api.py @@ -443,13 +443,13 @@ def api_upload_proto(query_params=None): @bp.route('/tag/', methods=["PUT"]) @require_user @with_query_params(accepted_query_params=[ - QueryParam(name='private_key', type_=str, optional=True) + QueryParam(name='private_id', type_=str, optional=True) ]) def api_create_tag(name: str, query_params=None): - private_key = None - if 'private_key' in query_params: - private_key = query_params['private_key'] - tag = Tag.create(name, private_key=private_key) + private_id = None + if 'private_id' in query_params: + private_id = query_params['private_id'] + tag = Tag.create(name, private_id=private_id) return better_jsonify(tag), 201 diff --git a/backend/blueprints/spa_api/utils/query_param_definitions.py b/backend/blueprints/spa_api/utils/query_param_definitions.py index 6ec13fdab..fdc7cb21f 100644 --- a/backend/blueprints/spa_api/utils/query_param_definitions.py +++ b/backend/blueprints/spa_api/utils/query_param_definitions.py @@ -32,11 +32,9 @@ def convert_string_to_enum(string: str) -> T: ] tag_params = [ - QueryParam(name="tags", optional=True, - type_=str, is_list=True, - required_siblings=['player_id']), QueryParam(name="private_tag_keys", optional=True, - tip='This is base 64 encoded it is not the private key directly.', + tip='This is only required only if you wish to apply a tag that is owned by another account. ' + + 'This is base 64 encoded.', type_=str, is_list=True) ] diff --git a/backend/blueprints/spa_api/utils/query_params_handler.py b/backend/blueprints/spa_api/utils/query_params_handler.py index cc9beb665..7651f8611 100644 --- a/backend/blueprints/spa_api/utils/query_params_handler.py +++ b/backend/blueprints/spa_api/utils/query_params_handler.py @@ -1,5 +1,4 @@ from typing import List, Dict, Any, Callable, Optional, Type -from urllib.parse import urlencode from flask import Request @@ -151,7 +150,3 @@ def validate(created_query_params) -> Optional[CalculatedError]: len(created_query_params[query]), len(created_query_params[sibling_query.name])) return validate - - -def create_query_string(query_params): - return urlencode(query_params) diff --git a/backend/database/wrapper/player_wrapper.py b/backend/database/wrapper/player_wrapper.py index 7fd388747..540942d9d 100644 --- a/backend/database/wrapper/player_wrapper.py +++ b/backend/database/wrapper/player_wrapper.py @@ -23,7 +23,7 @@ def create_default_player(session=None): if player is None: player = Player() - player.platformid = "LOCAL_PLATFORMID" if player is None else player.platformid + player.platformid = "LOCAL_PLATFORMID" if player.platformid is None else player.platformid player.platformname = 'test user with a really long name but even longer' if bool(random.getrandbits(1)): player.avatar = "https://media.istockphoto.com/photos/golden-retriever-puppy-looking-up-isolated-on-black-backround-picture-id466614709?k=6&m=466614709&s=612x612&w=0&h=AVW-4RuYXFPXxLBMHiqoAKnvLrMGT9g62SduH2eNHxA=" diff --git a/backend/database/wrapper/tag_wrapper.py b/backend/database/wrapper/tag_wrapper.py index 40abd5924..fe8a8413a 100644 --- a/backend/database/wrapper/tag_wrapper.py +++ b/backend/database/wrapper/tag_wrapper.py @@ -8,8 +8,8 @@ class TagWrapper: @staticmethod - def create_tag(session, user_id: str, name: str, private_key: str = None) -> Tag: - tag = Tag(name=name, owner=user_id, private_id=private_key) + def create_tag(session, user_id: str, name: str, private_id: str = None) -> Tag: + tag = Tag(name=name, owner=user_id, private_id=private_id) session.add(tag) session.commit() return tag diff --git a/backend/initial_setup.py b/backend/initial_setup.py index 5c6b4daf9..ff8420261 100644 --- a/backend/initial_setup.py +++ b/backend/initial_setup.py @@ -21,6 +21,7 @@ from backend.utils.checks import is_local_dev from backend.utils.metrics import MetricsHandler from backend.utils.logging import ErrorLogger +from backend.utils.safe_flask_globals import UserManager logger = logging.getLogger(__name__) logger.info("Setting up server.") diff --git a/requirements.txt b/requirements.txt index 5b579e749..543535583 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ kiwisolver==1.0.1 kombu==4.4 MarkupSafe==1.0 matplotlib==2.2.3 -numpy==1.15.1 +numpy==1.17.0 pandas==0.24.2 proj==0.1.0 protobuf==3.6.1 diff --git a/tests/integration_tests/no_react/heatmaps_test.py b/tests/integration_tests/no_react/heatmaps_test.py index 3318ab822..c0feb339f 100644 --- a/tests/integration_tests/no_react/heatmaps_test.py +++ b/tests/integration_tests/no_react/heatmaps_test.py @@ -1,4 +1,5 @@ import json +import logging import time import requests @@ -15,6 +16,7 @@ class Test_Heatmaps: @classmethod def setup_class(cls): + logging.basicConfig(level=logging.ERROR) cls.thread = KillableThread(target=start_server) cls.thread.daemon = True cls.thread.start() diff --git a/tests/integration_tests/no_react/test_upload.py b/tests/integration_tests/no_react/test_upload.py index e17b3f1c3..7631505e7 100644 --- a/tests/integration_tests/no_react/test_upload.py +++ b/tests/integration_tests/no_react/test_upload.py @@ -5,8 +5,7 @@ import requests from RLBotServer import start_server -from backend.database.objects import GameVisibilitySetting, User, Player -from backend.blueprints.spa_api.errors.errors import PlayerNotFound +from backend.database.objects import GameVisibilitySetting, Player from tests.utils.killable_thread import KillableThread from tests.utils.replay_utils import get_complex_replay_list, download_replay_discord @@ -23,6 +22,8 @@ class Test_BasicServerCommands(): @classmethod def setup_class(cls): + logging.basicConfig(level=logging.ERROR) + logger.setLevel(logging.DEBUG) cls.thread = KillableThread(target=start_server) cls.thread.daemon = True cls.thread.start() @@ -37,21 +38,47 @@ def test_upload_files(self, mock_user, no_errors_are_logged): replay_list = get_complex_replay_list()[0:4] - tags = ['TAG1', 'TAG2', 'TAG3', ['TAG4', 'TAG2']] + tags = [['TAG1'], ['TAG2'], ['TAG3'], ['TAG4', 'TAG2']] privacy = [GameVisibilitySetting.DEFAULT.name, GameVisibilitySetting.PUBLIC.name, GameVisibilitySetting.PRIVATE.name, GameVisibilitySetting.PRIVATE.name] users = [ - 'invalid', '76561198018756583', + 'invalid', '76561198018756583', '76561198018756583' ] + tag_keys = ['invalid_key'] + + def create_all_tags(): + created_tags = [] + for _tags in tags: + keys = [] + for _tag in _tags: + if _tag not in created_tags: + r = requests.put(LOCAL_URL + f'/api/tag/{_tag}') + r.raise_for_status() + created_tags.append(_tag) + + # create private id + r = requests.put(LOCAL_URL + f'/api/tag/{_tag}/private_key/{_tag}') + r.raise_for_status() + json = requests.get(LOCAL_URL + f'/api/tag/{_tag}/private_key').json() + keys.append(json) + tag_keys.append(keys) + for index, replay_url in enumerate(replay_list): - params = {'tags': tags[index], 'visibility': privacy[index], 'player_id': users[index]} - logger.debug('Testing:', replay_url) + if index == 1: + logger.error("CREATING INITIAL TAG DATA") + time.sleep(20) + create_all_tags() + time.sleep(1) + + params = {'visibility': privacy[index], 'player_id': users[index], 'private_tag_keys': tag_keys[index]} + logger.debug('TESTING URL:' + str(replay_url)) + logger.debug('TESTING PARAMS:' + str(params)) f = download_replay_discord(replay_url) r = requests.post(LOCAL_URL + '/api/upload', files={'replays': ('fake_file.replay', f)}, params=params) r.raise_for_status() @@ -68,11 +95,10 @@ def test_upload_files(self, mock_user, no_errors_are_logged): assert(int(result) == len(replay_list)) response = requests.get(LOCAL_URL + '/api/tag') - result = json.loads(response.content) - assert result[0]['owner_id'] == "76561198018756583" + assert result[0]['ownerId'] == "76561198018756583" assert result[0]['name'].startswith('TAG') - assert len(result) == 3 + assert len(result) == 4 response = requests.get(LOCAL_URL + '/api/player/76561198018756583/match_history?page=0&limit=10') assert response.status_code == 200 diff --git a/tests/integration_tests/no_react/upload_proto_test.py b/tests/integration_tests/no_react/upload_proto_test.py index 54191db18..8f4dd77c8 100644 --- a/tests/integration_tests/no_react/upload_proto_test.py +++ b/tests/integration_tests/no_react/upload_proto_test.py @@ -1,5 +1,6 @@ import base64 import json +import logging import time import zlib @@ -17,6 +18,7 @@ class Test_UploadingProtos(): @classmethod def setup_class(cls): + logging.basicConfig(level=logging.ERROR) cls.thread = KillableThread(target=start_server) cls.thread.daemon = True cls.thread.start() @@ -37,9 +39,9 @@ def test_upload_proto(self): 'proto': encoded_proto, 'pandas': encoded_pandas } - r = requests.post(LOCAL_URL + '/api/upload/proto', json=obj, params={'tags': ['TAG'], - 'visibility': GameVisibilitySetting.PRIVATE.name, - 'player_id': proto_game.players[0].id.id}) + r = requests.post(LOCAL_URL + '/api/upload/proto', json=obj, + params={'visibility': GameVisibilitySetting.PRIVATE.name, + 'player_id': proto_game.players[0].id.id}) r.raise_for_status() assert r.status_code == 200 @@ -51,9 +53,11 @@ def test_upload_proto(self): response = requests.get(LOCAL_URL + '/api/tag') result = json.loads(response.content) - assert result[0]['owner_id'] == "76561198018756583" - assert result[0]['name'].startswith('TAG') - assert len(result) == 1 + + # TODO: Readd test for tag using private key + # assert result[0]['ownerId'] == "76561198018756583" + # assert result[0]['name'].startswith('TAG') + # assert len(result) == 1 response = requests.get(LOCAL_URL + '/api/player/76561198018756583/match_history?page=0&limit=10') assert response.status_code == 200 diff --git a/tests/server_tests/api_tests/upload/tag_upload_test.py b/tests/server_tests/api_tests/upload/tag_upload_test.py index fae852148..a2477c0c8 100644 --- a/tests/server_tests/api_tests/upload/tag_upload_test.py +++ b/tests/server_tests/api_tests/upload/tag_upload_test.py @@ -3,6 +3,7 @@ import urllib import zlib +import pytest import responses from requests import Request from backend.blueprints.spa_api.service_layers.replay.tag import Tag as ServiceTag @@ -27,6 +28,7 @@ def setup_method(self): self.file = f self.stream = io.BytesIO(self.file) + @pytest.mark.skip(reason="tag names are disabled") def test_replay_basic_server_upload_with_tag(self, test_client): fake_session = get_current_session() game = fake_session.query(Game).first() @@ -52,7 +54,7 @@ def test_replay_basic_server_upload_with_tag(self, test_client): player = fake_session.query(Player.platformid == '76561198018756583').first() assert(player is not None) - + @pytest.mark.skip(reason="tag names are disabled") @responses.activate def test_replay_basic_server_upload_with_tags_gcp(self, test_client, gcp): responses.add(responses.POST, gcp.get_url()) @@ -76,6 +78,7 @@ def test_replay_basic_server_upload_with_tags_gcp(self, test_client, gcp): assert query_result['tags'] == [TAG_NAME, TAG_NAME + "hello"] assert query_result['uuid'] is not None + @pytest.mark.skip(reason="tag names are disabled") def test_replay_basic_server_upload_with_multiple_tags(self, test_client): fake_session = get_current_session() game = fake_session.query(Game).first() @@ -108,6 +111,7 @@ def test_replay_basic_server_upload_with_multiple_tags(self, test_client): assert response.status_code == 200 assert len(response.json['replays']) == 1 + @pytest.mark.skip(reason="tag names are disabled") def test_replay_basic_server_upload_with_duplicate_tags(self, test_client): fake_session = get_current_session() game = fake_session.query(Game).first() @@ -133,6 +137,7 @@ def test_replay_basic_server_upload_with_duplicate_tags(self, test_client): player = fake_session.query(Player.platformid == '76561198018756583').first() assert(player is not None) + @pytest.mark.skip(reason="tag names are disabled") def test_replay_basic_server_upload_tag_replay_no_player(self, test_client): params = {'tags': TAG_NAME} r = Request('POST', LOCAL_URL + '/api/upload', @@ -142,9 +147,12 @@ def test_replay_basic_server_upload_tag_replay_no_player(self, test_client): assert(response.status_code == 400) - def test_tag_creation_private_key(self, test_client, mock_user): + def test_tag_creation_private_id(self, test_client, mock_user): + fake_private_id = 'fake_private_id' + fake_session = get_current_session() - params = {'private_key': 'fake_private_key'} + + params = {'private_id': fake_private_id} r = Request('PUT', LOCAL_URL + '/api/tag/TAG', params=params) @@ -153,35 +161,45 @@ def test_tag_creation_private_key(self, test_client, mock_user): assert(response.status_code == 201) data = response.json assert data['name'] == 'TAG' - assert data['owner_id'] == default_player_id() + assert data['ownerId'] == default_player_id() tag = fake_session.query(Tag).first() assert tag.name == 'TAG' assert tag.owner == default_player_id() - assert tag.private_id == 'fake_private_key' + assert tag.private_id == fake_private_id r = Request('GET', LOCAL_URL + '/api/tag/TAG/private_key') response = test_client.send(r) assert(response.status_code == 200) - assert response.json == ServiceTag.encode_tag(tag.id, 'fake_private_key') + assert response.json == ServiceTag.encode_tag(tag.id, fake_private_id) + + def test_tag_encodes_decodes_correctly(self): + str1 = 150 + str2 = "World" + encoded = ServiceTag.encode_tag(str1, str2) + out1, out2 = ServiceTag.decode_tag(encoded) + assert out1 == str1 + assert out2 == str2 def test_tag_modification_with_private_key(self, test_client, mock_user): fake_session = get_current_session() + + # Create tag r = Request('PUT', LOCAL_URL + '/api/tag/TAG') response = test_client.send(r) assert(response.status_code == 201) data = response.json assert data['name'] == 'TAG' - assert data['owner_id'] == default_player_id() + assert data['ownerId'] == default_player_id() tag = fake_session.query(Tag).first() assert tag.name == 'TAG' assert tag.owner == default_player_id() assert tag.private_id is None - r = Request('PUT', LOCAL_URL + '/api/tag/TAG/private_key/fake_private_key') + r = Request('PUT', LOCAL_URL + '/api/tag/TAG/private_key/fake_private_id') response = test_client.send(r) assert(response.status_code == 204) @@ -191,13 +209,13 @@ def test_tag_modification_with_private_key(self, test_client, mock_user): tag = fake_session.query(Tag).first() assert tag.name == 'TAG' assert tag.owner == default_player_id() - assert tag.private_id == 'fake_private_key' + assert tag.private_id == 'fake_private_id' r = Request('GET', LOCAL_URL + '/api/tag/TAG/private_key') response = test_client.send(r) assert(response.status_code == 200) - assert response.json == ServiceTag.encode_tag(tag.id, 'fake_private_key') + assert response.json == ServiceTag.encode_tag(tag.id, 'fake_private_id') def test_tag_creation_no_private_key(self, test_client, mock_user): fake_session = get_current_session() @@ -208,7 +226,7 @@ def test_tag_creation_no_private_key(self, test_client, mock_user): assert(response.status_code == 201) data = response.json assert data['name'] == 'TAG' - assert data['owner_id'] == default_player_id() + assert data['ownerId'] == default_player_id() tag = fake_session.query(Tag).first() assert tag.name == 'TAG' @@ -225,7 +243,7 @@ def test_replay_basic_server_upload_with_private_tags(self, test_client, mock_us game = fake_session.query(Game).first() assert game is None - params = {'private_key': 'fake_private_key'} + params = {'private_id': 'fake_private_id'} r = Request('PUT', LOCAL_URL + '/api/tag/' + TAG_NAME, params=params) response = test_client.send(r) @@ -257,13 +275,13 @@ def test_replay_basic_server_upload_with_private_tags(self, test_client, mock_us assert(game.tags[0].name == TAG_NAME) assert(game.tags[0].owner == default_player_id()) assert(game.tags[0].games[0] == game) - assert(game.tags[0].private_id == 'fake_private_key') + assert(game.tags[0].private_id == 'fake_private_id') player = fake_session.query(Player.platformid == '76561198018756583').first() assert(player is not None) + @pytest.mark.skip(reason="tag names are disabled") def test_proto_upload_with_tags(self, test_client): - proto, pandas, proto_game = write_proto_pandas_to_file(get_test_file(get_complex_replay_list()[0], is_replay=True)) @@ -304,3 +322,24 @@ def test_key_encode_decode(self): test_id, test_key = ServiceTag.decode_tag(encoded) assert test_id == id assert test_key == keys[index] + + def test_tag_creation_encoding(self, test_client): + + tags = [['TAG1'], ['TAG2'], ['TAG3'], ['TAG4', 'TAG2']] + + tag_keys = ['invalid_key'] + created_tags = [] + for _tags in tags: + keys = [] + for _tag in _tags: + if _tag not in created_tags: + r = test_client.send(Request('PUT', LOCAL_URL + f'/api/tag/{_tag}')) + assert r.status_code == 201 + created_tags.append(_tag) + + # create private id + r = test_client.send(Request('PUT', LOCAL_URL + f'/api/tag/{_tag}/private_key/{_tag}')) + assert r.status_code == 204 + json = test_client.send(Request('GET', LOCAL_URL + f'/api/tag/{_tag}/private_key')).json + keys.append(json) + tag_keys.append(keys) diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 32543a544..a8701d9b2 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -16,6 +16,7 @@ import { ReplayPage } from "./Components/Pages/ReplayPage" import { ReplaysGroupPage } from "./Components/Pages/ReplaysGroupPage" import { ReplaysSearchPage } from "./Components/Pages/ReplaysSearchPage" import { StatusPage } from "./Components/Pages/StatusPage" +import { TagsPage } from "./Components/Pages/TagsPage" import { TrainingPackPage } from "./Components/Pages/TrainingPackPage" import { UploadPage } from "./Components/Pages/UploadPage" import { Notifications } from "./Components/Shared/Notification/Notifications" @@ -29,7 +30,8 @@ import { REPLAY_PAGE_LINK, REPLAYS_GROUP_PAGE_LINK, REPLAYS_SEARCH_PAGE_LINK, - STATUS_PAGE_LINK, TRAINING_LINK, + STATUS_PAGE_LINK, TAGS_PAGE_LINK, + TRAINING_LINK, UPLOAD_LINK } from "./Globals" @@ -71,6 +73,7 @@ class AppComponent extends React.Component { + {/*Redirect unknowns to root*/} diff --git a/webapp/src/Components/Home/HomePageAppBar.tsx b/webapp/src/Components/Home/HomePageAppBar.tsx index e922fc276..a31f17211 100644 --- a/webapp/src/Components/Home/HomePageAppBar.tsx +++ b/webapp/src/Components/Home/HomePageAppBar.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { AppBar, IconButton, Toolbar, Tooltip } from "@material-ui/core" import Menu from "@material-ui/icons/Menu" import * as React from "react" -import { ThemeContext } from "../../Theme" +import { ThemeContext } from "../../Contexts/ThemeContext" interface Props { toggleSideBar: () => void diff --git a/webapp/src/Components/Pages/TagsPage.tsx b/webapp/src/Components/Pages/TagsPage.tsx new file mode 100644 index 000000000..db2f104c5 --- /dev/null +++ b/webapp/src/Components/Pages/TagsPage.tsx @@ -0,0 +1,51 @@ +import { Grid, List, Paper } from "@material-ui/core" +import * as React from "react" +import { connect } from "react-redux" +import { Dispatch } from "redux" +import { StoreState, TagsAction } from "../../Redux" +import { getAllTags } from "../../Requests/Tag" +import { TagPageListItem } from "../Shared/Tag/TagPageListItem" +import { BasePage } from "./BasePage" + +const mapStateToProps = (state: StoreState) => ({ + tags: state.tags +}) + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + setTags: (tags: Tag[]) => dispatch(TagsAction.setTagsAction(tags)) +}) + +type Props = ReturnType + & ReturnType + +class TagsPageComponent extends React.PureComponent { + public componentDidMount() { + getAllTags() + .then((tags) => this.props.setTags(tags)) + } + + public render() { + const {tags} = this.props + return ( + + + + {tags !== null && + + + + {tags.map((tag) => ( + + ))} + + + + } + + + + ) + } +} + +export const TagsPage = connect(mapStateToProps, mapDispatchToProps)(TagsPageComponent) diff --git a/webapp/src/Components/Shared/NavBar/NavBar.tsx b/webapp/src/Components/Shared/NavBar/NavBar.tsx index ddcbc8c29..2a6ec4c79 100644 --- a/webapp/src/Components/Shared/NavBar/NavBar.tsx +++ b/webapp/src/Components/Shared/NavBar/NavBar.tsx @@ -19,10 +19,10 @@ import * as React from "react" import { connect } from "react-redux" import { Link } from "react-router-dom" import { Dispatch } from "redux" +import { ThemeContext } from "../../../Contexts/ThemeContext" import { GLOBAL_STATS_LINK } from "../../../Globals" import { LoggedInUserActions, StoreState } from "../../../Redux" import { getLoggedInUser } from "../../../Requests/Global" -import { ThemeContext } from "../../../Theme" import { Logo } from "../Logo/Logo" import { Search } from "../Search" import { UploadDialogWrapper } from "../Upload/UploadDialogWrapper" diff --git a/webapp/src/Components/Shared/SideBar.tsx b/webapp/src/Components/Shared/SideBar.tsx index 051666714..d9ca1f45f 100644 --- a/webapp/src/Components/Shared/SideBar.tsx +++ b/webapp/src/Components/Shared/SideBar.tsx @@ -11,6 +11,7 @@ import ShowChart from "@material-ui/icons/ShowChart" import TableChart from "@material-ui/icons/TableChart" import * as React from "react" import { Link } from "react-router-dom" +import { ThemeContext } from "../../Contexts/ThemeContext" import { ABOUT_LINK, EXPLANATIONS_LINK, @@ -23,7 +24,6 @@ import { REPLAYS_SEARCH_PAGE_LINK, UPLOAD_LINK } from "../../Globals" -import { ThemeContext } from "../../Theme" interface Props { open: boolean diff --git a/webapp/src/Components/Shared/Tag/TagDialogWrapper.tsx b/webapp/src/Components/Shared/Tag/TagDialogWrapper.tsx index fa1c7f8ba..921544ed9 100644 --- a/webapp/src/Components/Shared/Tag/TagDialogWrapper.tsx +++ b/webapp/src/Components/Shared/Tag/TagDialogWrapper.tsx @@ -2,7 +2,7 @@ import { faTags } from "@fortawesome/free-solid-svg-icons" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { createStyles, IconButton, Tooltip, WithStyles, withStyles } from "@material-ui/core" import * as React from "react" -import { Replay } from "../../../Models/Replay/Replay" +import { Replay } from "../../../Models" import { TagDialog } from "./TagDialog" const styles = createStyles({ diff --git a/webapp/src/Components/Shared/Tag/TagPageListItem.tsx b/webapp/src/Components/Shared/Tag/TagPageListItem.tsx new file mode 100644 index 000000000..06efba583 --- /dev/null +++ b/webapp/src/Components/Shared/Tag/TagPageListItem.tsx @@ -0,0 +1,77 @@ +import { faTags } from "@fortawesome/free-solid-svg-icons/faTags" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { IconButton, ListItem, ListItemIcon, ListItemSecondaryAction, ListItemText, Tooltip } from "@material-ui/core" +import Edit from "@material-ui/icons/Edit" +import FileCopy from "@material-ui/icons/FileCopy" +import Refresh from "@material-ui/icons/Refresh" +import * as React from "react" +import { connect } from "react-redux" +import { Dispatch } from "redux" +import { TagsAction } from "../../../Redux" +import { generateTagPrivateIdAndGetKey } from "../../../Requests/Tag" + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + addPrivateKeyToTagAction: (tag: Tag) => dispatch(TagsAction.addPrivateKeyToTagAction(tag)) +}) + +interface OwnProps { + tag: Tag +} + +type Props = OwnProps + & ReturnType + +class TagPageListItemComponent extends React.PureComponent { + public render() { + const tag = this.props.tag + const hasPrivateKey = tag.privateKey !== null + return ( + + + + + + + {hasPrivateKey && ( + + + + + + )} + + + + + + + + + {/*TODO: Enable tag rename functionality*/} + + + ) + } + + private readonly generatePrivateID = () => { + const tag = this.props.tag + generateTagPrivateIdAndGetKey(tag) + .then((privateKey) => ({...tag, privateKey})) + .then(this.props.addPrivateKeyToTagAction) + } + + private readonly copyPrivateKeyToClipboard = () => { + const tag = this.props.tag + + if (tag.privateKey !== null) { + (navigator as any).clipboard.writeText(tag.privateKey) + } + } +} + +export const TagPageListItem = connect(null, mapDispatchToProps)(TagPageListItemComponent) diff --git a/webapp/src/Components/Shared/Upload/AddTagPrivateKeyDialog..tsx b/webapp/src/Components/Shared/Upload/AddTagPrivateKeyDialog..tsx new file mode 100644 index 000000000..e986a4707 --- /dev/null +++ b/webapp/src/Components/Shared/Upload/AddTagPrivateKeyDialog..tsx @@ -0,0 +1,73 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + TextField +} from "@material-ui/core" +import * as React from "react" +import { Link } from "react-router-dom" +import { TAGS_PAGE_LINK } from "../../../Globals" + +interface Props { + open: boolean + toggleExternalKeyDialog: () => void + addExternalPrivateKey: (privateKey: string) => void +} + +interface State { + enteredExternalPrivateKey: string +} + +export class AddTagPrivateKeyDialog extends React.PureComponent { + constructor(props: Props) { + super(props) + this.state = {enteredExternalPrivateKey: ""} + } + + public render() { + return ( + + Add Tag by Private Key + + + Add private keys to upload replays to a tag belonging to another user. + {" Private keys can be found at the "} + { + tags page + } + . + + + + + + + + ) + } + + private readonly handleExternalPrivateKeyChange: React.ChangeEventHandler = (event) => { + this.setState({enteredExternalPrivateKey: event.target.value}) + } + + private readonly addPrivateKey = () => { + this.setState({enteredExternalPrivateKey: ""}) + this.props.addExternalPrivateKey(this.state.enteredExternalPrivateKey) + } +} diff --git a/webapp/src/Components/Shared/Upload/UploadFloatingButton.tsx b/webapp/src/Components/Shared/Upload/UploadFloatingButton.tsx index 0a7a87fec..fa563e4ca 100644 --- a/webapp/src/Components/Shared/Upload/UploadFloatingButton.tsx +++ b/webapp/src/Components/Shared/Upload/UploadFloatingButton.tsx @@ -1,4 +1,4 @@ -import { Button, Tooltip } from "@material-ui/core" +import { Fab, Tooltip } from "@material-ui/core" import { SvgIconProps } from "@material-ui/core/SvgIcon" import * as React from "react" @@ -17,10 +17,10 @@ export class UploadFloatingButton extends React.PureComponent { } return ( - + ) } diff --git a/webapp/src/Components/Shared/Upload/UploadForm.tsx b/webapp/src/Components/Shared/Upload/UploadForm.tsx index d13837e0b..c20a567c7 100644 --- a/webapp/src/Components/Shared/Upload/UploadForm.tsx +++ b/webapp/src/Components/Shared/Upload/UploadForm.tsx @@ -18,6 +18,7 @@ import { WithNotifications, withNotifications } from "../Notification/Notificati import { BakkesModAd } from "./BakkesModAd" import { addTaskIds } from "./StatusUtils" import { UploadDropzone } from "./UploadDropzone" +import { UploadTags } from "./UploadTags" const styles = (theme: Theme) => createStyles({ leftIcon: { @@ -35,13 +36,14 @@ interface State { files: File[] rejected: File[] uploadingStage?: "pressedUpload" | "uploaded" + selectedPrivateKeys: string[] filesRemaining: number } class UploadFormComponent extends React.PureComponent { constructor(props: Props) { super(props) - this.state = {files: [], rejected: [], filesRemaining: -1} + this.state = {files: [], rejected: [], selectedPrivateKeys: [], filesRemaining: -1} } public render() { @@ -60,6 +62,10 @@ class UploadFormComponent extends React.PureComponent { ".replay". } + +