From 1ee2412b59bd9aa2efb581357a6abb95dac71c99 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Tue, 3 Dec 2024 08:18:48 +0300 Subject: [PATCH 01/65] OS-8018. Not skip cleaning events and resources if profiling token missing --- docker_images/cleanmongodb/clean-mongo-db.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docker_images/cleanmongodb/clean-mongo-db.py b/docker_images/cleanmongodb/clean-mongo-db.py index ee3bc053..36e37be6 100644 --- a/docker_images/cleanmongodb/clean-mongo-db.py +++ b/docker_images/cleanmongodb/clean-mongo-db.py @@ -318,9 +318,6 @@ def split_chunk_by_files(self, chunk, available_rows_count, filename, return result def _delete_by_organization(self, org_id, token, infra_token): - if not token: - self.update_cleaned_at(organization_id=org_id) - return keeper_collections = [ self.mongo_client.keeper.event ] @@ -342,6 +339,10 @@ def _delete_by_organization(self, org_id, token, infra_token): for collection in restapi_collections: self.limits[collection] = self.delete_in_chunks( collection, 'organization_id', org_id) + + if not token: + self.update_cleaned_at(organization_id=org_id) + return for collection in arcee_collections: self.limits[collection] = self.delete_in_chunks( collection, 'token', token) From b354beddf03930e5ec86fb683c005266c060657b Mon Sep 17 00:00:00 2001 From: ek-hystax <33006768+ek-hystax@users.noreply.github.com> Date: Tue, 3 Dec 2024 09:21:28 +0400 Subject: [PATCH 02/65] OS-8036. Add check for BOOK_ENVIRONMENTS for environment booking action --- .../ui/src/components/EnvironmentsTable/EnvironmentsTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ngui/ui/src/components/EnvironmentsTable/EnvironmentsTable.tsx b/ngui/ui/src/components/EnvironmentsTable/EnvironmentsTable.tsx index 23e90cfa..95634c70 100644 --- a/ngui/ui/src/components/EnvironmentsTable/EnvironmentsTable.tsx +++ b/ngui/ui/src/components/EnvironmentsTable/EnvironmentsTable.tsx @@ -169,8 +169,8 @@ const EnvironmentsTable = ({ data, onUpdateActivity, entityId, isLoadingProps = isEnvironmentAvailable }); }, - dataTestId: `btn_book_${index}` - // requiredActions: ["BOOK_ENVIRONMENTS"] + dataTestId: `btn_book_${index}`, + requiredActions: ["BOOK_ENVIRONMENTS"] }); const getReleaseAction = (activeBooking, index) => ({ From 092a9bc561f2442758fbe13cefd2dfb522e3467f Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Tue, 3 Dec 2024 08:36:33 +0300 Subject: [PATCH 03/65] OS-3172. Improved env_properties messages in slack --- .../message_templates/env_alerts.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/slacker/slacker_server/message_templates/env_alerts.py b/slacker/slacker_server/message_templates/env_alerts.py index 0805ed6a..afc2774f 100644 --- a/slacker/slacker_server/message_templates/env_alerts.py +++ b/slacker/slacker_server/message_templates/env_alerts.py @@ -134,15 +134,16 @@ def get_message_acquired(resource_id, resource_name, public_ip, org_id, def get_env_property_updated_block(env_properties, public_ip, resource_id): env_properties_blocks = [] for prop in env_properties[:5]: - env_properties_blocks.append({ + env_properties_blocks.extend([{ "type": "section", "text": { "type": "mrkdwn", - "text": f"*Name:*\n{prop['name']}\n" - f"*Previous value:* \n{prop['previous_value']}\n" - f"*New value:*\n{prop['new_value']}" - } - }) + "text": f"*Property name:* {prop['name']}\n" + f"*Previous value:* {prop['previous_value'] or '-'}\n" + f"*New value:* {prop['new_value'] or '-'}" + }}, + {"type": "divider"} + ]) if len(env_properties) > 5: env_properties_blocks.append( { @@ -172,7 +173,7 @@ def get_current_env_property_block(curr_env_properties): text = '' for prop_name, value in curr_env_properties.items(): - text = text + f"*{prop_name}:*\n{value}\n" + text = text + f"*{prop_name}:* {value}\n" if not text: text = '-' From f29e46bf80bb704938c2c000c70ac97d59dbfdeb Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Tue, 3 Dec 2024 11:53:21 +0300 Subject: [PATCH 04/65] Feature/notification settings Co-authored-by: ek-hystax <33006768+ek-hystax@users.noreply.github.com> --- docker_images/herald_executor/worker.py | 127 +++++--- docker_images/webhook_executor/worker.py | 2 +- ...d604_checking_email_settings_task_state.py | 76 +++++ katara/katara_service/migrate.py | 2 +- katara/katara_service/models/models.py | 1 + katara/katara_worker/consts.py | 1 + .../katara_worker/reports_generators/base.py | 15 + .../organization_expenses.py | 4 +- .../reports_generators/pool_limit_exceed.py | 2 +- .../pool_limit_exceed_resources.py | 6 +- .../violated_constraints.py | 2 +- .../violated_constraints_diff.py | 2 +- katara/katara_worker/tasks.py | 44 ++- katara/katara_worker/transitions.py | 2 + ngui/server/api/restapi/client.ts | 46 +++ .../graphql/resolvers/restapi.generated.ts | 64 ++++ ngui/server/graphql/resolvers/restapi.ts | 13 + ngui/server/graphql/schemas/restapi.graphql | 32 ++ .../components/Accordion/Accordion.styles.ts | 54 +++- .../ui/src/components/Accordion/Accordion.tsx | 15 +- .../UserEmailNotificationSettings.tsx | 298 ++++++++++++++++++ .../UserEmailNotificationSettings/index.ts | 3 + .../UserEmailNotificationSettings/types.ts | 56 ++++ ...UserEmailNotificationSettingsContainer.tsx | 21 ++ .../index.ts | 3 + .../api/restapi/queries/restapi.queries.ts | 38 ++- ngui/ui/src/pages/Settings/Settings.tsx | 11 +- ngui/ui/src/theme.ts | 10 +- ngui/ui/src/translations/en-US/app.json | 39 +++ optscale_client/rest_api_client/client_v2.py | 15 + .../versions/a1d0494e9815_employee_emails.py | 105 ++++++ rest_api/rest_api_server/constants.py | 4 + .../rest_api_server/controllers/context.py | 12 +- .../rest_api_server/controllers/employee.py | 31 +- .../controllers/employee_email.py | 142 +++++++++ .../rest_api_server/controllers/invite.py | 35 +- .../rest_api_server/handlers/v2/__init__.py | 1 + .../handlers/v2/employee_emails.py | 179 +++++++++++ rest_api/rest_api_server/models/models.py | 27 ++ rest_api/rest_api_server/server.py | 6 + .../tests/unittests/test_employee_email.py | 173 ++++++++++ .../tests/unittests/test_pools.py | 23 +- 42 files changed, 1633 insertions(+), 109 deletions(-) create mode 100644 katara/katara_service/alembic/versions/f15bef09d604_checking_email_settings_task_state.py create mode 100644 ngui/ui/src/components/UserEmailNotificationSettings/UserEmailNotificationSettings.tsx create mode 100644 ngui/ui/src/components/UserEmailNotificationSettings/index.ts create mode 100644 ngui/ui/src/components/UserEmailNotificationSettings/types.ts create mode 100644 ngui/ui/src/containers/UserEmailNotificationSettingsContainer/UserEmailNotificationSettingsContainer.tsx create mode 100644 ngui/ui/src/containers/UserEmailNotificationSettingsContainer/index.ts create mode 100644 rest_api/rest_api_server/alembic/versions/a1d0494e9815_employee_emails.py create mode 100644 rest_api/rest_api_server/controllers/employee_email.py create mode 100644 rest_api/rest_api_server/handlers/v2/employee_emails.py create mode 100644 rest_api/rest_api_server/tests/unittests/test_employee_email.py diff --git a/docker_images/herald_executor/worker.py b/docker_images/herald_executor/worker.py index 4dbc49bf..b94451b3 100644 --- a/docker_images/herald_executor/worker.py +++ b/docker_images/herald_executor/worker.py @@ -13,12 +13,11 @@ from kombu.utils.debug import setup_logging from kombu import Exchange, Queue, binding import urllib3 - +from currency_symbols.currency_symbols import CURRENCY_SYMBOLS_MAP from optscale_client.config_client.client import Client as ConfigClient from optscale_client.rest_api_client.client_v2 import Client as RestClient from optscale_client.herald_client.client_v2 import Client as HeraldClient from optscale_client.auth_client.client_v2 import Client as AuthClient -from currency_symbols.currency_symbols import CURRENCY_SYMBOLS_MAP from tools.optscale_time import utcnow_timestamp, utcfromtimestamp LOG = get_logger(__name__) @@ -76,6 +75,16 @@ class HeraldTemplates(Enum): FIRST_RUN_STARTED = 'first_run_started' +CONSTRAINT_TYPE_TEMPLATE_MAP = { + 'expense_anomaly': HeraldTemplates.ANOMALY_DETECTION.value, + 'resource_count_anomaly': HeraldTemplates.ANOMALY_DETECTION.value, + 'expiring_budget': HeraldTemplates.EXPIRING_BUDGET.value, + 'recurring_budget': HeraldTemplates.RECURRING_BUDGET.value, + 'resource_quota': HeraldTemplates.RESOURCE_QUOTA.value, + 'tagging_policy': HeraldTemplates.TAGGING_POLICY.value +} + + class HeraldExecutorWorker(ConsumerMixin): def __init__(self, connection, config_cl): self.connection = connection @@ -118,32 +127,39 @@ def get_consumers(self, consumer, channel): return [consumer(queues=[TASK_QUEUE], accept=['json'], callbacks=[self.process_task], prefetch_count=10)] - def get_auth_users(self, user_ids): - _, response = self.auth_cl.user_list(user_ids) - return response - - def get_owner_manager_infos(self, organization_id, - tenant_auth_user_ids=None): - auth_users = [] - if tenant_auth_user_ids: - auth_users = self.get_auth_users(tenant_auth_user_ids) - all_user_info = {auth_user['id']: { - 'display_name': auth_user.get('display_name'), - 'email': auth_user.get('email') - } for auth_user in auth_users} - - _, org_managers = self.auth_cl.user_roles_get( - scope_ids=[organization_id], - role_purposes=[MANAGER_ROLE]) - for manager in org_managers: - user_id = manager['user_id'] - if not tenant_auth_user_ids or user_id not in tenant_auth_user_ids: + def get_owner_manager_infos( + self, organization_id, tenant_auth_user_ids=None, + email_template=None): + _, employees = self.rest_cl.employee_list(organization_id) + _, user_roles = self.auth_cl.user_roles_get( + scope_ids=[organization_id], + user_ids=[x['auth_user_id'] for x in employees['employees']] + ) + all_user_info = {} + for user_role in user_roles: + user_id = user_role['user_id'] + if (user_role['role_purpose'] == MANAGER_ROLE or + tenant_auth_user_ids and user_id in tenant_auth_user_ids): all_user_info[user_id] = { - 'display_name': manager.get('user_display_name'), - 'email': manager.get('user_email') + 'display_name': user_role.get('user_display_name'), + 'email': user_role.get('user_email') } + if email_template: + for employee in employees['employees']: + auth_user_id = employee['auth_user_id'] + if (auth_user_id in all_user_info and + not self.is_email_enabled(employee['id'], + email_template)): + all_user_info.pop(auth_user_id, None) return all_user_info + def is_email_enabled(self, employee_id, email_template): + _, employee_emails = self.rest_cl.employee_emails_get( + employee_id, email_template) + employee_email = employee_emails.get('employee_emails') + if employee_email: + return employee_email[0]['enabled'] + def _get_service_emails(self): return self.config_cl.optscale_email_recipient() @@ -217,7 +233,7 @@ def format_remained_time(start_date, end_date): shareable_booking_data = self._filter_bookings( shareable_bookings.get('data', []), resource_id, now_ts) for booking in shareable_booking_data: - acquired_by_id = booking.get('acquired_by_id') + acquired_by_id = booking.get('acquired_by', {}).get('id') if acquired_by_id: resource_tenant_ids.append(acquired_by_id) _, employees = self.rest_cl.employee_list(org_id=organization_id) @@ -227,11 +243,10 @@ def format_remained_time(start_date, end_date): tenant_auth_user_ids = [ emp['auth_user_id'] for emp in list(employee_id_map.values()) ] - for booking in shareable_booking_data: acquired_since = booking['acquired_since'] released_at = booking['released_at'] - acquired_by_id = booking.get('acquired_by_id') + acquired_by_id = booking.get('acquired_by', {}).get('id') utc_acquired_since = int( utcfromtimestamp(acquired_since).timestamp()) utc_released_at = int( @@ -272,7 +287,8 @@ def format_remained_time(start_date, end_date): }}) all_user_info = self.get_owner_manager_infos( - cloud_account['organization_id'], tenant_auth_user_ids) + cloud_account['organization_id'], tenant_auth_user_ids, + HeraldTemplates.ENVIRONMENT_CHANGES.value) env_properties_list = [ {'env_key': env_prop_key, 'env_value': env_prop_value} for env_prop_key, env_prop_value in env_properties.items() @@ -409,9 +425,11 @@ def execute_expense_alert(self, pool_id, organization_id, meta): employee_id = contact.get('employee_id') if employee_id: _, employee = self.rest_cl.employee_get(employee_id) - _, user = self.auth_cl.user_get(employee['auth_user_id']) - self.send_expenses_alert( - user['email'], alert, pool['name'], organization) + if self.is_email_enabled(employee['id'], + HeraldTemplates.POOL_ALERT.value): + _, user = self.auth_cl.user_get(employee['auth_user_id']) + self.send_expenses_alert( + user['email'], alert, pool['name'], organization) def execute_constraint_violated(self, object_id, organization_id, meta, object_type): @@ -423,6 +441,14 @@ def execute_constraint_violated(self, object_id, organization_id, meta, if user.get('slack_connected'): return + _, employees = self.rest_cl.employee_list(organization_id) + employee = next((x for x in employees['employees'] + if x['auth_user_id'] == object_id), None) + if employee and not self.is_email_enabled( + employee['id'], + HeraldTemplates.RESOURCE_OWNER_VIOLATION_ALERT.value): + return + hit_list = meta.get('violations') resource_type_map = { 'ttl': 'TTL', @@ -536,14 +562,6 @@ def _get_org_constraint_link(self, constraint, created_at, filters): def _get_org_constraint_template_params(self, organization, constraint, constraint_data, hit_date, latest_hit, link, user_info): - constraint_template_map = { - 'expense_anomaly': HeraldTemplates.ANOMALY_DETECTION.value, - 'resource_count_anomaly': HeraldTemplates.ANOMALY_DETECTION.value, - 'expiring_budget': HeraldTemplates.EXPIRING_BUDGET.value, - 'recurring_budget': HeraldTemplates.RECURRING_BUDGET.value, - 'resource_quota': HeraldTemplates.RESOURCE_QUOTA.value, - 'tagging_policy': HeraldTemplates.TAGGING_POLICY.value - } if 'anomaly' in constraint['type']: title = 'Anomaly detection alert' else: @@ -578,7 +596,7 @@ def _get_org_constraint_template_params(self, organization, constraint, if without_tag: conditions.append(f'without tag "{without_tag}"') params['texts']['conditions'] = ', '.join(conditions) - return params, title, constraint_template_map[constraint['type']] + return params, title def execute_organization_constraint_violated(self, constraint_id, organization_id): @@ -587,7 +605,8 @@ def execute_organization_constraint_violated(self, constraint_id, LOG.warning('Organization %s was not found, error code: %s' % ( organization_id, code)) return - code, constraint = self.rest_cl.organization_constraint_get(constraint_id) + code, constraint = self.rest_cl.organization_constraint_get( + constraint_id) if not constraint: LOG.warning( 'Organization constraint %s was not found, error code: %s' % ( @@ -616,9 +635,11 @@ def execute_organization_constraint_violated(self, constraint_id, constraint_data['definition']['start_date'] = utcfromtimestamp( int(constraint_data['definition']['start_date'])).strftime( '%m/%d/%Y %I:%M %p UTC') - managers = self.get_owner_manager_infos(organization_id) + template = CONSTRAINT_TYPE_TEMPLATE_MAP[constraint['type']] + managers = self.get_owner_manager_infos( + organization_id, email_template=template) for user_id, user_info in managers.items(): - params, subject, template = self._get_org_constraint_template_params( + params, subject = self._get_org_constraint_template_params( organization, constraint, constraint_data, hit_date, latest_hit, link, user_info) self.herald_cl.email_send( @@ -634,7 +655,9 @@ def execute_new_security_recommendation(self, organization_id, return for i, data_dict in enumerate(module_count_list): module_count_list[i] = data_dict - managers = self.get_owner_manager_infos(organization_id) + managers = self.get_owner_manager_infos( + organization_id, + email_template=HeraldTemplates.NEW_SECURITY_RECOMMENDATION.value) for user_id, user_info in managers.items(): template_params = { 'texts': { @@ -662,7 +685,8 @@ def execute_saving_spike(self, organization_id, meta): opt['saving'] = round(opt['saving'], 2) top3[i] = opt - managers = self.get_owner_manager_infos(organization_id) + managers = self.get_owner_manager_infos( + organization_id, email_template=HeraldTemplates.SAVING_SPIKE.value) for user_id, user_info in managers.items(): template_params = { 'texts': { @@ -683,7 +707,9 @@ def execute_saving_spike(self, organization_id, meta): def execute_report_imports_passed_for_org(self, organization_id): _, organization = self.rest_cl.organization_get(organization_id) - managers = self.get_owner_manager_infos(organization_id) + managers = self.get_owner_manager_infos( + organization_id, + email_template=HeraldTemplates.REPORT_IMPORT_PASSED.value) emails = [x['email'] for x in managers.values()] subject = 'Expenses initial processing completed' template_params = { @@ -691,10 +717,11 @@ def execute_report_imports_passed_for_org(self, organization_id): 'organization': self._get_organization_params(organization), } } - self.herald_cl.email_send( - emails, subject, - template_type=HeraldTemplates.REPORT_IMPORT_PASSED.value, - template_params=template_params) + if emails: + self.herald_cl.email_send( + emails, subject, + template_type=HeraldTemplates.REPORT_IMPORT_PASSED.value, + template_params=template_params) def execute_insider_prices(self): self._send_service_email('Insider faced Azure SSLError', diff --git a/docker_images/webhook_executor/worker.py b/docker_images/webhook_executor/worker.py index b1634bdf..ebe4ef7e 100644 --- a/docker_images/webhook_executor/worker.py +++ b/docker_images/webhook_executor/worker.py @@ -101,7 +101,7 @@ def get_environment_meta(self, webhook, meta_info): ssh_key_map = json.loads(ssh_key_map_json) ssh_key = ssh_key_map.get('key') booking['ssh_key'] = ssh_key - owner_id = booking.get('acquired_by_id') + owner_id = booking.get('acquired_by', {}).get('id') owner = {} if owner_id: _, owner = self.rest_cl.employee_get(owner_id) diff --git a/katara/katara_service/alembic/versions/f15bef09d604_checking_email_settings_task_state.py b/katara/katara_service/alembic/versions/f15bef09d604_checking_email_settings_task_state.py new file mode 100644 index 00000000..4c7959ea --- /dev/null +++ b/katara/katara_service/alembic/versions/f15bef09d604_checking_email_settings_task_state.py @@ -0,0 +1,76 @@ +""""checking_email_settings_task_state" + +Revision ID: f15bef09d604 +Revises: 66dbed1e88e6 +Create Date: 2024-08-30 12:02:40.374500 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.orm import Session +from sqlalchemy.sql import table, column +from sqlalchemy import update, String + +# revision identifiers, used by Alembic. +revision = "f15bef09d604" +down_revision = "66dbed1e88e6" +branch_labels = None +depends_on = None + + +old_states = sa.Enum( + "created", + "started", + "getting_scopes", + "got_scopes", + "getting_recipients", + "got_recipients", + "generating_data", + "generated_data", + "putting_to_object_storage", + "put_to_object_storage", + "putting_to_herald", + "completed", + "error", +) +new_states = sa.Enum( + "created", + "started", + "getting_scopes", + "got_scopes", + "getting_recipients", + "got_recipients", + "checking_email_settings", + "generating_data", + "generated_data", + "putting_to_object_storage", + "put_to_object_storage", + "putting_to_herald", + "completed", + "error", +) + + +def upgrade(): + op.alter_column("task", "state", existing_type=new_states, nullable=False) + + +def downgrade(): + task_table = table( + "task", + column("state", String(128)), + ) + bind = op.get_bind() + session = Session(bind=bind) + try: + update_task_stmt = ( + update(task_table) + .values(state="started") + .where(task_table.c.state == "checking_email_settings") + ) + session.execute(update_task_stmt) + session.commit() + finally: + session.close() + + op.alter_column("task", "state", existing_type=old_states, nullable=False) diff --git a/katara/katara_service/migrate.py b/katara/katara_service/migrate.py index c094e134..77256f1e 100644 --- a/katara/katara_service/migrate.py +++ b/katara/katara_service/migrate.py @@ -35,7 +35,7 @@ def save(self, host, username, password, db, file_name='alembic.ini'): config.write(fh) -def execute(cmd, path='..'): +def execute(cmd, path='../..'): LOG.debug('Executing command %s', ''.join(cmd)) myenv = os.environ.copy() myenv['PYTHONPATH'] = path diff --git a/katara/katara_service/models/models.py b/katara/katara_service/models/models.py index 206c266d..b42b728c 100644 --- a/katara/katara_service/models/models.py +++ b/katara/katara_service/models/models.py @@ -29,6 +29,7 @@ class TaskState(enum.Enum): got_scopes = 'got_scopes' getting_recipients = 'getting_recipients' got_recipients = 'got_recipients' + checking_email_settings = 'checking_email_settings' generating_data = 'generating_data' generated_data = 'generated_data' putting_to_herald = 'putting_to_herald' diff --git a/katara/katara_worker/consts.py b/katara/katara_worker/consts.py index 46749202..7fceabc5 100644 --- a/katara/katara_worker/consts.py +++ b/katara/katara_worker/consts.py @@ -5,6 +5,7 @@ class TaskState(object): GOT_SCOPES = 'got_scopes' GETTING_RECIPIENTS = 'getting_recipients' GOT_RECIPIENTS = 'got_recipients' + CHECKING_EMAIL_SETTINGS = 'checking_email_settings' GENERATING_DATA = 'generating_data' GENERATED_DATA = 'generated_data' PUTTING_TO_HERALD = 'putting_to_herald' diff --git a/katara/katara_worker/reports_generators/base.py b/katara/katara_worker/reports_generators/base.py index 88910910..329d228e 100644 --- a/katara/katara_worker/reports_generators/base.py +++ b/katara/katara_worker/reports_generators/base.py @@ -1,8 +1,18 @@ +import os from currency_symbols.currency_symbols import CURRENCY_SYMBOLS_MAP from optscale_client.auth_client.client_v2 import Client as AuthClient from optscale_client.rest_api_client.client_v2 import Client as RestClient +MODULE_NAME_EMAIL_TEMPLATE = { + 'organization_expenses': 'weekly_expense_report', + 'pool_limit_exceed': 'pool_exceed_report', + 'pool_limit_exceed_resources': 'pool_exceed_resources_report', + 'violated_constraints': 'resource_owner_violation_report', + 'violated_constraints_diff': 'pool_owner_violation_report' +} + + class Base(object): def __init__(self, organization_id, report_data, config_client): self.organization_id = organization_id @@ -30,3 +40,8 @@ def auth_cl(self): @staticmethod def get_currency_code(currency): return CURRENCY_SYMBOLS_MAP.get(currency, '') + + @staticmethod + def get_template_type(path): + return MODULE_NAME_EMAIL_TEMPLATE[(os.path.splitext( + os.path.basename(path)))[0]] diff --git a/katara/katara_worker/reports_generators/organization_expenses.py b/katara/katara_worker/reports_generators/organization_expenses.py index 3763d168..e8d4026b 100644 --- a/katara/katara_worker/reports_generators/organization_expenses.py +++ b/katara/katara_worker/reports_generators/organization_expenses.py @@ -1,8 +1,8 @@ import uuid from calendar import monthrange -from katara.katara_worker.reports_generators.base import Base from tools.optscale_time import utcnow +from katara.katara_worker.reports_generators.base import Base class OrganizationExpenses(Base): @@ -54,7 +54,7 @@ def generate(self): return { 'email': [self.report_data['user_email']], - 'template_type': 'weekly_expense_report', + 'template_type': self.get_template_type(__file__), 'subject': 'OptScale weekly expense report', 'template_params': { 'texts': { diff --git a/katara/katara_worker/reports_generators/pool_limit_exceed.py b/katara/katara_worker/reports_generators/pool_limit_exceed.py index a8071a1d..1f6f6550 100644 --- a/katara/katara_worker/reports_generators/pool_limit_exceed.py +++ b/katara/katara_worker/reports_generators/pool_limit_exceed.py @@ -34,7 +34,7 @@ def generate(self): return return { 'email': [self.report_data['user_email']], - 'template_type': 'pool_exceed_report', + 'template_type': self.get_template_type(__file__), 'subject': 'Action Required: Hystax OptScale Pool Limit ' 'Exceed Alert', 'template_params': { diff --git a/katara/katara_worker/reports_generators/pool_limit_exceed_resources.py b/katara/katara_worker/reports_generators/pool_limit_exceed_resources.py index fe824abb..8648e0c2 100644 --- a/katara/katara_worker/reports_generators/pool_limit_exceed_resources.py +++ b/katara/katara_worker/reports_generators/pool_limit_exceed_resources.py @@ -1,4 +1,6 @@ -from katara.katara_worker.reports_generators.base import Base +from katara.katara_worker.reports_generators.base import ( + Base, MODULE_NAME_EMAIL_TEMPLATE +) class PoolExceedResources(Base): @@ -51,7 +53,7 @@ def generate(self): return return { 'email': [self.report_data['user_email']], - 'template_type': 'pool_exceed_resources_report', + 'template_type': self.get_template_type(__file__), 'subject': 'Action Required: Hystax OptScale Pool Limit ' 'Exceed Alert', 'template_params': { diff --git a/katara/katara_worker/reports_generators/violated_constraints.py b/katara/katara_worker/reports_generators/violated_constraints.py index 51f57ddd..100b2697 100644 --- a/katara/katara_worker/reports_generators/violated_constraints.py +++ b/katara/katara_worker/reports_generators/violated_constraints.py @@ -34,7 +34,7 @@ def generate(self): res_constaint['type'] = type_value_for_replace return { 'email': [self.report_data['user_email']], - 'template_type': 'resource_owner_violation_report', + 'template_type': self.get_template_type(__file__), 'subject': 'Action required: Hystax OptScale Resource Constraints Report', 'template_params': { diff --git a/katara/katara_worker/reports_generators/violated_constraints_diff.py b/katara/katara_worker/reports_generators/violated_constraints_diff.py index 06fafe6a..86f001ea 100644 --- a/katara/katara_worker/reports_generators/violated_constraints_diff.py +++ b/katara/katara_worker/reports_generators/violated_constraints_diff.py @@ -6,7 +6,7 @@ class ViolatedConstraintsDiff(ViolatedConstraints): def generate(self): report = super().generate() if report: - report['template_type'] = 'pool_owner_violation_report' + report['template_type'] = self.get_template_type(__file__) return report diff --git a/katara/katara_worker/tasks.py b/katara/katara_worker/tasks.py index 030e15b0..efe5b685 100644 --- a/katara/katara_worker/tasks.py +++ b/katara/katara_worker/tasks.py @@ -7,6 +7,9 @@ from katara.katara_worker.consts import TaskState +from katara.katara_worker.reports_generators.base import ( + MODULE_NAME_EMAIL_TEMPLATE +) from katara.katara_worker.reports_generators.report import create_report @@ -272,13 +275,52 @@ def execute(self): result['user_role'] = user_role new_tasks.append({ 'schedule_id': task['schedule_id'], - 'state': TaskState.GENERATING_DATA, + 'state': TaskState.CHECKING_EMAIL_SETTINGS, 'result': json.dumps(result), 'parent_id': task['id']}) self.katara_cl.tasks_create(tasks=new_tasks) super().execute() +class CheckingEmployeeEmailSettings(CheckTimeoutThreshold): + def execute(self): + _, task = self.katara_cl.task_get( + self.body['task_id'], expanded=True) + schedule = task.get('schedule') or {} + organization_id = schedule.get('recipient', {}).get('scope_id') + result = self._load_result(task['result']) + auth_user_id = result['user_role']['user_id'] + _, employees = self.rest_cl.employee_list(organization_id) + employee = next((x for x in employees['employees'] + if x['auth_user_id'] == auth_user_id), None) + if not employee: + LOG.info('Employee not found, completing task %s', + self.body['task_id']) + SetCompleted(body=self.body, message=self.message, + config_cl=self.config_cl, + on_continue_cb=self.on_continue_cb, + on_complete_cb=self.on_complete_cb).execute() + return + module_name = schedule.get('report', {}).get('module_name') + email_template = MODULE_NAME_EMAIL_TEMPLATE[module_name] + _, email_templates = self.rest_cl.employee_emails_get( + employee['id'], email_template=email_template) + if (not email_templates.get('employee_emails') or + not email_templates['employee_emails'][0]['enabled']): + LOG.info('Employee email %s for employee %s is disabled, ' + 'completing task %s', module_name, auth_user_id, + self.body['task_id']) + SetCompleted(body=self.body, message=self.message, + config_cl=self.config_cl, + on_continue_cb=self.on_continue_cb, + on_complete_cb=self.on_complete_cb).execute() + return + self.katara_cl.task_update( + self.body['task_id'], result=json.dumps(result), + state=TaskState.GENERATING_DATA) + super().execute() + + class GenerateReportData(CheckTimeoutThreshold): def execute(self): _, task = self.katara_cl.task_get( diff --git a/katara/katara_worker/transitions.py b/katara/katara_worker/transitions.py index e6a0f46d..c5529465 100644 --- a/katara/katara_worker/transitions.py +++ b/katara/katara_worker/transitions.py @@ -6,6 +6,7 @@ SetGettingRecipients, GetRecipients, SetGeneratingReportData, + CheckingEmployeeEmailSettings, GenerateReportData, SetPuttingToHerald, PutToHerald @@ -19,6 +20,7 @@ TaskState.GOT_SCOPES: SetGettingRecipients, TaskState.GETTING_RECIPIENTS: GetRecipients, TaskState.GOT_RECIPIENTS: SetGeneratingReportData, + TaskState.CHECKING_EMAIL_SETTINGS: CheckingEmployeeEmailSettings, TaskState.GENERATING_DATA: GenerateReportData, TaskState.GENERATED_DATA: SetPuttingToHerald, TaskState.PUTTING_TO_HERALD: PutToHerald diff --git a/ngui/server/api/restapi/client.ts b/ngui/server/api/restapi/client.ts index 7ba68b38..d6e98ca0 100644 --- a/ngui/server/api/restapi/client.ts +++ b/ngui/server/api/restapi/client.ts @@ -1,7 +1,9 @@ import BaseClient from "../baseClient.js"; import { DataSourceRequestParams, + MutationUpdateEmployeeEmailsArgs, UpdateDataSourceInput, + MutationUpdateEmployeeEmailArgs, } from "../../graphql/resolvers/restapi.generated.js"; class RestClient extends BaseClient { @@ -44,6 +46,50 @@ class RestClient extends BaseClient { return dataSource; } + + async getEmployeeEmails(employeeId: string) { + const path = `employees/${employeeId}/emails`; + + const emails = await this.get(path); + + return emails.employee_emails; + } + + async updateEmployeeEmails( + employeeId: MutationUpdateEmployeeEmailsArgs["employeeId"], + params: MutationUpdateEmployeeEmailsArgs["params"] + ) { + const path = `employees/${employeeId}/emails/bulk`; + + const emails = await this.post(path, { + body: params, + }); + + const emailIds = [...(params?.enable ?? []), ...(params.disable ?? [])]; + + return emails.employee_emails.filter((email) => + emailIds.includes(email.id) + ); + } + + async updateEmployeeEmail( + employeeId: MutationUpdateEmployeeEmailArgs["employeeId"], + params: MutationUpdateEmployeeEmailArgs["params"] + ) { + const { emailId, action } = params; + + const path = `employees/${employeeId}/emails/bulk`; + + const emails = await this.post(path, { + body: { + [action === "enable" ? "enable" : "disable"]: [emailId], + }, + }); + + const email = emails.employee_emails.find((email) => email.id === emailId); + + return email; + } } export default RestClient; diff --git a/ngui/server/graphql/resolvers/restapi.generated.ts b/ngui/server/graphql/resolvers/restapi.generated.ts index 652934c2..501a9291 100644 --- a/ngui/server/graphql/resolvers/restapi.generated.ts +++ b/ngui/server/graphql/resolvers/restapi.generated.ts @@ -235,6 +235,15 @@ export type DatabricksDataSource = DataSourceInterface & { type: DataSourceType; }; +export type EmployeeEmail = { + __typename?: 'EmployeeEmail'; + available_by_role: Scalars['Boolean']['output']; + email_template: Scalars['String']['output']; + employee_id: Scalars['ID']['output']; + enabled: Scalars['Boolean']['output']; + id: Scalars['ID']['output']; +}; + export type EnvironmentDataSource = DataSourceInterface & { __typename?: 'EnvironmentDataSource'; account_id: Scalars['String']['output']; @@ -327,6 +336,8 @@ export type K8sDataSource = DataSourceInterface & { export type Mutation = { __typename?: 'Mutation'; updateDataSource?: Maybe; + updateEmployeeEmail?: Maybe; + updateEmployeeEmails?: Maybe>>; }; @@ -335,6 +346,18 @@ export type MutationUpdateDataSourceArgs = { params: UpdateDataSourceInput; }; + +export type MutationUpdateEmployeeEmailArgs = { + employeeId: Scalars['ID']['input']; + params: UpdateEmployeeEmailInput; +}; + + +export type MutationUpdateEmployeeEmailsArgs = { + employeeId: Scalars['ID']['input']; + params: UpdateEmployeeEmailsInput; +}; + export type NebiusConfig = { __typename?: 'NebiusConfig'; access_key_id?: Maybe; @@ -376,6 +399,7 @@ export type NebiusDataSource = DataSourceInterface & { export type Query = { __typename?: 'Query'; dataSource?: Maybe; + employeeEmails?: Maybe>>; }; @@ -384,6 +408,11 @@ export type QueryDataSourceArgs = { requestParams?: InputMaybe; }; + +export type QueryEmployeeEmailsArgs = { + employeeId: Scalars['ID']['input']; +}; + export type UpdateDataSourceInput = { alibabaConfig?: InputMaybe; awsLinkedConfig?: InputMaybe; @@ -399,6 +428,21 @@ export type UpdateDataSourceInput = { nebiusConfig?: InputMaybe; }; +export type UpdateEmployeeEmailInput = { + action: UpdateEmployeeEmailsAction; + emailId: Scalars['ID']['input']; +}; + +export enum UpdateEmployeeEmailsAction { + Disable = 'disable', + Enable = 'enable' +} + +export type UpdateEmployeeEmailsInput = { + disable?: InputMaybe>; + enable?: InputMaybe>; +}; + export type ResolverTypeWrapper = Promise | T; @@ -496,6 +540,7 @@ export type ResolversTypes = { DatabricksConfig: ResolverTypeWrapper; DatabricksConfigInput: DatabricksConfigInput; DatabricksDataSource: ResolverTypeWrapper; + EmployeeEmail: ResolverTypeWrapper; EnvironmentDataSource: ResolverTypeWrapper; Float: ResolverTypeWrapper; GcpBillingDataConfig: ResolverTypeWrapper; @@ -517,6 +562,9 @@ export type ResolversTypes = { Query: ResolverTypeWrapper<{}>; String: ResolverTypeWrapper; UpdateDataSourceInput: UpdateDataSourceInput; + UpdateEmployeeEmailInput: UpdateEmployeeEmailInput; + UpdateEmployeeEmailsAction: UpdateEmployeeEmailsAction; + UpdateEmployeeEmailsInput: UpdateEmployeeEmailsInput; }; /** Mapping between all available schema types and the resolvers parents */ @@ -542,6 +590,7 @@ export type ResolversParentTypes = { DatabricksConfig: DatabricksConfig; DatabricksConfigInput: DatabricksConfigInput; DatabricksDataSource: DatabricksDataSource; + EmployeeEmail: EmployeeEmail; EnvironmentDataSource: EnvironmentDataSource; Float: Scalars['Float']['output']; GcpBillingDataConfig: GcpBillingDataConfig; @@ -563,6 +612,8 @@ export type ResolversParentTypes = { Query: {}; String: Scalars['String']['output']; UpdateDataSourceInput: UpdateDataSourceInput; + UpdateEmployeeEmailInput: UpdateEmployeeEmailInput; + UpdateEmployeeEmailsInput: UpdateEmployeeEmailsInput; }; export type AlibabaConfigResolvers = { @@ -727,6 +778,15 @@ export type DatabricksDataSourceResolvers; }; +export type EmployeeEmailResolvers = { + available_by_role?: Resolver; + email_template?: Resolver; + employee_id?: Resolver; + enabled?: Resolver; + id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type EnvironmentDataSourceResolvers = { account_id?: Resolver; details?: Resolver, ParentType, ContextType>; @@ -807,6 +867,8 @@ export type K8sDataSourceResolvers = { updateDataSource?: Resolver, ParentType, ContextType, RequireFields>; + updateEmployeeEmail?: Resolver, ParentType, ContextType, RequireFields>; + updateEmployeeEmails?: Resolver>>, ParentType, ContextType, RequireFields>; }; export type NebiusConfigResolvers = { @@ -838,6 +900,7 @@ export type NebiusDataSourceResolvers = { dataSource?: Resolver, ParentType, ContextType, RequireFields>; + employeeEmails?: Resolver>>, ParentType, ContextType, RequireFields>; }; export type Resolvers = { @@ -854,6 +917,7 @@ export type Resolvers = { DataSourceInterface?: DataSourceInterfaceResolvers; DatabricksConfig?: DatabricksConfigResolvers; DatabricksDataSource?: DatabricksDataSourceResolvers; + EmployeeEmail?: EmployeeEmailResolvers; EnvironmentDataSource?: EnvironmentDataSourceResolvers; GcpBillingDataConfig?: GcpBillingDataConfigResolvers; GcpConfig?: GcpConfigResolvers; diff --git a/ngui/server/graphql/resolvers/restapi.ts b/ngui/server/graphql/resolvers/restapi.ts index 3cdcd89d..599e3534 100644 --- a/ngui/server/graphql/resolvers/restapi.ts +++ b/ngui/server/graphql/resolvers/restapi.ts @@ -44,11 +44,24 @@ const resolvers: Resolvers = { dataSource: async (_, { dataSourceId, requestParams }, { dataSources }) => { return dataSources.restapi.getDataSource(dataSourceId, requestParams); }, + employeeEmails: async (_, { employeeId }, { dataSources }) => { + return dataSources.restapi.getEmployeeEmails(employeeId); + }, }, Mutation: { updateDataSource: async (_, { dataSourceId, params }, { dataSources }) => { return dataSources.restapi.updateDataSource(dataSourceId, params); }, + updateEmployeeEmails: async ( + _, + { employeeId, params }, + { dataSources } + ) => { + return dataSources.restapi.updateEmployeeEmails(employeeId, params); + }, + updateEmployeeEmail: async (_, { employeeId, params }, { dataSources }) => { + return dataSources.restapi.updateEmployeeEmail(employeeId, params); + }, }, }; diff --git a/ngui/server/graphql/schemas/restapi.graphql b/ngui/server/graphql/schemas/restapi.graphql index 9b11d989..2ce9a481 100644 --- a/ngui/server/graphql/schemas/restapi.graphql +++ b/ngui/server/graphql/schemas/restapi.graphql @@ -351,11 +351,35 @@ input UpdateDataSourceInput { k8sConfig: K8sConfigInput } +type EmployeeEmail { + id: ID! + employee_id: ID! + email_template: String! + enabled: Boolean! + available_by_role: Boolean! +} + +input UpdateEmployeeEmailsInput { + enable: [ID!] + disable: [ID!] +} + +enum UpdateEmployeeEmailsAction { + enable + disable +} + +input UpdateEmployeeEmailInput { + emailId: ID! + action: UpdateEmployeeEmailsAction! +} + type Query { dataSource( dataSourceId: ID! requestParams: DataSourceRequestParams ): DataSourceInterface + employeeEmails(employeeId: ID!): [EmployeeEmail] } type Mutation { @@ -363,4 +387,12 @@ type Mutation { dataSourceId: ID! params: UpdateDataSourceInput! ): DataSourceInterface + updateEmployeeEmails( + employeeId: ID! + params: UpdateEmployeeEmailsInput! + ): [EmployeeEmail] + updateEmployeeEmail( + employeeId: ID! + params: UpdateEmployeeEmailInput! + ): EmployeeEmail } diff --git a/ngui/ui/src/components/Accordion/Accordion.styles.ts b/ngui/ui/src/components/Accordion/Accordion.styles.ts index 477258f7..163c2f32 100644 --- a/ngui/ui/src/components/Accordion/Accordion.styles.ts +++ b/ngui/ui/src/components/Accordion/Accordion.styles.ts @@ -1,24 +1,45 @@ import { makeStyles } from "tss-react/mui"; -const useStyles = makeStyles()((theme) => ({ +const getExpandColorStyles = ({ theme, expandTitleColor, alwaysHighlightTitle = false }) => { + const style = { + background: { + backgroundColor: theme.palette.background.default + } + }[expandTitleColor] ?? { + color: theme.palette.secondary.contrastText, + backgroundColor: theme.palette.action.selected, + "& svg": { + color: theme.palette.secondary.contrastText + }, + "& p": { + color: theme.palette.secondary.contrastText + }, + "& input": { + color: theme.palette.secondary.contrastText + } + }; + + return { + "&.MuiAccordionSummary-root": alwaysHighlightTitle + ? style + : { + "&.Mui-expanded": style + } + }; +}; + +const useStyles = makeStyles()((theme, { expandTitleColor, alwaysHighlightTitle }) => ({ details: { display: "block" }, summary: { - flexDirection: "row-reverse", - "&.MuiAccordionSummary-root": { - "&.Mui-expanded": { - "& svg": { - color: theme.palette.secondary.contrastText - }, - "& p": { - color: theme.palette.secondary.contrastText - }, - "& input": { - color: theme.palette.secondary.contrastText - } - } - } + flexDirection: "row-reverse" + }, + enableBorder: { + borderBottom: `1px solid ${theme.palette.divider}` + }, + disableShadows: { + boxShadow: "none" }, inheritFlexDirection: { flexDirection: "inherit" @@ -33,7 +54,8 @@ const useStyles = makeStyles()((theme) => ({ }, zeroSummaryMinHeight: { minHeight: "0" - } + }, + expandTitleColor: getExpandColorStyles({ theme, expandTitleColor, alwaysHighlightTitle }) })); export default useStyles; diff --git a/ngui/ui/src/components/Accordion/Accordion.tsx b/ngui/ui/src/components/Accordion/Accordion.tsx index d7b27497..9d614314 100644 --- a/ngui/ui/src/components/Accordion/Accordion.tsx +++ b/ngui/ui/src/components/Accordion/Accordion.tsx @@ -13,15 +13,22 @@ const Accordion = ({ inheritFlexDirection = false, actions = null, headerDataTestId, + disableShadows = false, + enabledBorder = false, + expandTitleColor, + alwaysHighlightTitle = false, ...rest }) => { - const { classes, cx } = useStyles(); + const { classes, cx } = useStyles({ + expandTitleColor, + alwaysHighlightTitle + }); return ( {summary} diff --git a/ngui/ui/src/components/UserEmailNotificationSettings/UserEmailNotificationSettings.tsx b/ngui/ui/src/components/UserEmailNotificationSettings/UserEmailNotificationSettings.tsx new file mode 100644 index 00000000..4c70611c --- /dev/null +++ b/ngui/ui/src/components/UserEmailNotificationSettings/UserEmailNotificationSettings.tsx @@ -0,0 +1,298 @@ +import { useMutation } from "@apollo/client"; +import { Box, CircularProgress, Stack, Switch, Typography } from "@mui/material"; +import { FormattedMessage } from "react-intl"; +import Accordion from "components/Accordion"; +import Chip from "components/Chip"; +import KeyValueLabel from "components/KeyValueLabel"; +import PanelLoader from "components/PanelLoader"; +import SubTitle from "components/SubTitle"; +import { UPDATE_EMPLOYEE_EMAIL, UPDATE_EMPLOYEE_EMAILS } from "graphql/api/restapi/queries/restapi.queries"; +import { isEmpty as isEmptyArray } from "utils/arrays"; +import { SPACING_2 } from "utils/layouts"; +import { ObjectKeys } from "utils/types"; +import { + ApiEmployeeEmail, + EmailSettingProps, + EmployeeEmail, + LoadingSwitchProps, + UserEmailNotificationSettingsProps, + UserEmailSettingsProps +} from "./types"; + +const EMAIL_TEMPLATES = { + finOps: { + weekly_expense_report: { + title: "emailTemplates.finOps.weekly_expense_report.title", + description: "emailTemplates.finOps.weekly_expense_report.description" + }, + pool_exceed_resources_report: { + title: "emailTemplates.finOps.pool_exceed_resources_report.title", + description: "emailTemplates.finOps.pool_exceed_resources_report.description" + }, + pool_exceed_report: { + title: "emailTemplates.finOps.pool_exceed_report.title", + description: "emailTemplates.finOps.pool_exceed_report.description" + }, + alert: { + title: "emailTemplates.finOps.alert.title", + description: "emailTemplates.finOps.alert.description" + }, + saving_spike: { + title: "emailTemplates.finOps.saving_spike.title", + description: "emailTemplates.finOps.saving_spike.description" + } + }, + policy: { + resource_owner_violation_report: { + title: "emailTemplates.policy.resource_owner_violation_report.title", + description: "emailTemplates.policy.resource_owner_violation_report.description" + }, + pool_owner_violation_report: { + title: "emailTemplates.policy.pool_owner_violation_report.title", + description: "emailTemplates.policy.pool_owner_violation_report.description" + }, + resource_owner_violation_alert: { + title: "emailTemplates.policy.resource_owner_violation_alert.title", + description: "emailTemplates.policy.resource_owner_violation_alert.description" + }, + anomaly_detection_alert: { + title: "emailTemplates.policy.anomaly_detection_alert.title", + description: "emailTemplates.policy.anomaly_detection_alert.description" + }, + organization_policy_expiring_budget: { + title: "emailTemplates.policy.organization_policy_expiring_budget.title", + description: "emailTemplates.policy.organization_policy_expiring_budget.description" + }, + organization_policy_quota: { + title: "emailTemplates.policy.organization_policy_quota.title", + description: "emailTemplates.policy.organization_policy_quota.description" + }, + organization_policy_recurring_budget: { + title: "emailTemplates.policy.organization_policy_recurring_budget.title", + description: "emailTemplates.policy.organization_policy_recurring_budget.description" + }, + organization_policy_tagging: { + title: "emailTemplates.policy.organization_policy_tagging.title", + description: "emailTemplates.policy.organization_policy_tagging.description" + } + }, + recommendations: { + new_security_recommendation: { + title: "emailTemplates.recommendations.new_security_recommendation.title", + description: "emailTemplates.recommendations.new_security_recommendation.description" + } + }, + systemNotifications: { + environment_changes: { + title: "emailTemplates.systemNotifications.environment_changes.title", + description: "emailTemplates.systemNotifications.environment_changes.description" + }, + report_imports_passed_for_org: { + title: "emailTemplates.systemNotifications.report_imports_passed_for_org.title", + description: "emailTemplates.systemNotifications.report_imports_passed_for_org.description" + } + }, + accountManagement: { + invite: { + title: "emailTemplates.accountManagement.invite.title", + description: "emailTemplates.accountManagement.invite.description" + } + } +} as const; + +const LoadingSwitch = ({ checked, onChange, isLoading = false }: LoadingSwitchProps) => { + const icon = ( + (checked ? theme.palette.secondary.main : theme.palette.background.default), + boxShadow: (theme) => theme.shadows[1] + }} + > + {isLoading && } + + ); + + return ; +}; + +const EmailSetting = ({ emailId, employeeId, enabled, emailTitle, description }: EmailSettingProps) => { + const [updateEmployeeEmail, { loading: updateEmployeeEmailLoading }] = useMutation(UPDATE_EMPLOYEE_EMAIL); + + return ( + + + + + + { + const { checked } = event.target; + + updateEmployeeEmail({ + variables: { + employeeId, + params: { + emailId, + action: checked ? "enable" : "disable" + } + } + }); + }} + isLoading={updateEmployeeEmailLoading} + /> + + {} + + ); +}; + +const UserEmailSettings = ({ title, employeeEmails }: UserEmailSettingsProps) => { + const { employee_id: employeeId } = employeeEmails[0]; + + const [updateEmployeeEmails, { loading: updateEmployeeEmailsLoading }] = useMutation(UPDATE_EMPLOYEE_EMAILS); + + const areAllEmailsEnabled = employeeEmails.every((email) => email.enabled); + + const enabledEmailsCount = employeeEmails.filter((email) => email.enabled).length; + const totalEmailsCount = employeeEmails.length; + + return ( + theme.spacing(2) + } + }} + > + + + {title} + + } + /> + } + /> + + { + // prevent opening the accordion when clicking on the switch + e.stopPropagation(); + }} + > + { + const { checked } = event.target; + + updateEmployeeEmails({ + variables: { + employeeId, + params: { + [checked ? "enable" : "disable"]: employeeEmails.map((email) => email.id) + } + } + }); + }} + isLoading={updateEmployeeEmailsLoading} + /> + + + + {employeeEmails.map((email) => { + const { id: emailId, enabled, title: emailTitle, description } = email; + + return ( + + ); + })} + + + ); +}; + +const getGroupedEmailTemplates = (employeeEmails: ApiEmployeeEmail[]) => { + const employeeEmailsMap = Object.fromEntries(employeeEmails.map((email) => [email.email_template, email])); + + return Object.fromEntries( + Object.entries(EMAIL_TEMPLATES).map(([groupName, templates]) => [ + groupName, + Object.entries(templates) + .filter(([templateName]) => templateName in employeeEmailsMap) + .map(([templateName, { title, description }]) => { + const email = employeeEmailsMap[templateName]; + + return { ...email, title, description } as EmployeeEmail; + }) + .filter(({ available_by_role: availableByRole }) => availableByRole) + ]) + ) as { + [K in ObjectKeys]: EmployeeEmail[]; + }; +}; + +const UserEmailNotificationSettings = ({ employeeEmails, isLoading = false }: UserEmailNotificationSettingsProps) => { + if (isLoading) { + return ; + } + + if (isEmptyArray(employeeEmails)) { + return ; + } + + const { finOps, policy, recommendations, systemNotifications, accountManagement } = getGroupedEmailTemplates(employeeEmails); + return ( + <> + {isEmptyArray(finOps) ? null : } employeeEmails={finOps} />} + {isEmptyArray(policy) ? null : ( + } employeeEmails={policy} /> + )} + {isEmptyArray(recommendations) ? null : ( + } employeeEmails={recommendations} /> + )} + {isEmptyArray(systemNotifications) ? null : ( + } employeeEmails={systemNotifications} /> + )} + {isEmptyArray(accountManagement) ? null : ( + } employeeEmails={accountManagement} /> + )} + + ); +}; + +export default UserEmailNotificationSettings; diff --git a/ngui/ui/src/components/UserEmailNotificationSettings/index.ts b/ngui/ui/src/components/UserEmailNotificationSettings/index.ts new file mode 100644 index 00000000..de4b998b --- /dev/null +++ b/ngui/ui/src/components/UserEmailNotificationSettings/index.ts @@ -0,0 +1,3 @@ +import UserEmailNotificationSettings from "./UserEmailNotificationSettings"; + +export default UserEmailNotificationSettings; diff --git a/ngui/ui/src/components/UserEmailNotificationSettings/types.ts b/ngui/ui/src/components/UserEmailNotificationSettings/types.ts new file mode 100644 index 00000000..d75343bd --- /dev/null +++ b/ngui/ui/src/components/UserEmailNotificationSettings/types.ts @@ -0,0 +1,56 @@ +import { ChangeEvent, ReactNode } from "react"; + +// TODO TS: Replace with apollo types +export type ApiEmployeeEmail = { + id: string; + available_by_role: boolean; + email_template: + | "weekly_expense_report" + | "pool_exceed_resources_report" + | "pool_exceed_report" + | "alert" + | "saving_spike" + | "resource_owner_violation_report" + | "pool_owner_violation_report" + | "resource_owner_violation_alert" + | "anomaly_detection_alert" + | "organization_policy_expiring_budget" + | "organization_policy_quota" + | "organization_policy_recurring_budget" + | "organization_policy_tagging" + | "new_security_recommendation" + | "environment_changes" + | "report_imports_passed_for_org" + | "invite"; + enabled: boolean; + employee_id: string; +}; + +export type EmployeeEmail = { + title: string; + description: string; +} & ApiEmployeeEmail; + +export type EmailSettingProps = { + emailId: string; + employeeId: string; + enabled: boolean; + emailTitle: string; + description: string; +}; + +export type LoadingSwitchProps = { + checked: boolean; + onChange: (event: ChangeEvent) => void; + isLoading?: boolean; +}; + +export type UserEmailSettingsProps = { + title: ReactNode; + employeeEmails: EmployeeEmail[]; +}; + +export type UserEmailNotificationSettingsProps = { + employeeEmails: ApiEmployeeEmail[]; + isLoading: boolean; +}; diff --git a/ngui/ui/src/containers/UserEmailNotificationSettingsContainer/UserEmailNotificationSettingsContainer.tsx b/ngui/ui/src/containers/UserEmailNotificationSettingsContainer/UserEmailNotificationSettingsContainer.tsx new file mode 100644 index 00000000..a129f234 --- /dev/null +++ b/ngui/ui/src/containers/UserEmailNotificationSettingsContainer/UserEmailNotificationSettingsContainer.tsx @@ -0,0 +1,21 @@ +import { useQuery } from "@apollo/client"; +import { GET_CURRENT_EMPLOYEE } from "api/restapi/actionTypes"; +import UserEmailNotificationSettings from "components/UserEmailNotificationSettings"; +import { GET_EMPLOYEE_EMAILS } from "graphql/api/restapi/queries/restapi.queries"; +import { useApiData } from "hooks/useApiData"; + +const UserEmailNotificationSettingsContainer = () => { + const { + apiData: { currentEmployee = {} } + } = useApiData(GET_CURRENT_EMPLOYEE); + + const { loading, data } = useQuery(GET_EMPLOYEE_EMAILS, { + variables: { + employeeId: currentEmployee.id + } + }); + + return ; +}; + +export default UserEmailNotificationSettingsContainer; diff --git a/ngui/ui/src/containers/UserEmailNotificationSettingsContainer/index.ts b/ngui/ui/src/containers/UserEmailNotificationSettingsContainer/index.ts new file mode 100644 index 00000000..7006f645 --- /dev/null +++ b/ngui/ui/src/containers/UserEmailNotificationSettingsContainer/index.ts @@ -0,0 +1,3 @@ +import UserEmailNotificationSettingsContainer from "./UserEmailNotificationSettingsContainer"; + +export default UserEmailNotificationSettingsContainer; diff --git a/ngui/ui/src/graphql/api/restapi/queries/restapi.queries.ts b/ngui/ui/src/graphql/api/restapi/queries/restapi.queries.ts index 8fb4afad..8c27e4c4 100644 --- a/ngui/ui/src/graphql/api/restapi/queries/restapi.queries.ts +++ b/ngui/ui/src/graphql/api/restapi/queries/restapi.queries.ts @@ -110,4 +110,40 @@ const UPDATE_DATA_SOURCE = gql` } `; -export { GET_DATA_SOURCE, UPDATE_DATA_SOURCE }; +const GET_EMPLOYEE_EMAILS = gql` + query EmployeeEmails($employeeId: ID!) { + employeeEmails(employeeId: $employeeId) { + id + employee_id + email_template + enabled + available_by_role + } + } +`; + +const UPDATE_EMPLOYEE_EMAILS = gql` + mutation UpdateEmployeeEmails($employeeId: ID!, $params: UpdateEmployeeEmailsInput!) { + updateEmployeeEmails(employeeId: $employeeId, params: $params) { + id + employee_id + email_template + enabled + available_by_role + } + } +`; + +const UPDATE_EMPLOYEE_EMAIL = gql` + mutation UpdateEmployeeEmail($employeeId: ID!, $params: UpdateEmployeeEmailInput!) { + updateEmployeeEmail(employeeId: $employeeId, params: $params) { + id + employee_id + email_template + enabled + available_by_role + } + } +`; + +export { GET_DATA_SOURCE, UPDATE_DATA_SOURCE, GET_EMPLOYEE_EMAILS, UPDATE_EMPLOYEE_EMAILS, UPDATE_EMPLOYEE_EMAIL }; diff --git a/ngui/ui/src/pages/Settings/Settings.tsx b/ngui/ui/src/pages/Settings/Settings.tsx index 24597b47..3c253d4e 100644 --- a/ngui/ui/src/pages/Settings/Settings.tsx +++ b/ngui/ui/src/pages/Settings/Settings.tsx @@ -5,6 +5,7 @@ import TabsWrapper from "components/TabsWrapper"; import InvitationsContainer from "containers/InvitationsContainer"; import ModeContainer from "containers/ModeContainer"; import SshSettingsContainer from "containers/SshSettingsContainer"; +import UserEmailNotificationSettingsContainer from "containers/UserEmailNotificationSettingsContainer"; import { useIsOptScaleModeEnabled } from "hooks/useIsOptScaleModeEnabled"; import { OPTSCALE_MODE } from "utils/constants"; @@ -18,7 +19,8 @@ export const SETTINGS_TABS = Object.freeze({ ORGANIZATION: "organization", INVITATIONS: "invitations", MODE: "mode", - SSH: "sshKeys" + SSH: "sshKeys", + EMAIL_NOTIFICATIONS: "emailNotifications" }); const Settings = () => { @@ -48,7 +50,12 @@ const Settings = () => { node: } ] - : []) + : []), + { + title: SETTINGS_TABS.EMAIL_NOTIFICATIONS, + dataTestId: `tab_${SETTINGS_TABS.EMAIL_NOTIFICATIONS}`, + node: + } ]; return ( diff --git a/ngui/ui/src/theme.ts b/ngui/ui/src/theme.ts index 8504f276..5d8e8b2d 100644 --- a/ngui/ui/src/theme.ts +++ b/ngui/ui/src/theme.ts @@ -261,6 +261,7 @@ const getThemeConfig = (settings = {}) => { styleOverrides: { root: { "&:before": { + // disable border between accordions display: "none" } } @@ -277,14 +278,7 @@ const getThemeConfig = (settings = {}) => { }, root: { "&.Mui-expanded": { - minHeight: "48px", - color: secondary.contrastText, - backgroundColor: ACTION_SELECTED - } - }, - expandIconWrapper: { - "&.Mui-expanded": { - color: secondary.contrastText + minHeight: "48px" } } } diff --git a/ngui/ui/src/translations/en-US/app.json b/ngui/ui/src/translations/en-US/app.json index fef2176d..94406434 100644 --- a/ngui/ui/src/translations/en-US/app.json +++ b/ngui/ui/src/translations/en-US/app.json @@ -46,6 +46,7 @@ "accessKey": "Access key", "accessKeyId": "Access key ID", "accountId": "Account ID", + "accountManagementTitle": "Account Management", "acquireWebhook": "Acquire webhook", "action": "Action", "actions": "Actions", @@ -630,6 +631,41 @@ "email": "Email", "emailVerificationDescription": "To verify your email, please enter the verification code sent to:", "emailVerifiedSuccessfully": "Email has been verified successfully!", + "emailNotifications": "Email notifications", + "emailTemplates.accountManagement.invite.description": "Notification of an invitation to join OptScale", + "emailTemplates.accountManagement.invite.title": "Invitation notification", + "emailTemplates.finOps.alert.description": "Notification of a pool limit being reached or exceeded", + "emailTemplates.finOps.alert.title": "Pool limit alert", + "emailTemplates.finOps.pool_exceed_report.description": "Alert for exceeding the limits of a specific resource pool", + "emailTemplates.finOps.pool_exceed_report.title": "Pool limit exceed alert", + "emailTemplates.finOps.pool_exceed_resources_report.description": "Notification that resource pools have exceeded or are forecasted to exceed their defined limits", + "emailTemplates.finOps.pool_exceed_resources_report.title": "Pool limit exceed alert", + "emailTemplates.finOps.saving_spike.description": "Alert about new saving opportunities", + "emailTemplates.finOps.saving_spike.title": "Saving spike", + "emailTemplates.finOps.weekly_expense_report.description": "A summary of weekly expenses", + "emailTemplates.finOps.weekly_expense_report.title": "Weekly expense report", + "emailTemplates.policy.anomaly_detection_alert.description": "Alert for detecting unusual activity or anomalies", + "emailTemplates.policy.anomaly_detection_alert.title": "Anomaly detection", + "emailTemplates.policy.organization_policy_expiring_budget.description": "Notification of a violation related to an expiring budget policy", + "emailTemplates.policy.organization_policy_expiring_budget.title": "Expiring budget policy violation", + "emailTemplates.policy.organization_policy_quota.description": "Notification of a violation related to a quota policy", + "emailTemplates.policy.organization_policy_quota.title": "Quota policy violation", + "emailTemplates.policy.organization_policy_recurring_budget.description": "Notification of a violation related to a recurring budget policy", + "emailTemplates.policy.organization_policy_recurring_budget.title": "Recurring budget policy violation", + "emailTemplates.policy.organization_policy_tagging.description": "Notification of a violation related to a tagging policy", + "emailTemplates.policy.organization_policy_tagging.title": "Tagging policy violation", + "emailTemplates.policy.pool_owner_violation_report.description": "Report regarding resource constraint violations within your managed pool", + "emailTemplates.policy.pool_owner_violation_report.title": "Resource constraints report", + "emailTemplates.policy.resource_owner_violation_alert.description": "Alert for detecting new constraint violations", + "emailTemplates.policy.resource_owner_violation_alert.title": "Resource constraint violation alert", + "emailTemplates.policy.resource_owner_violation_report.description": "Notification of resource constraint violations within your managed pool", + "emailTemplates.policy.resource_owner_violation_report.title": "Resource constraints report", + "emailTemplates.recommendations.new_security_recommendation.description": "Alert regarding a newly detected security recommendation", + "emailTemplates.recommendations.new_security_recommendation.title": "New security recommendation detection", + "emailTemplates.systemNotifications.environment_changes.description": "Notification of changes made in a Shared Environment", + "emailTemplates.systemNotifications.environment_changes.title": "Environment changed", + "emailTemplates.systemNotifications.report_imports_passed_for_org.description": "Confirmation that initial expense processing for your organization is complete", + "emailTemplates.systemNotifications.report_imports_passed_for_org.title": "Expenses initial processing completed", "employee": "Employee", "enabled": "Enabled", "endDate": "End date", @@ -1303,6 +1339,7 @@ "noArchivedRecommendationsAvailable": "No archived recommendations available.", "noArtifacts": "No artifacts", "noAutomaticResourceAssignmentRules": "No automatic resource assignment rules", + "noAvailableEmailNotifications": "No available email notifications", "noBIExports": "No Business Intelligence exports", "noBuckets": "No buckets", "noChildDataSourcesDiscovered": "No child data sources have been discovered", @@ -1554,6 +1591,7 @@ "pleaseUseTheFollowingEnvCollectorUrl": "Please use the following ENV_COLLECTOR_URL in your automation to set properties of this Shared Environment:", "pluralHoursValue": "{value, plural,\n =0 {hours}\n =1 { hour}\n other { hours}\n}", "policies": "Policies", + "policyAlertsTitle": "Policy Alerts", "policyName": "Policy name", "policyType": "Policy type", "policyViolations": "Policy violations", @@ -2098,6 +2136,7 @@ "survey": "Survey", "syncTooltips": "Sync tooltips", "system": "System", + "systemNotificationsTitle": "System Notifications", "tSystems": "T-Systems", "table": "Table", "tag": "Tag", diff --git a/optscale_client/rest_api_client/client_v2.py b/optscale_client/rest_api_client/client_v2.py index 0f401237..dd99910b 100644 --- a/optscale_client/rest_api_client/client_v2.py +++ b/optscale_client/rest_api_client/client_v2.py @@ -2225,3 +2225,18 @@ def verify_email_url(): def verify_email(self, email): url = self.verify_email_url() return self.post(url, {'email': email}) + + @staticmethod + def employee_emails_url(employee_id): + return '%s/emails' % Client.employee_url(employee_id) + + def employee_emails_get(self, employee_id, email_template=None): + return self.get(self.employee_emails_url(employee_id) + self.query_url( + email_template=email_template)) + + @staticmethod + def employee_emails_bulk_url(employee_id): + return '%s/bulk' % Client.employee_emails_url(employee_id) + + def employee_emails_bulk(self, employee_id, params): + return self.post(self.employee_emails_bulk_url(employee_id), params) diff --git a/rest_api/rest_api_server/alembic/versions/a1d0494e9815_employee_emails.py b/rest_api/rest_api_server/alembic/versions/a1d0494e9815_employee_emails.py new file mode 100644 index 00000000..6ed6e599 --- /dev/null +++ b/rest_api/rest_api_server/alembic/versions/a1d0494e9815_employee_emails.py @@ -0,0 +1,105 @@ +""""employee_emails" + +Revision ID: a1d0494e9815 +Revises: 7d91396a219d +Create Date: 2024-08-30 04:17:27.866034 + +""" +import uuid +from alembic import op +import sqlalchemy as sa +from sqlalchemy import and_, Boolean, insert, Integer, select, String +from sqlalchemy.orm import Session +from sqlalchemy.sql import table, column +from rest_api.rest_api_server.models.types import ( + MediumLargeNullableString, NullableBool, NullableUuid +) + +# revision identifiers, used by Alembic. +revision = 'a1d0494e9815' +down_revision = '7d91396a219d' +branch_labels = None +depends_on = None + +EMAIL_TEMPLATES = [ + 'alert', + 'anomaly_detection_alert', + 'employee_greetings', + 'environment_changes', + 'invite', + 'new_security_recommendation', + 'organization_policy_expiring_budget', + 'organization_policy_quota', + 'organization_policy_recurring_budget', + 'organization_policy_tagging', + 'pool_exceed_report', + 'pool_exceed_resources_report', + 'pool_owner_violation_report', + 'resource_owner_violation_alert', + 'resource_owner_violation_report', + 'report_imports_passed_for_org', + 'saving_spike', + 'weekly_expense_report' +] + + +def _fill_table(): + bind = op.get_bind() + session = Session(bind=bind) + try: + org_t = table('organization', + column('id', String(36)), + column('deleted_at', Integer()), + column('is_demo', Integer())) + emp_t = table('employee', + column('id', String(36)), + column('organization_id', String(36)), + column('deleted_at', Integer())) + emp_email_t = table('employee_email', + column('id', String(36)), + column('employee_id', String(36)), + column('email_template', String(256)), + column('enabled', Boolean()), + column('deleted_at', Integer())) + cmd = select([emp_t.c.id]).where( + and_(emp_t.c.deleted_at == 0, + emp_t.c.organization_id.in_( + select([org_t.c.id]).where( + and_(org_t.c.deleted_at == 0, + org_t.c.is_demo.is_(False))))) + ) + employee_ids = session.execute(cmd) + for employee in employee_ids: + for email_template in EMAIL_TEMPLATES: + insert_cmd = insert(emp_email_t).values( + id=str(uuid.uuid4()), + employee_id=employee['id'], + email_template=email_template, + enabled=True, + deleted_at=0 + ) + session.execute(insert_cmd) + session.commit() + finally: + session.close() + + +def upgrade(): + op.create_table( + 'employee_email', + sa.Column('id', NullableUuid(length=36), nullable=False), + sa.Column('employee_id', NullableUuid(length=36), nullable=False), + sa.Column('email_template', MediumLargeNullableString(length=128), + nullable=False), + sa.Column('enabled', NullableBool(), nullable=False), + sa.Column('deleted_at', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['employee_id'], ['employee.id'], ), + sa.UniqueConstraint('employee_id', 'email_template', 'deleted_at', + name='uc_employee_email_template') + ) + _fill_table() + + +def downgrade(): + op.drop_table('employee_email') diff --git a/rest_api/rest_api_server/constants.py b/rest_api/rest_api_server/constants.py index 5fa614cb..231c799b 100644 --- a/rest_api/rest_api_server/constants.py +++ b/rest_api/rest_api_server/constants.py @@ -229,6 +229,10 @@ class UrlsV2(Urls): 'restore_password': r"%s/restore_password", 'profiling_token_info': r"%s/profiling_tokens/(?P[^/]+)", 'verify_email': r"%s/verify_email", + 'employee_emails_collection': + r"%s/employees/(?P[^/]+)/emails", + 'employee_emails_bulk': + r"%s/employees/(?P[^/]+)/emails/bulk", }) diff --git a/rest_api/rest_api_server/controllers/context.py b/rest_api/rest_api_server/controllers/context.py index cbf618c7..054d703a 100644 --- a/rest_api/rest_api_server/controllers/context.py +++ b/rest_api/rest_api_server/controllers/context.py @@ -7,7 +7,8 @@ from rest_api.rest_api_server.models.models import ( Organization, CloudAccount, Employee, Pool, ReportImport, PoolAlert, PoolPolicy, ResourceConstraint, Rule, ShareableBooking, Webhook, - OrganizationConstraint, OrganizationBI, OrganizationGemini, PowerSchedule) + OrganizationConstraint, OrganizationBI, OrganizationGemini, PowerSchedule, + EmployeeEmail) from tools.optscale_exceptions.common_exc import (WrongArgumentsException, NotFoundException) from rest_api.rest_api_server.utils import tp_executor_context @@ -45,7 +46,8 @@ def _get_input(self, **kwargs): 'resource_constraint', 'rule', 'shareable_booking', 'webhook', 'organization_constraint', 'organization_bi', - 'organization_gemini', 'power_schedule']: + 'organization_gemini', 'power_schedule', + 'employee_email']: raise WrongArgumentsException(Err.OE0174, [type_name]) return type_name, uuid @@ -64,6 +66,7 @@ def _get_item(self, type_name, uuid): 'organization_bi': OrganizationBI.__name__, 'organization_gemini': OrganizationGemini.__name__, 'power_schedule': PowerSchedule.__name__, + 'employee_email': EmployeeEmail.__name__ } def call_query(base): @@ -91,6 +94,7 @@ def call_pipeline(base): 'organization_gemini': (self.session.query(OrganizationGemini), call_query), 'power_schedule': (self.session.query(PowerSchedule), call_query), + 'employee_email': (self.session.query(EmployeeEmail), call_query), } query_base, func = query_map.get(type_name) @@ -156,6 +160,10 @@ def context(self, **kwargs): 'power_schedule': lambda x: ( 'organization', self._get_item('organization', x.organization_id) ), + 'employee_email': lambda x: ( + 'employee', + self._get_item('employee', x.employee_id) + ), } item = self._get_item(type_name, uuid) source_type = type_name diff --git a/rest_api/rest_api_server/controllers/employee.py b/rest_api/rest_api_server/controllers/employee.py index cf014feb..aee34d26 100644 --- a/rest_api/rest_api_server/controllers/employee.py +++ b/rest_api/rest_api_server/controllers/employee.py @@ -1,6 +1,7 @@ import logging import re import requests +from datetime import datetime, timezone from optscale_client.config_client.client import etcd from sqlalchemy import exists, and_, or_, func from sqlalchemy.exc import IntegrityError @@ -8,8 +9,10 @@ NotFoundException, ConflictException, ForbiddenException, UnauthorizedException, WrongArgumentsException) -from rest_api.rest_api_server.controllers.base import BaseController, MongoMixin -from rest_api.rest_api_server.controllers.base_async import BaseAsyncControllerWrapper +from rest_api.rest_api_server.controllers.base import ( + BaseController, MongoMixin) +from rest_api.rest_api_server.controllers.base_async import ( + BaseAsyncControllerWrapper) from rest_api.rest_api_server.controllers.expense import ( CloudFilteredEmployeeFormattedExpenseController, PoolFilteredEmployeeFormattedExpenseController) @@ -17,12 +20,14 @@ OrganizationConstraintController) from rest_api.rest_api_server.controllers.profiling.base import ( BaseProfilingController) +from rest_api.rest_api_server.controllers.employee_email import ( + EmployeeEmailController) from rest_api.rest_api_server.exceptions import Err from rest_api.rest_api_server.models.enums import ( AuthenticationType, PoolPurposes, RolePurposes) from rest_api.rest_api_server.models.models import ( - AssignmentRequest, Employee, Layout, Organization, Pool, Rule, - ShareableBooking) + AssignmentRequest, Employee, EmployeeEmail, Layout, Organization, Pool, + Rule, ShareableBooking) from rest_api.rest_api_server.utils import Config from optscale_client.auth_client.client_v2 import Client as AuthClient @@ -221,6 +226,15 @@ def list(self, organization_id, last_login=False, **kwargs): result.append(item) return result + def _create_employee_emails(self, employee_id): + emp_email_ctr = EmployeeEmailController(self.session, self._config) + emp_email_ctr.create_all_email_templates(employee_id) + + def create(self, **kwargs): + employee = super().create(**kwargs) + self._create_employee_emails(employee.id) + return employee + def get_expenses(self, employee, start_date, end_date, filter_by): controller_map = { 'cloud': CloudFilteredEmployeeFormattedExpenseController, @@ -331,6 +345,13 @@ def _reassign_resources_to_new_owner(self, new_owner_id, employee, scopes): self.session.rollback() raise WrongArgumentsException(Err.OE0003, [str(ex)]) + def _delete_employee_emails(self, employee_id): + self.session.query(EmployeeEmail).filter( + EmployeeEmail.employee_id == employee_id, + EmployeeEmail.deleted_at == 0 + ).update({EmployeeEmail.deleted_at: int(datetime.now( + tz=timezone.utc).timestamp())}) + def delete(self, item_id, reassign_resources=True, **kwargs): employee = self.get(item_id) scopes = self.get_org_and_pool_summary_map(employee.organization_id) @@ -344,6 +365,8 @@ def delete(self, item_id, reassign_resources=True, **kwargs): raise UnauthorizedException(Err.OE0235, []) raise + self._delete_employee_emails(item_id) + if reassign_resources: new_owner_id = kwargs.get('new_owner_id') user_id = kwargs.get('user_id') diff --git a/rest_api/rest_api_server/controllers/employee_email.py b/rest_api/rest_api_server/controllers/employee_email.py new file mode 100644 index 00000000..aa84f5c3 --- /dev/null +++ b/rest_api/rest_api_server/controllers/employee_email.py @@ -0,0 +1,142 @@ +import logging +from sqlalchemy import and_ +from sqlalchemy.exc import IntegrityError +from tools.optscale_exceptions.common_exc import ( + NotFoundException, WrongArgumentsException +) + +from rest_api.rest_api_server.controllers.base import BaseController +from rest_api.rest_api_server.controllers.base_async import ( + BaseAsyncControllerWrapper +) +from rest_api.rest_api_server.controllers.pool import PoolController +from rest_api.rest_api_server.exceptions import Err +from rest_api.rest_api_server.models.models import Employee, EmployeeEmail + +LOG = logging.getLogger(__name__) + +ROLE_TEMPLATES = { + 'optscale_manager': [ + 'anomaly_detection_alert', + 'new_security_recommendation', + 'saving_spike', + 'organization_policy_expiring_budget', + 'organization_policy_quota', + 'organization_policy_recurring_budget', + 'organization_policy_tagging', + 'pool_exceed_report', + 'pool_owner_violation_report', + 'report_imports_passed_for_org', + 'weekly_expense_report', + 'environment_changes', + 'resource_owner_violation_alert', + ], + 'optscale_engineer': [ + 'pool_exceed_resources_report', + 'resource_owner_violation_report', + 'environment_changes', + 'resource_owner_violation_alert', + ], + 'optscale_member': [ + 'alert', + 'invite', + 'employee_greetings', + ] +} + + +class EmployeeEmailController(BaseController): + + def _get_model_type(self): + return EmployeeEmail + + def get_employee(self, employee_id): + employee = self.session.query(Employee).filter( + Employee.id == employee_id, + Employee.deleted_at == 0).scalar() + if not employee: + raise NotFoundException(Err.OE0002, + [Employee.__name__, employee_id]) + return employee + + def create_all_email_templates(self, employee_id): + email_templates = set(t for t_list in ROLE_TEMPLATES.values() + for t in t_list) + model = self._get_model_type() + for template in email_templates: + employee_email = model(employee_id=employee_id, + email_template=template, + enabled=True) + self.session.add(employee_email) + try: + self.session.commit() + except IntegrityError as exc: + self.session.rollback() + raise WrongArgumentsException(Err.OE0003, [str(exc)]) + + def _get_scopes(self, organization_id): + pool_ctrl = PoolController(self.session, self._config) + pools = pool_ctrl.get_organization_pools(organization_id) + scopes = [x['id'] for x in pools] + scopes.append(organization_id) + return scopes + + def list(self, employee_id, **kwargs): + employee = self.get_employee(employee_id) + model = self._get_model_type() + email_template = kwargs.get('email_template') + employee_emails = self.session.query(model).filter(and_( + model.employee_id == employee_id, + model.deleted_at == 0 + )).all() + if email_template: + employee_emails = self.session.query(model).filter(and_( + model.employee_id == employee_id, + model.deleted_at == 0, + model.email_template == email_template + )).all() + result = {'employee_emails': [ + x.to_dict() for x in employee_emails + ]} + scopes = self._get_scopes(employee.organization_id) + _, roles = self.auth_client.user_roles_get( + [employee.auth_user_id], + scope_ids=scopes + ) + role_purposes = [x['role_purpose'] for x in roles] + available_emails = set( + template for purpose, templates in ROLE_TEMPLATES.items() + for template in templates if purpose in role_purposes + ) + available_emails.update(ROLE_TEMPLATES['optscale_member']) + for employee_email in result['employee_emails']: + employee_email['available_by_role'] = True + if employee_email['email_template'] not in available_emails: + employee_email['available_by_role'] = False + return result + + def bulk_update(self, employee_id, **kwargs): + self.get_employee(employee_id) + enable = kwargs.get('enable', []) + disable = kwargs.get('disable', []) + model = self._get_model_type() + employee_emails = self.session.query(model).filter( + model.employee_id == employee_id, + model.deleted_at == 0, + model.id.in_(enable + disable)).all() + for employee_email in employee_emails: + if employee_email.id in enable: + employee_email.enabled = True + elif employee_email.id in disable: + employee_email.enabled = False + self.session.add(employee_email) + try: + self.session.commit() + except IntegrityError as exc: + raise WrongArgumentsException(Err.OE0003, [str(exc)]) + return self.list(employee_id) + + +class EmployeeEmailAsyncController(BaseAsyncControllerWrapper): + def _get_controller_class(self): + return EmployeeEmailController diff --git a/rest_api/rest_api_server/controllers/invite.py b/rest_api/rest_api_server/controllers/invite.py index 29d6d3b5..2f52286b 100644 --- a/rest_api/rest_api_server/controllers/invite.py +++ b/rest_api/rest_api_server/controllers/invite.py @@ -8,8 +8,11 @@ from etcd import EtcdKeyNotFound from rest_api.rest_api_server.controllers.base import BaseController -from rest_api.rest_api_server.controllers.base_async import BaseAsyncControllerWrapper +from rest_api.rest_api_server.controllers.base_async import ( + BaseAsyncControllerWrapper) from rest_api.rest_api_server.controllers.employee import EmployeeController +from rest_api.rest_api_server.controllers.employee_email import ( + EmployeeEmailController) from rest_api.rest_api_server.exceptions import Err from rest_api.rest_api_server.models.enums import InviteAssignmentScopeTypes from rest_api.rest_api_server.models.models import ( @@ -57,11 +60,29 @@ def get_scopes(self, ids): def get_invite_expiration_days(self): try: - invite_expiration_days = int(self._config.read('/restapi/invite_expiration_days').value) + invite_expiration_days = int( + self._config.read('/restapi/invite_expiration_days').value) except EtcdKeyNotFound: invite_expiration_days = 30 return invite_expiration_days + def _is_invite_email_enabled(self, organization_id, email): + exists, info = self.check_user_exists(email) + if not exists: + return True + employee_ctrl = EmployeeController(self.session, self._config) + employee = employee_ctrl.list(organization_id, + auth_user_id=info['id']) + if employee: + empl_email_ctrl = EmployeeEmailController( + self.session, self._config) + emails = empl_email_ctrl.list( + employee[0]['id'], email_template='invite') + if emails['employee_emails'] and not emails['employee_emails'][0][ + 'enabled']: + return False + return True + def create(self, email, user_id, user_info, invite_assignments: 'list', show_link=False): def get_highest_role(current, new): @@ -136,9 +157,10 @@ def get_highest_role(current, new): invite_url = self.generate_link(email) if show_link: invite_dict['url'] = invite_url - self.send_notification( - email, invite_url, organization.name, organization.id, - organization.currency) + if self._is_invite_email_enabled(organization.id, email): + self.send_notification( + email, invite_url, organization.name, organization.id, + organization.currency) meta = { 'object_name': organization.name, 'email': email, @@ -305,7 +327,8 @@ def generate_link(self, email): base_url=base_url, action='invited', params=params) return url - def send_notification(self, email, url, organization_name, organization_id, currency): + def send_notification(self, email, url, organization_name, organization_id, + currency): subject = 'OptScale invitation notification' template_params = { 'texts': { diff --git a/rest_api/rest_api_server/handlers/v2/__init__.py b/rest_api/rest_api_server/handlers/v2/__init__.py index 6e028378..151e5539 100644 --- a/rest_api/rest_api_server/handlers/v2/__init__.py +++ b/rest_api/rest_api_server/handlers/v2/__init__.py @@ -81,3 +81,4 @@ import rest_api.rest_api_server.handlers.v2.ri_group_breakdowns import rest_api.rest_api_server.handlers.v2.restore_passwords import rest_api.rest_api_server.handlers.v2.verify_emails +import rest_api.rest_api_server.handlers.v2.employee_emails diff --git a/rest_api/rest_api_server/handlers/v2/employee_emails.py b/rest_api/rest_api_server/handlers/v2/employee_emails.py new file mode 100644 index 00000000..ed31349b --- /dev/null +++ b/rest_api/rest_api_server/handlers/v2/employee_emails.py @@ -0,0 +1,179 @@ +import json +from rest_api.rest_api_server.controllers.employee_email import ( + EmployeeEmailAsyncController +) +from rest_api.rest_api_server.handlers.v1.base_async import ( + BaseAsyncCollectionHandler +) +from rest_api.rest_api_server.handlers.v1.base import BaseAuthHandler +from rest_api.rest_api_server.handlers.v2.base import BaseHandler +from rest_api.rest_api_server.utils import ( + check_list_attribute, ModelEncoder, raise_unexpected_exception, run_task +) +from tools.optscale_exceptions.common_exc import WrongArgumentsException +from tools.optscale_exceptions.http_exc import OptHTTPError + + +class EmployeeEmailsAsyncCollectionHandler(BaseAsyncCollectionHandler, + BaseAuthHandler, BaseHandler): + def _get_controller_class(self): + return EmployeeEmailAsyncController + + async def get(self, employee_id): + """ + --- + description: | + Gets a list of employee emails. + Required permission: INFO_ORGANIZATION or CLUSTER_SECRET + tags: [employee_emails] + summary: List of employee emails + parameters: + - name: employee_id + in: path + description: Id of employee + required: true + type: string + - name: email_template + in: query + description: Name of email template + required: false + type: string + responses: + 200: + description: Employee emails list + schema: + type: object + properties: + employee_emails: + type: array + items: + type: object + properties: + id: + type: string + description: Employee email id + deleted_at: + type: string + description: | + Deleted timestamp (service field) + employee_id: + type: string + description: Employee id + enabled: + type: boolean + description: Is email sending enabled + email_template: + type: string + description: Email template name + 401: + description: | + Unauthorized: + - OE0235: Unauthorized + - OE0237: This resource requires authorization + 403: + description: | + Forbidden: + - OE0236: Bad secret + 404: + description: | + Not found: + - OE0002: Employee not found + security: + - token: [] + - secret: [] + """ + if not self.check_cluster_secret(raises=False): + await self.check_permissions( + 'INFO_ORGANIZATION', 'employee', employee_id) + email_template = self.get_arg('email_template', str, None) + res = await run_task(self.controller.list, employee_id, + email_template=email_template) + self.write(json.dumps(res, cls=ModelEncoder)) + + def post(self, *args, **kwargs): + self.raise405() + + +class EmployeeEmailsBulkAsyncCollectionHandler(BaseAsyncCollectionHandler, + BaseAuthHandler, BaseHandler): + def _get_controller_class(self): + return EmployeeEmailAsyncController + + def get(self, *args, **kwargs): + self.raise405() + + def _validate_params(self, **kwargs): + allowed_params = ['enable', 'disable'] + try: + args_unexpected = list(filter( + lambda x: x not in allowed_params, + kwargs.keys())) + if args_unexpected: + raise_unexpected_exception(args_unexpected) + for param in allowed_params: + if param in kwargs and kwargs[param]: + check_list_attribute(param, kwargs[param]) + except WrongArgumentsException as ex: + raise OptHTTPError.from_opt_exception(400, ex) + + async def post(self, employee_id): + """ + --- + description: | + Bulk update employee emails + Required permission: INFO_ORGANIZATION + tags: [employee_emails] + summary: Bulk update employee emails + parameters: + - name: employee_id + in: path + description: Employee id + required: true + type: string + - in: body + name: body + description: Ids of employee emails to enable/disable + required: true + schema: + type: object + required: true + properties: + enable: + type: array + description: list of employee emails ids to enable + items: + type: string + description: employee email id + disable: + type: array + description: list of employee emails ids to disable + items: + type: string + description: employee email id + responses: + 200: + description: Employee emails data + 400: + description: | + Wrong arguments: + - OE0385: Argument should be a list + - OE0212: Unexpected parameters + 401: + description: | + Unauthorized: + - OE0237: This resource requires authorization + 404: + description: | + Not found: + - OE0002: Employee not found + security: + - token: [] + """ + await self.check_permissions( + 'INFO_ORGANIZATION', 'employee', employee_id) + data = self._request_body() + self._validate_params(**data) + res = await run_task( + self.controller.bulk_update, employee_id, **data + ) + self.write(json.dumps(res, cls=ModelEncoder)) diff --git a/rest_api/rest_api_server/models/models.py b/rest_api/rest_api_server/models/models.py index 11396850..06b789d1 100644 --- a/rest_api/rest_api_server/models/models.py +++ b/rest_api/rest_api_server/models/models.py @@ -1700,3 +1700,30 @@ def _validate(self, key, value): @hybrid_property def deleted(self): return false() + + +class EmployeeEmail(Base, ValidatorMixin, MutableMixin): + __tablename__ = 'employee_email' + id = Column(NullableUuid('id'), primary_key=True, default=gen_id, + info=ColumnPermissions.create_only) + employee_id = Column(Uuid('employee_id'), + ForeignKey('employee.id'), + info=ColumnPermissions.create_only, + nullable=False) + employee = relationship("Employee", foreign_keys=[employee_id]) + email_template = Column(MediumLargeNullableString("email_template"), + nullable=False, info=ColumnPermissions.create_only) + enabled = Column(NullableBool('enabled'), nullable=False, default=True, + info=ColumnPermissions.full) + + __table_args__ = ( + UniqueConstraint("employee_id", "email_template", "deleted_at", + name="uc_employee_email_template"),) + + @hybrid_property + def unique_fields(self): + return ["employee_id", "email_template"] + + @validates("employee_id", "enabled", "email_template") + def _validate(self, key, value): + return self.get_validator(key, value) diff --git a/rest_api/rest_api_server/server.py b/rest_api/rest_api_server/server.py index 36a179ad..b60174c4 100644 --- a/rest_api/rest_api_server/server.py +++ b/rest_api/rest_api_server/server.py @@ -540,6 +540,12 @@ def get_handlers(handler_kwargs, version=None): (urls_v2.verify_email, h_v2.verify_emails.VerifyEmailAsyncCollectionHandler, handler_kwargs), + (urls_v2.employee_emails_collection, + h_v2.employee_emails.EmployeeEmailsAsyncCollectionHandler, + handler_kwargs), + (urls_v2.employee_emails_bulk, + h_v2.employee_emails.EmployeeEmailsBulkAsyncCollectionHandler, + handler_kwargs), *profiling_urls, ]) return result diff --git a/rest_api/rest_api_server/tests/unittests/test_employee_email.py b/rest_api/rest_api_server/tests/unittests/test_employee_email.py new file mode 100644 index 00000000..46243e2d --- /dev/null +++ b/rest_api/rest_api_server/tests/unittests/test_employee_email.py @@ -0,0 +1,173 @@ +import uuid +from unittest.mock import patch +from rest_api.rest_api_server.controllers.employee_email import ROLE_TEMPLATES +from rest_api.rest_api_server.tests.unittests.test_api_base import TestApiBase + + +class TestOrganizationApi(TestApiBase): + + def setUp(self, version='v2'): + super().setUp(version) + _, self.org = self.client.organization_create( + {'name': "organization"}) + self.org_id = self.org['id'] + self.user_id = self.gen_id() + self._mock_auth_user(self.user_id) + _, self.employee = self.client.employee_create( + self.org_id, {'name': 'name1', 'auth_user_id': self.user_id}) + patch('rest_api.rest_api_server.controllers.employee_email.' + 'EmployeeEmailController.auth_client').start() + patch('rest_api.rest_api_server.controllers.employee_email.' + 'EmployeeEmailController.auth_client.user_roles_get', + return_value=( + 200, [{'user_id': self.employee['auth_user_id'], + 'role_purpose': 'optscale_engineer'}])).start() + self.valid_params = { + 'employee_id': self.employee['id'], + 'email_template': 'saving_spike', + 'enabled': True + } + + def test_get_employee_emails(self): + code, employee = self.client.employee_create( + self.org_id, {'name': 'name1', 'auth_user_id': str(uuid.uuid4())}) + self.assertEqual(code, 201) + patch('rest_api.rest_api_server.controllers.employee_email.' + 'EmployeeEmailController.auth_client.user_roles_get', + return_value=( + 200, [{'user_id': employee['auth_user_id'], + 'role_purpose': 'optscale_member'}])).start() + emails_num = len(set(t for t_list in ROLE_TEMPLATES.values() + for t in t_list)) + code, employee_emails = self.client.employee_emails_get( + employee['id']) + self.assertEqual(code, 200) + self.assertEqual(len(employee_emails['employee_emails']), emails_num) + self.assertEqual(len([x for x in employee_emails['employee_emails'] + if x['available_by_role']]), + len(ROLE_TEMPLATES['optscale_member'])) + + # employee is manager + patch('rest_api.rest_api_server.controllers.employee_email.' + 'EmployeeEmailController.auth_client.user_roles_get', + return_value=( + 200, [{'user_id': employee['auth_user_id'], + 'role_purpose': 'optscale_manager'}])).start() + code, employee_emails = self.client.employee_emails_get( + employee['id']) + self.assertEqual(code, 200) + self.assertEqual(len(employee_emails['employee_emails']), emails_num) + self.assertEqual( + len([x for x in employee_emails['employee_emails'] + if x['available_by_role']]), + len(ROLE_TEMPLATES['optscale_manager'] + ROLE_TEMPLATES[ + 'optscale_member'])) + + # employee is engineer + patch('rest_api.rest_api_server.controllers.employee_email.' + 'EmployeeEmailController.auth_client.user_roles_get', + return_value=( + 200, [{'user_id': employee['auth_user_id'], + 'role_purpose': 'optscale_engineer'}])).start() + code, employee_emails = self.client.employee_emails_get( + employee['id']) + self.assertEqual(code, 200) + self.assertEqual(len(employee_emails['employee_emails']), emails_num) + self.assertEqual( + len([x for x in employee_emails['employee_emails'] + if x['available_by_role']]), + len(ROLE_TEMPLATES['optscale_engineer'] + ROLE_TEMPLATES[ + 'optscale_member'])) + + def test_employee_email_get_by_email_template(self): + code, employee_emails = self.client.employee_emails_get( + self.employee['id'], email_template='saving_spike') + self.assertEqual(code, 200) + self.assertEqual(len(employee_emails['employee_emails']), 1) + self.assertEqual( + employee_emails['employee_emails'][0]['email_template'], + 'saving_spike' + ) + + def test_employee_email_get_invalid_employee(self): + code, resp = self.client.employee_emails_get('employee_id') + self.assertEqual(code, 404) + self.assertEqual(resp['error']['error_code'], 'OE0002') + + def test_employee_email_bulk(self): + _, resp = self.client.employee_emails_get(self.employee['id']) + employee_email1 = resp['employee_emails'][0]['id'] + employee_email2 = resp['employee_emails'][1]['id'] + params = { + 'enable': [employee_email1], + 'disable': [employee_email2] + } + code, resp = self.client.employee_emails_bulk( + self.employee['id'], params) + self.assertEqual(code, 200) + emp_email1 = list(filter(lambda x: x['id'] == employee_email1, + resp['employee_emails'])) + self.assertEqual(emp_email1[0]['enabled'], True) + emp_email2 = list(filter(lambda x: x['id'] == employee_email2, + resp['employee_emails'])) + self.assertEqual(emp_email2[0]['enabled'], False) + + def test_employee_email_bulk_invalid_id(self): + params = { + 'enable': ['test'] + } + code, resp = self.client.employee_emails_bulk( + self.employee['id'], params) + self.assertEqual(code, 200) + + def test_employee_email_bulk_empty(self): + _, resp = self.client.employee_emails_get(self.employee['id']) + code, resp = self.client.employee_emails_bulk( + self.employee['id'], {}) + self.assertEqual(code, 200) + emails_num = len(set(t for t_list in ROLE_TEMPLATES.values() + for t in t_list)) + self.assertEqual(len(resp['employee_emails']), emails_num) + + def test_employee_email_bulk_unexpected(self): + _, resp = self.client.employee_emails_get(self.employee['id']) + code, resp = self.client.employee_emails_bulk( + self.employee['id'], {'unexpected': 'param'}) + self.assertEqual(code, 400) + self.assertEqual(resp['error']['error_code'], 'OE0212') + + def test_employee_email_bulk_invalid_params(self): + for param in ['enable', 'disable']: + for value in ['test', 123, {'test': 123}]: + code, resp = self.client.employee_emails_bulk( + self.employee['id'], {param: value}) + self.assertEqual(code, 400) + self.assertEqual(resp['error']['error_code'], 'OE0385') + + def test_employee_email_bulk_invalid_employee(self): + code, resp = self.client.employee_emails_bulk('employee_id', + {'enable': ['test']}) + self.assertEqual(code, 404) + self.assertEqual(resp['error']['error_code'], 'OE0002') + + def test_employee_email_not_allowed(self): + url = self.client.employee_emails_url(self.employee['id']) + code, _ = self.client.patch(url, {}) + self.assertEqual(code, 405) + + code, _ = self.client.post(url, {}) + self.assertEqual(code, 405) + + code, _ = self.client.delete(url, {}) + self.assertEqual(code, 405) + + def test_employee_email_bulk_not_allowed(self): + url = self.client.employee_emails_bulk_url(self.employee['id']) + code, _ = self.client.patch(url, {}) + self.assertEqual(code, 405) + + code, _ = self.client.get(url, {}) + self.assertEqual(code, 405) + + code, _ = self.client.delete(url, {}) + self.assertEqual(code, 405) diff --git a/rest_api/rest_api_server/tests/unittests/test_pools.py b/rest_api/rest_api_server/tests/unittests/test_pools.py index f8de1ac7..30de0188 100644 --- a/rest_api/rest_api_server/tests/unittests/test_pools.py +++ b/rest_api/rest_api_server/tests/unittests/test_pools.py @@ -868,8 +868,9 @@ def test_delete_pool_reassign_cleanup(self): resource = self._create_resource( cloud_account['id'], employee2['id'], child_pool['id']) self._mock_auth_user(user2_id) - patch('rest_api.rest_api_server.controllers.assignment.AssignmentController.' - '_authorize_action_for_pool', return_value=True).start() + patch('rest_api.rest_api_server.controllers.assignment.' + 'AssignmentController._authorize_action_for_pool', + return_value=True).start() code, request = self.client.assignment_request_create(self.org_id, { 'resource_id': resource['id'], 'approver_id': employee1['id'], @@ -895,20 +896,27 @@ def test_delete_pool_reassign_cleanup(self): 'role_name': 'Manager', 'role_scope': None}] patch( - 'rest_api.rest_api_server.controllers.invite.InviteController.get_invite_expiration_days', + 'rest_api.rest_api_server.controllers.invite.' + 'InviteController.get_invite_expiration_days', return_value=30).start() patch( - 'rest_api.rest_api_server.controllers.invite.InviteController.check_user_exists', + 'rest_api.rest_api_server.controllers.invite.' + 'InviteController.check_user_exists', return_value=(True, {})).start() patch( - 'rest_api.rest_api_server.controllers.invite.InviteController.get_user_auth_assignments', + 'rest_api.rest_api_server.controllers.invite.' + 'InviteController.get_user_auth_assignments', return_value=user_assignments).start() patch( - 'rest_api.rest_api_server.handlers.v1.base.BaseAuthHandler._get_user_info', + 'rest_api.rest_api_server.handlers.v1.base.' + 'BaseAuthHandler._get_user_info', return_value={ 'display_name': 'default', 'email': 'email@email.com' }).start() + patch('rest_api.rest_api_server.controllers.invite.' + 'InviteController.check_user_exists', + return_value=(False, {})).start() code, invites = self.client.invite_create({ 'invites': { 'some@email.com': [ @@ -927,7 +935,8 @@ def test_delete_pool_reassign_cleanup(self): _, ar = self.client.assignment_request_list(self.org_id) self.assertEqual(len(ar['assignment_requests']['outgoing']), 0) patch( - 'rest_api.rest_api_server.handlers.v1.base.BaseAuthHandler._get_user_info', + 'rest_api.rest_api_server.handlers.v1.base.' + 'BaseAuthHandler._get_user_info', return_value={ 'display_name': 'default', 'email': 'some@email.com' From 936dc529c458c7a3928c4d5bf29b7840a9693859 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Wed, 4 Dec 2024 12:03:55 +0300 Subject: [PATCH 05/65] OS-8003. inactive_users for gcp --- .../modules/recommendations/inactive_users.py | 45 ++++++++++++++++--- tools/cloud_adapter/clouds/gcp.py | 22 +++++++-- tools/cloud_adapter/setup.py | 1 + 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/bumiworker/bumiworker/modules/recommendations/inactive_users.py b/bumiworker/bumiworker/modules/recommendations/inactive_users.py index ab93231c..cb10d248 100644 --- a/bumiworker/bumiworker/modules/recommendations/inactive_users.py +++ b/bumiworker/bumiworker/modules/recommendations/inactive_users.py @@ -5,6 +5,8 @@ DEFAULT_DAYS_THRESHOLD = 90 +INTERVAL = 300 +GCP_METRIC_NAME = 'iam.googleapis.com/service_account/authn_events_count' MSEC_IN_SEC = 1000 LOG = logging.getLogger(__name__) @@ -12,6 +14,7 @@ class InactiveUsers(InactiveUsersBase): SUPPORTED_CLOUD_TYPES = [ 'aws_cnr', + 'gcp_cnr', 'nebius' ] @@ -28,6 +31,8 @@ def list_users(self, cloud_adapter): result = [] for folder_id in cloud_adapter.folders: result.extend(cloud_adapter.service_accounts_list(folder_id)) + elif cloud_type == 'gcp_cnr': + result = cloud_adapter.service_accounts_list() else: result = cloud_adapter.list_users() return result @@ -56,6 +61,33 @@ def is_outdated(last_used_): 'last_used': int(last_used.timestamp()) } + def handle_gcp_user(self, user, now, cloud_adapter, days_threshold): + last_used = 0 + service_account_id = user.unique_id + inactive_threshold = self._get_inactive_threshold(days_threshold) + end_date = now + # there is no created_at for service account, so extend dates range to + # try to get last_used + start_date = now - inactive_threshold - inactive_threshold + service_account_usage = cloud_adapter.get_metric( + GCP_METRIC_NAME, [service_account_id], INTERVAL, start_date, + end_date, id_field='unique_id' + ) + used_dates = [ + point.interval.end_time for data in service_account_usage + for point in data.points if point.value.double_value != 0 + ] + if used_dates: + last_used_dt = max(used_dates) + last_used = int(last_used_dt.timestamp()) + if not self._is_outdated(now, last_used_dt, inactive_threshold): + return + return { + 'user_name': user.display_name, + 'user_id': service_account_id, + 'last_used': last_used + } + def handle_nebius_user(self, user, now, cloud_adapter, days_threshold): service_account_id = user['id'] folder_id = user['folderId'] @@ -99,12 +131,13 @@ def handle_nebius_user(self, user, now, cloud_adapter, days_threshold): def handle_user(self, user, now, cloud_adapter, days_threshold): cloud_type = cloud_adapter.config['type'] - if cloud_type == 'aws_cnr': - return self.handle_aws_user(user, now, cloud_adapter, - days_threshold) - else: - return self.handle_nebius_user(user, now, cloud_adapter, - days_threshold) + cloud_func_map = { + "aws_cnr": self.handle_aws_user, + "gcp_cnr": self.handle_gcp_user, + "nebius": self.handle_nebius_user, + } + func = cloud_func_map[cloud_type] + return func(user, now, cloud_adapter, days_threshold) def main(organization_id, config_client, created_at, **kwargs): diff --git a/tools/cloud_adapter/clouds/gcp.py b/tools/cloud_adapter/clouds/gcp.py index 9992738b..f5093c2d 100644 --- a/tools/cloud_adapter/clouds/gcp.py +++ b/tools/cloud_adapter/clouds/gcp.py @@ -14,6 +14,7 @@ from google.cloud import compute from google.cloud import storage from google.cloud import monitoring_v3 +from google.cloud.iam_admin_v1 import IAMClient, types import tools.cloud_adapter.exceptions import tools.cloud_adapter.model @@ -731,6 +732,12 @@ def compute_networks_client(self): self.credentials, ) + @cached_property + def iam_client(self): + return IAMClient.from_service_account_info( + self.credentials, + ) + @cached_property def storage_client(self): return storage.Client.from_service_account_info( @@ -1337,21 +1344,23 @@ def _metrics_aggregation(interval): ) @staticmethod - def _metrics_filter(metric_name, instance_ids): + def _metrics_filter(metric_name, instance_ids, id_field): type_filter = f'metric.type = "{metric_name}"' instance_ids_filter = " OR ".join( [ - "resource.labels.instance_id = " + instance_id + f"resource.labels.{id_field} = " + instance_id for instance_id in instance_ids ] ) return f"{type_filter} AND ({instance_ids_filter})" - def get_metric(self, metric_name, instance_ids, interval, start_date, end_date): + def get_metric(self, metric_name, instance_ids, interval, start_date, + end_date, id_field="instance_id"): results = self.metrics_client.list_time_series( request={ "name": f"projects/{self.project_id}", - "filter": self._metrics_filter(metric_name, instance_ids), + "filter": self._metrics_filter(metric_name, instance_ids, + id_field), "interval": self._metrics_interval(start_date, end_date), "view": monitoring_v3.ListTimeSeriesRequest.TimeSeriesView.FULL, "aggregation": self._metrics_aggregation(interval), @@ -1602,6 +1611,11 @@ def _get_region_locations(self, region: str) -> list[str]: raise RegionNotFoundException(f"Region `{region}` was not found in cloud") return locations + def service_accounts_list(self): + request = types.ListServiceAccountsRequest() + request.name = f"projects/{self.project_id}" + return list(self.iam_client.list_service_accounts(request=request)) + def start_instance(self, instance_name, zone): try: self.compute_instances_client.start( diff --git a/tools/cloud_adapter/setup.py b/tools/cloud_adapter/setup.py index 60d0cfde..7726f3d0 100644 --- a/tools/cloud_adapter/setup.py +++ b/tools/cloud_adapter/setup.py @@ -30,6 +30,7 @@ "azure-identity==1.6.1", # Gcp + 'google-cloud-iam==2.16.1', 'google-cloud-bigquery==3.11.4', 'google-cloud-compute==1.14.1', 'google-cloud-storage==2.10.0', From daaa7dafd07ec1b3bc80f6134606a106bcd35d86 Mon Sep 17 00:00:00 2001 From: ek-hystax <33006768+ek-hystax@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:28:09 +0400 Subject: [PATCH 06/65] OS-8043. Add the GCP service to the "Inactive IAM users" recommendation --- .../recommendations/InactiveUsers.tsx | 8 ++++---- ngui/ui/src/hooks/useRecommendationServices.ts | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ngui/ui/src/containers/RecommendationsOverviewContainer/recommendations/InactiveUsers.tsx b/ngui/ui/src/containers/RecommendationsOverviewContainer/recommendations/InactiveUsers.tsx index 48495936..8b9cc769 100644 --- a/ngui/ui/src/containers/RecommendationsOverviewContainer/recommendations/InactiveUsers.tsx +++ b/ngui/ui/src/containers/RecommendationsOverviewContainer/recommendations/InactiveUsers.tsx @@ -1,7 +1,7 @@ import InactiveUsersModal from "components/SideModalManager/SideModals/recommendations/InactiveUsersModal"; -import { AWS_IAM, NEBIUS_SERVICE } from "hooks/useRecommendationServices"; +import { AWS_IAM, GCP_IAM, NEBIUS_SERVICE } from "hooks/useRecommendationServices"; import { detectedAt, lastUsed, name, userLocation } from "utils/columns"; -import { AWS_CNR, NEBIUS } from "utils/constants"; +import { AWS_CNR, GCP_CNR, NEBIUS } from "utils/constants"; import BaseRecommendation, { CATEGORY_SECURITY } from "./BaseRecommendation"; const columns = [ @@ -28,9 +28,9 @@ class InactiveUsers extends BaseRecommendation { emptyMessageId = "noInactiveUsers"; - services = [AWS_IAM, NEBIUS_SERVICE]; + services = [AWS_IAM, NEBIUS_SERVICE, GCP_IAM]; - appliedDataSources = [AWS_CNR, NEBIUS]; + appliedDataSources = [AWS_CNR, NEBIUS, GCP_CNR]; categories = [CATEGORY_SECURITY]; diff --git a/ngui/ui/src/hooks/useRecommendationServices.ts b/ngui/ui/src/hooks/useRecommendationServices.ts index 4eee191f..1df24cae 100644 --- a/ngui/ui/src/hooks/useRecommendationServices.ts +++ b/ngui/ui/src/hooks/useRecommendationServices.ts @@ -20,6 +20,7 @@ export const AZURE_COMPUTE = "azureCompute"; export const AZURE_NETWORK = "azureNetwork"; export const GCP_COMPUTE_ENGINE = "gcpComputeEngine"; +export const GCP_IAM = "gcpAim"; export const NEBIUS_SERVICE = "nebius"; @@ -88,6 +89,10 @@ const GCP_SERVICES = Object.freeze({ [GCP_COMPUTE_ENGINE]: { type: GCP_CNR, name: "services.computeEngine" + }, + [GCP_IAM]: { + type: GCP_CNR, + name: "services.iam" } }); From f88aa20f81d86f46596506e53997221e69dd28ed Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Thu, 5 Dec 2024 09:27:41 +0300 Subject: [PATCH 07/65] OS-8047. Fixed invalid reason for gcp inactive_users archive --- bumiworker/bumiworker/modules/archive/inactive_users.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bumiworker/bumiworker/modules/archive/inactive_users.py b/bumiworker/bumiworker/modules/archive/inactive_users.py index 81cbe495..ad5fde9d 100644 --- a/bumiworker/bumiworker/modules/archive/inactive_users.py +++ b/bumiworker/bumiworker/modules/archive/inactive_users.py @@ -1,7 +1,8 @@ import logging from bumiworker.bumiworker.consts import ArchiveReason -from bumiworker.bumiworker.modules.inactive_users_base import ArchiveInactiveUsersBase +from bumiworker.bumiworker.modules.inactive_users_base import ( + ArchiveInactiveUsersBase) from bumiworker.bumiworker.modules.recommendations.inactive_users import ( InactiveUsers as InactiveUsersRecommendation) @@ -12,6 +13,7 @@ class InactiveUsers(ArchiveInactiveUsersBase, InactiveUsersRecommendation): SUPPORTED_CLOUD_TYPES = [ 'aws_cnr', + 'gcp_cnr', 'nebius' ] From 696f3dc1d0b55d30f8b96c3ec29f4597beaa4647 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:17:06 +0300 Subject: [PATCH 08/65] OS-8040. Changed regions for discovering RDS + new region on Alibaba --- tools/cloud_adapter/clouds/alibaba.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tools/cloud_adapter/clouds/alibaba.py b/tools/cloud_adapter/clouds/alibaba.py index 1055df8a..5530b610 100644 --- a/tools/cloud_adapter/clouds/alibaba.py +++ b/tools/cloud_adapter/clouds/alibaba.py @@ -43,6 +43,7 @@ from aliyunsdkrds.request.v20140815 import ( DescribeAvailableClassesRequest, DescribeDBInstancesRequest, + DescribeRegionsRequest as DescribeRdsRegionsRequest, DescribeTagsRequest as DescribeRdsTagsRequest, DescribeDBInstanceAttributeRequest, ) @@ -271,6 +272,11 @@ def _list_region_details(self): regions = self._send_request(request)['Regions']['Region'] return self._exclude_closed_regions(regions) + def _list_rds_region_details(self): + request = DescribeRdsRegionsRequest.DescribeRegionsRequest() + regions = self._send_request(request)['Regions']['RDSRegion'] + return self._exclude_closed_regions(regions) + def _find_region(self, id_or_name): if id_or_name not in self._regions_map: for region_info in self._list_region_details(): @@ -641,11 +647,8 @@ def snapshot_chain_discovery_calls(self): for r in self._list_region_details()] def rds_instance_discovery_calls(self): - excluded_regions = ['cn-wuhan-lr'] - # rds instances discover in this regions raises error for some reasons return [(self._discover_region_rds_instances, (r,)) - for r in self._list_region_details() - if r['RegionId'] not in excluded_regions] + for r in self._list_rds_region_details()] def ip_address_discovery_calls(self): return [(self._discover_ip_addresses, (r,)) @@ -713,6 +716,9 @@ def _get_coordinates_map(self): 'cn-wulanchabu': { 'name': 'China (Ulanqab)', 'longitude': 113.0597863, 'latitude': 41.0177905}, + 'cn-wulanchabu-acdr-1': { + 'name': 'Wulanchabu HDG ACDR', + 'longitude': 113.132585, 'latitude': 40.994786}, 'cn-hangzhou': { 'name': 'China (Hangzhou)', 'longitude': 120.0314647, 'latitude': 30.2613156}, From c65660c6ff21dce15fa7ddf07e93e5211172371c Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Thu, 5 Dec 2024 13:17:55 +0300 Subject: [PATCH 09/65] OS-7996. GCP s3_public_buckets support --- .../recommendations/s3_public_buckets.py | 1 + tools/cloud_adapter/clouds/gcp.py | 21 ++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/bumiworker/bumiworker/modules/recommendations/s3_public_buckets.py b/bumiworker/bumiworker/modules/recommendations/s3_public_buckets.py index 24e59b02..41a133ab 100644 --- a/bumiworker/bumiworker/modules/recommendations/s3_public_buckets.py +++ b/bumiworker/bumiworker/modules/recommendations/s3_public_buckets.py @@ -5,6 +5,7 @@ SUPPORTED_CLOUD_TYPES = [ 'aws_cnr', + 'gcp_cnr', 'nebius' ] diff --git a/tools/cloud_adapter/clouds/gcp.py b/tools/cloud_adapter/clouds/gcp.py index f5093c2d..5a5994cd 100644 --- a/tools/cloud_adapter/clouds/gcp.py +++ b/tools/cloud_adapter/clouds/gcp.py @@ -488,13 +488,28 @@ def post_discover(self): class GcpBucket(tools.cloud_adapter.model.BucketResource, GcpResource): + def __init__(self, cloud_bucket: storage.Bucket, cloud_adapter): GcpResource.__init__(self, cloud_bucket, cloud_adapter) + is_public_acls = False + is_public_policy = False + iam_policy = cloud_bucket.get_iam_policy() + iam = cloud_bucket.iam_configuration + if iam.public_access_prevention != 'enforced': + for binding in iam_policy.bindings: + if "allUsers" in binding["members"]: + is_public_policy = True + break + if not iam.uniform_bucket_level_access_enabled: + acls = list(cloud_bucket.acl) + for acl in acls: + if acl["entity"] == "allUsers": + is_public_acls = True + break super().__init__( **self._common_fields, - # TODO: how to detect public buckets? - is_public_policy=False, - is_public_acls=False, + is_public_policy=is_public_policy, + is_public_acls=is_public_acls, ) def _get_console_link(self): From 3a336874aab2146d1c9c40c2848bf652b25c489e Mon Sep 17 00:00:00 2001 From: ek-hystax <33006768+ek-hystax@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:18:09 +0400 Subject: [PATCH 10/65] OS-8046. Add the GCP service to the "Public S3 buckets" recommendation --- .../recommendations/PublicS3Buckets.tsx | 8 ++++---- ngui/ui/src/hooks/useRecommendationServices.ts | 5 +++++ ngui/ui/src/translations/en-US/app.json | 5 +++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/ngui/ui/src/containers/RecommendationsOverviewContainer/recommendations/PublicS3Buckets.tsx b/ngui/ui/src/containers/RecommendationsOverviewContainer/recommendations/PublicS3Buckets.tsx index 8b963f73..699b1730 100644 --- a/ngui/ui/src/containers/RecommendationsOverviewContainer/recommendations/PublicS3Buckets.tsx +++ b/ngui/ui/src/containers/RecommendationsOverviewContainer/recommendations/PublicS3Buckets.tsx @@ -1,9 +1,9 @@ import { FormattedMessage } from "react-intl"; import RecommendationListItemResourceLabel from "components/RecommendationListItemResourceLabel"; import TextWithDataTestId from "components/TextWithDataTestId"; -import { AWS_S3, NEBIUS_SERVICE } from "hooks/useRecommendationServices"; +import { AWS_S3, GCP_CLOUD_STORAGE, NEBIUS_SERVICE } from "hooks/useRecommendationServices"; import { detectedAt, poolOwner, resource, resourceLocation } from "utils/columns"; -import { AWS_CNR, NEBIUS } from "utils/constants"; +import { AWS_CNR, GCP_CNR, NEBIUS } from "utils/constants"; import BaseRecommendation, { CATEGORY_SECURITY } from "./BaseRecommendation"; const columns = [ @@ -58,9 +58,9 @@ class PublicS3Buckets extends BaseRecommendation { emptyMessageId = "noPublicS3Buckets"; - services = [AWS_S3, NEBIUS_SERVICE]; + services = [AWS_S3, NEBIUS_SERVICE, GCP_CLOUD_STORAGE]; - appliedDataSources = [AWS_CNR, NEBIUS]; + appliedDataSources = [AWS_CNR, NEBIUS, GCP_CNR]; categories = [CATEGORY_SECURITY]; diff --git a/ngui/ui/src/hooks/useRecommendationServices.ts b/ngui/ui/src/hooks/useRecommendationServices.ts index 1df24cae..79138d99 100644 --- a/ngui/ui/src/hooks/useRecommendationServices.ts +++ b/ngui/ui/src/hooks/useRecommendationServices.ts @@ -21,6 +21,7 @@ export const AZURE_NETWORK = "azureNetwork"; export const GCP_COMPUTE_ENGINE = "gcpComputeEngine"; export const GCP_IAM = "gcpAim"; +export const GCP_CLOUD_STORAGE = "gcpCloudStorage"; export const NEBIUS_SERVICE = "nebius"; @@ -93,6 +94,10 @@ const GCP_SERVICES = Object.freeze({ [GCP_IAM]: { type: GCP_CNR, name: "services.iam" + }, + [GCP_CLOUD_STORAGE]: { + type: GCP_CNR, + name: "services.cloudStorage" } }); diff --git a/ngui/ui/src/translations/en-US/app.json b/ngui/ui/src/translations/en-US/app.json index 94406434..0746c0d5 100644 --- a/ngui/ui/src/translations/en-US/app.json +++ b/ngui/ui/src/translations/en-US/app.json @@ -629,8 +629,6 @@ "edit{}": "Edit {value}", "eitherMinOrMaxMustBeDefined": "Either minimimum or maximum must be defined", "email": "Email", - "emailVerificationDescription": "To verify your email, please enter the verification code sent to:", - "emailVerifiedSuccessfully": "Email has been verified successfully!", "emailNotifications": "Email notifications", "emailTemplates.accountManagement.invite.description": "Notification of an invitation to join OptScale", "emailTemplates.accountManagement.invite.title": "Invitation notification", @@ -666,6 +664,8 @@ "emailTemplates.systemNotifications.environment_changes.title": "Environment changed", "emailTemplates.systemNotifications.report_imports_passed_for_org.description": "Confirmation that initial expense processing for your organization is complete", "emailTemplates.systemNotifications.report_imports_passed_for_org.title": "Expenses initial processing completed", + "emailVerificationDescription": "To verify your email, please enter the verification code sent to:", + "emailVerifiedSuccessfully": "Email has been verified successfully!", "employee": "Employee", "enabled": "Enabled", "endDate": "End date", @@ -1983,6 +1983,7 @@ "serverError": "Server error", "service": "Service", "serviceAccountId": "Service account ID", + "services.cloudStorage": "Cloud Storage", "services.compute": "Compute", "services.computeEngine": "Compute Engine", "services.ebs": "EBS", From 949a8a842f0cb64be1a9169de60e1640cff4b1f0 Mon Sep 17 00:00:00 2001 From: v-hx <146182370+v-hx@users.noreply.github.com> Date: Fri, 6 Dec 2024 09:17:24 +0300 Subject: [PATCH 11/65] OS-7831: Reimplemented error boundary (#1210) This is supposed to reset the error caught by the error boundary when navigating to a different location. https://datatrendstech.atlassian.net/browse/OS-7831 --- .../ErrorBoundary/ErrorBoundary.tsx | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/ngui/ui/src/components/ErrorBoundary/ErrorBoundary.tsx b/ngui/ui/src/components/ErrorBoundary/ErrorBoundary.tsx index 3b638757..397a2727 100644 --- a/ngui/ui/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/ngui/ui/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -1,24 +1,40 @@ -import { Component } from "react"; +import { Component, PropsWithChildren } from "react"; +import { useLocation, type Location } from "react-router-dom"; import SomethingWentWrong from "components/SomethingWentWrong"; -class ErrorBoundary extends Component { - static defaultProps = { - FallbackComponent: SomethingWentWrong - }; +type ErrorBoundaryState = { + hasError: boolean; +}; - state = { - error: null, - info: null - }; +type ErrorBoundaryProps = PropsWithChildren<{ + location: Location; +}>; - componentDidCatch(error, info) { - this.setState({ error, info }); +class ErrorBoundaryInner extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + // Set the local state when the error is caught to display a fallback error component + static getDerivedStateFromError(): Partial { + return { hasError: true }; + } + + componentDidUpdate(prevProps: ErrorBoundaryProps) { + if (prevProps.location.key !== this.props.location.key) { + this.setState({ hasError: false }); + } } render() { - const { FallbackComponent } = this.props; - return this.state.error === null ? this.props.children : ; + return this.state.hasError ? : this.props.children; } } +const ErrorBoundary = ({ children }: PropsWithChildren) => { + const location = useLocation(); + return {children}; +}; + export default ErrorBoundary; From 2a5490bce79bd7f9ba501784f602b31d0f5a72dd Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Fri, 6 Dec 2024 09:21:58 +0300 Subject: [PATCH 12/65] OS-7988. Archived recommendation, checklist, webhook collections clean in cleanmongodb --- docker_images/cleanmongodb/clean-mongo-db.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docker_images/cleanmongodb/clean-mongo-db.py b/docker_images/cleanmongodb/clean-mongo-db.py index 36e37be6..9a005627 100644 --- a/docker_images/cleanmongodb/clean-mongo-db.py +++ b/docker_images/cleanmongodb/clean-mongo-db.py @@ -29,6 +29,11 @@ def __init__(self): # linked to cloud_account_id self.mongo_client.restapi.raw_expenses: ROWS_LIMIT, self.mongo_client.restapi.resources: ROWS_LIMIT, + # linked to organization_id + self.mongo_client.restapi.archived_recommendations: ROWS_LIMIT, + self.mongo_client.restapi.checklists: ROWS_LIMIT, + self.mongo_client.restapi.webhook_observer: ROWS_LIMIT, + self.mongo_client.restapi.webhook_logs: ROWS_LIMIT, # linked to run_id self.mongo_client.arcee.console: ROWS_LIMIT, self.mongo_client.arcee.log: ROWS_LIMIT, @@ -318,11 +323,17 @@ def split_chunk_by_files(self, chunk, available_rows_count, filename, return result def _delete_by_organization(self, org_id, token, infra_token): + restapi_collections = [ + self.mongo_client.restapi.archived_recommendations, + self.mongo_client.restapi.checklists, + # delete clusters resources + self.mongo_client.restapi.resources, + self.mongo_client.restapi.webhook_observer, + self.mongo_client.restapi.webhook_logs + ] keeper_collections = [ self.mongo_client.keeper.event ] - # delete clusters resources - restapi_collections = [self.mongo_client.restapi.resources] # delete ml objects arcee_collections = [self.mongo_client.arcee.dataset, self.mongo_client.arcee.metric, From 261ef1f0fa73c01231b491e8600703472d92c045 Mon Sep 17 00:00:00 2001 From: alis-hx <161693960+alis-hx@users.noreply.github.com> Date: Fri, 6 Dec 2024 10:22:05 +0300 Subject: [PATCH 13/65] OS-7921 [Community Documentation] Add tips for Assignment Rules page (#1203) ## Description [Community Documentation] Add tips for Assignment Rules page --- ngui/ui/public/docs/assignment-rules.md | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 ngui/ui/public/docs/assignment-rules.md diff --git a/ngui/ui/public/docs/assignment-rules.md b/ngui/ui/public/docs/assignment-rules.md new file mode 100644 index 00000000..ceb92c77 --- /dev/null +++ b/ngui/ui/public/docs/assignment-rules.md @@ -0,0 +1,28 @@ +### **Summary** + +**Assignment Rules** is an interface for viewing and managing all existing assignment rules in the system. +Get a list of rules displayed in a tabular format. View the status of each rule and take appropriate actions directly from this page. + +### **View** + +- Details: Monitor to whom the resource is assigned, a summary of the conditions that trigger the rule, and the priority of each assignment rule. + +- Filter the table: the 'Search' feature to refine the data. + +### **Actions** + +- Permission limitations: Use the buttons and table to access available actions based on your permissions. + +- Add an Assignment Rule: Easily create a new rule by clicking the green "Add" button. Specify the name, conditions, and assign to fields. + +- Update Priorities: Use the Actions column buttons to manage rules. + +- Re-apply Ruleset Action: Initiate a new check of the already assigned resources against the current ruleset. + +### **Tips** + +- Prioritize Critical Tasks: Assign resources to high-priority tasks first, +ensuring that critical operations or deadlines are met before allocating resources to less urgent activities. + +- Balance Workload: Distribute resources evenly across tasks to avoid overloading +any single resource. \ No newline at end of file From eb9ceb07c77679770b71ff9c1b693c7e588d9412 Mon Sep 17 00:00:00 2001 From: ek-hystax <33006768+ek-hystax@users.noreply.github.com> Date: Fri, 6 Dec 2024 13:52:30 +0400 Subject: [PATCH 14/65] OS-8051. Fix assignment-rules.md file name --- ngui/ui/public/docs/{assignment-rules.md => assignment-rules.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ngui/ui/public/docs/{assignment-rules.md => assignment-rules.md} (100%) diff --git a/ngui/ui/public/docs/assignment-rules.md b/ngui/ui/public/docs/assignment-rules.md similarity index 100% rename from ngui/ui/public/docs/assignment-rules.md rename to ngui/ui/public/docs/assignment-rules.md From 4686d763148f7abe1d1e4c069e8c87bb1de19738 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:34:04 +0300 Subject: [PATCH 15/65] OS-8048. Support token param for get layout and list artifacts --- optscale_client/rest_api_client/client_v2.py | 11 +++-- .../rest_api_server/handlers/v2/layouts.py | 25 ++++++++--- .../handlers/v2/profiling/artifacts.py | 26 ++++++++--- .../handlers/v2/profiling/executors.py | 4 +- .../handlers/v2/profiling/runs.py | 5 ++- .../tests/unittests/test_layouts.py | 18 ++++++-- .../unittests/test_profiling_artifacts.py | 44 +++++++++++++++++++ .../tests/unittests/test_profiling_base.py | 5 +++ .../unittests/test_profiling_executors.py | 11 +++++ .../tests/unittests/test_profiling_runs.py | 32 ++++++++------ 10 files changed, 144 insertions(+), 37 deletions(-) diff --git a/optscale_client/rest_api_client/client_v2.py b/optscale_client/rest_api_client/client_v2.py index dd99910b..f1cfb412 100644 --- a/optscale_client/rest_api_client/client_v2.py +++ b/optscale_client/rest_api_client/client_v2.py @@ -2106,8 +2106,10 @@ def layouts_list(self, org_id, layout_type=None, include_shared=False, entity_id=entity_id, token=token) return self.get(url) - def layout_get(self, org_id, layout_id): + def layout_get(self, org_id, layout_id, token=None): url = self.layouts_url(org_id, layout_id) + if token: + url += self.query_url(token=token) return self.get(url) def layout_update(self, org_id, layout_id, params): @@ -2197,8 +2199,11 @@ def artifact_update(self, org_id, artifact_id, **params): def artifacts_get(self, org_id, **params): return self.get(self.artifacts_url(org_id) + self.query_url(**params)) - def artifact_get(self, org_id, artifact_id): - return self.get(self.artifacts_url(org_id, artifact_id)) + def artifact_get(self, org_id, artifact_id, token=None): + url = self.artifacts_url(org_id, artifact_id) + if token: + url += self.query_url(token=token) + return self.get(url) def artifact_delete(self, org_id, artifact_id): return self.delete(self.artifacts_url(org_id, artifact_id)) diff --git a/rest_api/rest_api_server/handlers/v2/layouts.py b/rest_api/rest_api_server/handlers/v2/layouts.py index 0db5face..d7751e5f 100644 --- a/rest_api/rest_api_server/handlers/v2/layouts.py +++ b/rest_api/rest_api_server/handlers/v2/layouts.py @@ -1,13 +1,12 @@ import json from rest_api.rest_api_server.controllers.layout import LayoutsAsyncController -from rest_api.rest_api_server.handlers.v2.base import BaseHandler from rest_api.rest_api_server.handlers.v2.profiling.base import ( ProfilingHandler) from rest_api.rest_api_server.handlers.v1.base_async import ( BaseAsyncItemHandler, BaseAsyncCollectionHandler) from rest_api.rest_api_server.handlers.v1.base import ( - BaseAuthHandler, BaseAuthQueryTokenHandler) + BaseAuthQueryTokenHandler) from rest_api.rest_api_server.utils import (run_task, ModelEncoder) @@ -222,7 +221,7 @@ async def post(self, organization_id, **_url_params): class LayoutsAsyncItemHandler( - BaseAsyncItemHandler, BaseAuthHandler, BaseHandler): + BaseAsyncItemHandler, BaseAuthQueryTokenHandler, ProfilingHandler): def _get_controller_class(self): return LayoutsAsyncController @@ -245,6 +244,12 @@ async def get(self, organization_id, layout_id): description: Layout id required: true type: string + - name: token + in: query + description: | + Unique token related to organization profiling token + required: false + type: string responses: 200: description: Layout @@ -281,11 +286,19 @@ async def get(self, organization_id, layout_id): security: - token: [] """ - await self.check_permissions( + secret = False + token = self.get_arg('token', str, None) + if await self.check_md5_profiling_token( + organization_id, token, raises=False): + user_id = None + secret = True + else: + await self.check_permissions( 'INFO_ORGANIZATION', 'organization', organization_id) - user_id = await self.check_self_auth() + user_id = await self.check_self_auth() res = await run_task( - self.controller.get_item, user_id, organization_id, layout_id) + self.controller.get_item, user_id, organization_id, layout_id, + secret=secret) self.write(json.dumps(res.to_dict())) async def patch(self, organization_id, layout_id): diff --git a/rest_api/rest_api_server/handlers/v2/profiling/artifacts.py b/rest_api/rest_api_server/handlers/v2/profiling/artifacts.py index 21d1f7a9..6cf82e70 100644 --- a/rest_api/rest_api_server/handlers/v2/profiling/artifacts.py +++ b/rest_api/rest_api_server/handlers/v2/profiling/artifacts.py @@ -5,7 +5,8 @@ ArtifactAsyncController) from rest_api.rest_api_server.handlers.v1.base_async import ( BaseAsyncCollectionHandler, BaseAsyncItemHandler) -from rest_api.rest_api_server.handlers.v1.base import BaseAuthHandler +from rest_api.rest_api_server.handlers.v1.base import ( + BaseAuthHandler, BaseAuthQueryTokenHandler) from rest_api.rest_api_server.handlers.v2.profiling.base import ( ProfilingHandler) from rest_api.rest_api_server.exceptions import Err @@ -50,8 +51,9 @@ def _validate_params(data, is_new=True): raise OptHTTPError.from_opt_exception(400, exc) -class ArtifactsAsyncCollectionHandler(BaseAsyncCollectionHandler, - BaseAuthHandler, ProfilingHandler): +class ArtifactsAsyncCollectionHandler( + BaseAsyncCollectionHandler, BaseAuthQueryTokenHandler, + ProfilingHandler): def _get_controller_class(self): return ArtifactAsyncController @@ -228,6 +230,13 @@ async def get(self, organization_id, **url_params): description: return artifacts starting from this number required: false type: integer + - name: token + in: query + description: | + Unique token related to organization profiling token + (only with run_id) + required: false + type: string responses: 200: description: Organization artifacts list @@ -293,11 +302,14 @@ async def get(self, organization_id, **url_params): security: - token: [] """ - await self.check_permissions( - 'INFO_ORGANIZATION', 'organization', organization_id) - token = await self._get_profiling_token(organization_id) + token = self.get_arg('token', str, None) params = self._get_query_params() - res = await run_task(self.controller.list, token, **params) + if not (await self.check_md5_profiling_token( + organization_id, token, raises=False) and params.get('run_id')): + await self.check_permissions( + 'INFO_ORGANIZATION', 'organization', organization_id) + profiling_token = await self._get_profiling_token(organization_id) + res = await run_task(self.controller.list, profiling_token, **params) self.write(json.dumps(res, cls=ModelEncoder)) diff --git a/rest_api/rest_api_server/handlers/v2/profiling/executors.py b/rest_api/rest_api_server/handlers/v2/profiling/executors.py index 6b42ea6c..76391033 100644 --- a/rest_api/rest_api_server/handlers/v2/profiling/executors.py +++ b/rest_api/rest_api_server/handlers/v2/profiling/executors.py @@ -145,10 +145,10 @@ async def get(self, organization_id, **url_params): organization_id, token, raises=False) or not run_ids: await self.check_permissions( 'INFO_ORGANIZATION', 'organization', organization_id) - token = await self._get_profiling_token(organization_id) + profiling_token = await self._get_profiling_token(organization_id) task_ids = self.get_arg('task_id', str, repeated=True) res = await run_task( - self.controller.list, organization_id, task_ids, token, + self.controller.list, organization_id, task_ids, profiling_token, run_ids=run_ids ) tasks_dict = {'executors': res} diff --git a/rest_api/rest_api_server/handlers/v2/profiling/runs.py b/rest_api/rest_api_server/handlers/v2/profiling/runs.py index 118d64ac..45181bb5 100644 --- a/rest_api/rest_api_server/handlers/v2/profiling/runs.py +++ b/rest_api/rest_api_server/handlers/v2/profiling/runs.py @@ -382,8 +382,9 @@ async def get(self, organization_id, id, **url_params): organization_id, token, raises=False): await self.check_permissions( 'INFO_ORGANIZATION', 'organization', organization_id) - token = await self._get_profiling_token(organization_id) - res = await run_task(self.controller.get, organization_id, id, token) + profiling_token = await self._get_profiling_token(organization_id) + res = await run_task(self.controller.get, organization_id, id, + profiling_token) self.write(json.dumps(res, cls=ModelEncoder)) async def delete(self, organization_id, id, **kwargs): diff --git a/rest_api/rest_api_server/tests/unittests/test_layouts.py b/rest_api/rest_api_server/tests/unittests/test_layouts.py index 42ca2276..bc32c692 100644 --- a/rest_api/rest_api_server/tests/unittests/test_layouts.py +++ b/rest_api/rest_api_server/tests/unittests/test_layouts.py @@ -31,6 +31,8 @@ def setUp(self, version="v2"): _, self.employee_org2 = self.client.employee_create( self.org2['id'], {'name': 'John Org2', 'auth_user_id': self.user_id2}) + _, resp = self.client.profiling_token_get(self.org['id']) + self.profiling_token = resp['token'] self.valid_layout = { 'name': 'layout', 'type': 'test', @@ -114,7 +116,7 @@ def test_list_layouts(self): type_ = 'test_type' layout0 = self.create_layout(owner_id=self.employee_org2['id'], type_=type_, shared=True) - layout1 = self.create_layout(owner_id=self.employee['id']) + self.create_layout(owner_id=self.employee['id']) layout2 = self.create_layout( type_=type_, owner_id=self.employee['id']) layout3 = self.create_layout( @@ -122,7 +124,7 @@ def test_list_layouts(self): layout4 = self.create_layout( owner_id=self.employee2['id'], entity_id=self.user_id, shared=True, type_='ml_run_charts_dashboard') - layout5 = self.create_layout( + self.create_layout( owner_id=self.employee2['id'], entity_id=self.user_id, shared=False, type_='ml_run_charts_dashboard') layout6 = self.create_layout( @@ -181,7 +183,7 @@ def test_list_layouts(self): # arcee token in query secret_p.return_value = False code, res = self.client.layouts_list( - self.org_id, token=self.get_profiling_token(self.org_id), + self.org_id, token=self.get_md5_token_hash(self.profiling_token), layout_type='ml_run_charts_dashboard', include_shared=True) self.assertEqual(code, 200) self.assertEqual(len(res['layouts']), 1) @@ -189,7 +191,8 @@ def test_list_layouts(self): code, res = self.client.layouts_list( self.org_id, layout_type='ml_run_charts_dashboard', - token=self.get_profiling_token(self.org_id), + token=self.get_md5_token_hash( + self.get_profiling_token(self.org_id)), include_shared=False) self.assertEqual(code, 200) self.assertEqual(len(res['layouts']), 0) @@ -264,6 +267,13 @@ def test_get_layout(self): self.assertEqual(code, 404) self.assertEqual(res['error']['error_code'], 'OE0002') + # by token + code, res = self.client.layout_get( + self.org_id, layout3['id'], + token=self.get_md5_token_hash(self.profiling_token)) + self.assertEqual(code, 200) + self.assertEqual(res['id'], layout3['id']) + def test_delete_layout(self): layout1 = self.create_layout(owner_id=self.employee['id']) code, _ = self.client.layout_delete( diff --git a/rest_api/rest_api_server/tests/unittests/test_profiling_artifacts.py b/rest_api/rest_api_server/tests/unittests/test_profiling_artifacts.py index e20352c3..5d581fd5 100644 --- a/rest_api/rest_api_server/tests/unittests/test_profiling_artifacts.py +++ b/rest_api/rest_api_server/tests/unittests/test_profiling_artifacts.py @@ -1,5 +1,7 @@ import uuid from unittest.mock import patch +from tools.optscale_exceptions.http_exc import OptHTTPError +from rest_api.rest_api_server.exceptions import Err from rest_api.rest_api_server.tests.unittests.test_profiling_base import ( TestProfilingBase) @@ -31,6 +33,8 @@ def setUp(self, version='v2'): 'description': 'Test description', 'tags': {'tag': 'tag'} } + _, resp = self.client.profiling_token_get(self.org['id']) + self.profiling_token = resp['token'] def test_create_artifact(self): code, resp = self.client.artifact_create( @@ -106,6 +110,46 @@ def test_list_artifacts(self): self.assertEqual(len(resp['artifacts']), 1) self.assertEqual(resp['artifacts'][0]['id'], artifact['id']) + def test_list_artifacts_by_token(self): + code, artifact = self.client.artifact_create( + self.org['id'], self.valid_artifact) + self.assertEqual(code, 201) + + def side_eff(_action, *_args, **_kwargs): + raise OptHTTPError(403, Err.OE0234, []) + + patch( + 'rest_api.rest_api_server.handlers.v1.base.' + 'BaseAuthHandler.check_permissions', + side_effect=side_eff).start() + + code, resp = self.client.artifacts_get( + self.org['id'], run_id=[self.valid_artifact['run_id']], + task_id=[self.task['id']], created_at_gt=0, + created_at_lt=artifact['created_at'] + 1, limit=1, start_from=0, + token=self.get_md5_token_hash(self.profiling_token) + ) + self.assertEqual(code, 200) + self.assertEqual(len(resp['artifacts']), 1) + self.assertEqual(resp['artifacts'][0]['id'], artifact['id']) + + code, resp = self.client.artifacts_get( + self.org['id'], task_id=[self.task['id']], created_at_gt=0, + created_at_lt=artifact['created_at'] + 1, limit=1, start_from=0, + token=self.get_md5_token_hash(self.profiling_token) + ) + self.assertEqual(code, 403) + self.assertEqual(resp['error']['error_code'], 'OE0234') + + code, resp = self.client.artifacts_get( + self.org['id'], run_id=[self.valid_artifact['run_id']], + task_id=[self.task['id']], created_at_gt=0, + created_at_lt=artifact['created_at'] + 1, limit=1, start_from=0, + token='123' + ) + self.assertEqual(code, 403) + self.assertEqual(resp['error']['error_code'], 'OE0234') + def test_list_artifacts_invalid_params(self): for param in ['created_at_gt', 'created_at_lt', 'start_from', 'limit']: code, resp = self.client.artifacts_get(self.org['id'], diff --git a/rest_api/rest_api_server/tests/unittests/test_profiling_base.py b/rest_api/rest_api_server/tests/unittests/test_profiling_base.py index 4c8f944b..3d278ae3 100644 --- a/rest_api/rest_api_server/tests/unittests/test_profiling_base.py +++ b/rest_api/rest_api_server/tests/unittests/test_profiling_base.py @@ -1,5 +1,6 @@ from datetime import datetime, timezone import uuid +import hashlib from typing import Optional, List from rest_api.rest_api_server.tests.unittests.test_api_base import TestApiBase @@ -36,6 +37,10 @@ def get_profiling_token(organization_id): if token: return token.token + @staticmethod + def get_md5_token_hash(profiling_token): + return hashlib.md5(profiling_token.encode('utf-8')).hexdigest() + def _gen_executor(self, token, **kwargs): executor_id = kwargs.pop('_id', None) if not executor_id: diff --git a/rest_api/rest_api_server/tests/unittests/test_profiling_executors.py b/rest_api/rest_api_server/tests/unittests/test_profiling_executors.py index 05d06796..daac047f 100644 --- a/rest_api/rest_api_server/tests/unittests/test_profiling_executors.py +++ b/rest_api/rest_api_server/tests/unittests/test_profiling_executors.py @@ -32,6 +32,8 @@ def setUp(self, version='v2'): } _, self.cloud_acc = self.create_cloud_account( self.org['id'], config, auth_user_id=self.user_id) + _, resp = self.client.profiling_token_get(self.org['id']) + self.profiling_token = resp['token'] def test_list_executors(self): code, task = self.client.task_create( @@ -271,3 +273,12 @@ def side_eff(_action, *_args, **_kwargs): self.org['id'], task['id'], token='123') self.assertEqual(code, 403) self.assertEqual(resp['error']['error_code'], 'OE0234') + + code, resp = self.client.executor_list( + self.org['id'], run_ids=r1['_id'], + token=self.get_md5_token_hash(self.profiling_token) + ) + self.assertEqual(code, 200) + self.assertEqual(len(resp['executors']), 1) + self.assertEqual(resp['executors'][0]['instance_id'], + valid_resource['cloud_resource_id']) diff --git a/rest_api/rest_api_server/tests/unittests/test_profiling_runs.py b/rest_api/rest_api_server/tests/unittests/test_profiling_runs.py index d6aab154..1d8310f0 100644 --- a/rest_api/rest_api_server/tests/unittests/test_profiling_runs.py +++ b/rest_api/rest_api_server/tests/unittests/test_profiling_runs.py @@ -29,6 +29,8 @@ def setUp(self, version='v2'): 'commit_id': "1fde95d5664ae9e542610993e17ee81b135b55c0", 'status': "dirty" } + _, resp = self.client.profiling_token_get(self.org['id']) + self.profiling_token = resp['token'] def test_get_run(self): code, resp = self.client.run_get(self.org['id'], '123') @@ -90,9 +92,8 @@ def test_get_run_with_token(self): 's3://ml-bucket/dataset', data={'step': 2000, 'loss': 55}, git=self.git_data) - token = self.get_profiling_token(self.org['id']) code, resp = self.client.run_get( - self.org['id'], run['_id'], token=token) + self.org['id'], run['_id']) self.assertEqual(code, 200) def side_eff(_action, *_args, **_kwargs): @@ -108,6 +109,11 @@ def side_eff(_action, *_args, **_kwargs): self.assertEqual(code, 403) self.assertEqual(resp['error']['error_code'], 'OE0234') + code, resp = self.client.run_get( + self.org['id'], run['_id'], + token=self.get_md5_token_hash(self.profiling_token)) + self.assertEqual(code, 200) + def test_get_run_console_data(self): metric_1 = self._create_metric(self.org['id'], 'loss') metric_2 = self._create_metric(self.org['id'], 'metric_2') @@ -581,7 +587,7 @@ def test_not_completed_run_tasks(self): self.cloud_resource_create_bulk( cloud_acc['id'], body, behavior='skip_existing', return_resources=True) - now_dt = datetime(2022, 5, 10) + now_dt = datetime(2022, 5, 10, tzinfo=timezone.utc) now = int(now_dt.timestamp()) code, task = self.client.task_create( self.org['id'], { @@ -653,22 +659,22 @@ def test_not_completed_run_cost(self): self.assertEqual(code, 200) raw_data = [ { - 'start_date': datetime(2022, 5, 15, 12), - 'end_date': datetime(2022, 5, 15, 16), + 'start_date': datetime(2022, 5, 15, 12, tzinfo=timezone.utc), + 'end_date': datetime(2022, 5, 15, 16, tzinfo=timezone.utc), 'cost': 120, 'cloud_account_id': cloud_acc['id'], 'resource_id': res_1['cloud_resource_id'] }, { - 'start_date': datetime(2022, 5, 15), - 'end_date': datetime(2022, 5, 16), + 'start_date': datetime(2022, 5, 15, tzinfo=timezone.utc), + 'end_date': datetime(2022, 5, 16, tzinfo=timezone.utc), 'cost': 179, 'cloud_account_id': cloud_acc['id'], 'resource_id': res_2['cloud_resource_id'] }, { - 'start_date': datetime(2022, 5, 15, 15), - 'end_date': datetime(2022, 5, 15, 16), + 'start_date': datetime(2022, 5, 15, 15, tzinfo=timezone.utc), + 'end_date': datetime(2022, 5, 15, 16, tzinfo=timezone.utc), 'identity/TimeInterval': '2017-11-01T00:00:00Z/2017-11-01T01:00:00Z', 'cost': 200, 'box_usage': True, @@ -694,12 +700,12 @@ def test_not_completed_run_cost(self): 'name': 'My test project', 'key': 'test_project', }) - with freeze_time(datetime(2022, 5, 16)): + with freeze_time(datetime(2022, 5, 16, tzinfo=timezone.utc)): self._create_run( self.org['id'], task['id'], [res_2['cloud_resource_id']], start=int(datetime( - 2022, 5, 15, 14).timestamp()), + 2022, 5, 15, 14, tzinfo=timezone.utc).timestamp()), finish=None, state=3) code, resp = self.client.task_get(self.org['id'], task['id']) self.assertEqual(code, 200) @@ -710,13 +716,13 @@ def test_not_completed_run_cost(self): self.org['id'], task['id'], [res_1['cloud_resource_id']], start=int(datetime( - 2022, 5, 15, 14).timestamp()), + 2022, 5, 15, 14, tzinfo=timezone.utc).timestamp()), finish=None, state=1) code, resp = self.client.task_get(self.org['id'], task['id']) self.assertEqual(code, 200) self.assertEqual(resp['last_run_cost'], 50) self.assertEqual(resp['last_run_duration'], 10 * 3600) - with freeze_time(datetime(2022, 5, 17)): + with freeze_time(datetime(2022, 5, 17, tzinfo=timezone.utc)): code, resp = self.client.task_get(self.org['id'], task['id']) self.assertEqual(code, 200) self.assertEqual(resp['last_run_cost'], 50 + 120) From a758676bd5e48c8c4d71628668d9a1830424b062 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:35:16 +0300 Subject: [PATCH 16/65] OS-3430. Fixed incorrect status of env in email --- docker_images/herald_executor/worker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docker_images/herald_executor/worker.py b/docker_images/herald_executor/worker.py index b94451b3..f94f378a 100644 --- a/docker_images/herald_executor/worker.py +++ b/docker_images/herald_executor/worker.py @@ -246,12 +246,11 @@ def format_remained_time(start_date, end_date): for booking in shareable_booking_data: acquired_since = booking['acquired_since'] released_at = booking['released_at'] - acquired_by_id = booking.get('acquired_by', {}).get('id') utc_acquired_since = int( utcfromtimestamp(acquired_since).timestamp()) utc_released_at = int( utcfromtimestamp(released_at).timestamp()) - user_name = employee_id_map.get(acquired_by_id, {}).get('name') + user_name = booking.get('acquired_by', {}).get('name') if not user_name: LOG.error('Could not detect employee name for booking %s', booking['id']) From 1af6c92f6462edd107597372c6f44c8ba0dfd4c2 Mon Sep 17 00:00:00 2001 From: ek-hystax <33006768+ek-hystax@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:39:16 +0400 Subject: [PATCH 17/65] OS-8050. Update express --- jira_ui/server/package-lock.json | 34 ++++++++++++++++++-------------- jira_ui/server/package.json | 2 +- jira_ui/ui/package-lock.json | 34 ++++++++++++++++++-------------- jira_ui/ui/package.json | 2 +- ngui/server/package.json | 2 +- ngui/server/pnpm-lock.yaml | 16 +++++++-------- ngui/ui/package.json | 2 +- ngui/ui/pnpm-lock.yaml | 22 ++++++++++----------- 8 files changed, 61 insertions(+), 53 deletions(-) diff --git a/jira_ui/server/package-lock.json b/jira_ui/server/package-lock.json index c893df20..9f201b04 100644 --- a/jira_ui/server/package-lock.json +++ b/jira_ui/server/package-lock.json @@ -11,7 +11,7 @@ "license": "ISC", "dependencies": { "dotenv": "^16.3.1", - "express": "^4.21.1", + "express": "^4.21.2", "express-rate-limit": "^6.10.0" }, "devDependencies": { @@ -303,9 +303,9 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -326,7 +326,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -341,6 +341,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-rate-limit": { @@ -815,9 +819,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -1347,9 +1351,9 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, "express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -1370,7 +1374,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -1706,9 +1710,9 @@ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "picomatch": { "version": "2.3.1", diff --git a/jira_ui/server/package.json b/jira_ui/server/package.json index 9eabe5d9..2895c3d1 100644 --- a/jira_ui/server/package.json +++ b/jira_ui/server/package.json @@ -14,7 +14,7 @@ "license": "ISC", "dependencies": { "dotenv": "^16.3.1", - "express": "^4.21.1", + "express": "^4.21.2", "express-rate-limit": "^6.10.0" }, "devDependencies": { diff --git a/jira_ui/ui/package-lock.json b/jira_ui/ui/package-lock.json index 08da5992..1a233eb3 100644 --- a/jira_ui/ui/package-lock.json +++ b/jira_ui/ui/package-lock.json @@ -20,7 +20,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@vitejs/plugin-react-swc": "^3.7.0", "dotenv": "^16.3.1", - "express": "^4.21.1", + "express": "^4.21.2", "http-proxy-middleware": "^2.0.7", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -5829,9 +5829,9 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -5852,7 +5852,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -5867,6 +5867,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { @@ -8273,9 +8277,9 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "node_modules/path-type": { "version": "4.0.0", @@ -14076,9 +14080,9 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, "express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -14099,7 +14103,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -15749,9 +15753,9 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "path-type": { "version": "4.0.0", diff --git a/jira_ui/ui/package.json b/jira_ui/ui/package.json index 544fcaca..33a6b201 100644 --- a/jira_ui/ui/package.json +++ b/jira_ui/ui/package.json @@ -16,7 +16,7 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@vitejs/plugin-react-swc": "^3.7.0", "dotenv": "^16.3.1", - "express": "^4.21.1", + "express": "^4.21.2", "http-proxy-middleware": "^2.0.7", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/ngui/server/package.json b/ngui/server/package.json index 27ec0693..704bc409 100644 --- a/ngui/server/package.json +++ b/ngui/server/package.json @@ -31,7 +31,7 @@ "@types/node": "^20.14.9", "body-parser": "^1.20.3", "cors": "^2.8.5", - "express": "^4.21.1", + "express": "^4.21.2", "graphql": "^16.9.0", "graphql-scalars": "^1.23.0", "http-proxy-middleware": "^2.0.7", diff --git a/ngui/server/pnpm-lock.yaml b/ngui/server/pnpm-lock.yaml index d7e1cf29..b57e51a6 100644 --- a/ngui/server/pnpm-lock.yaml +++ b/ngui/server/pnpm-lock.yaml @@ -34,8 +34,8 @@ dependencies: specifier: ^2.8.5 version: 2.8.5 express: - specifier: ^4.21.1 - version: 4.21.1 + specifier: ^4.21.2 + version: 4.21.2 graphql: specifier: ^16.9.0 version: 16.9.0 @@ -151,7 +151,7 @@ packages: '@types/node-fetch': 2.6.11 async-retry: 1.3.3 cors: 2.8.5 - express: 4.21.1 + express: 4.21.2 graphql: 16.9.0 loglevel: 1.9.2 lru-cache: 7.18.3 @@ -2472,8 +2472,8 @@ packages: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} dev: false - /express@4.21.1: - resolution: {integrity: sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==} + /express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} dependencies: accepts: 1.3.8 @@ -2495,7 +2495,7 @@ packages: methods: 1.1.2 on-finished: 2.4.1 parseurl: 1.3.3 - path-to-regexp: 0.1.10 + path-to-regexp: 0.1.12 proxy-addr: 2.0.7 qs: 6.13.0 range-parser: 1.2.1 @@ -3498,8 +3498,8 @@ packages: path-root-regex: 0.1.2 dev: true - /path-to-regexp@0.1.10: - resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==} + /path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} dev: false /path-type@4.0.0: diff --git a/ngui/ui/package.json b/ngui/ui/package.json index da13aa63..72718458 100644 --- a/ngui/ui/package.json +++ b/ngui/ui/package.json @@ -36,7 +36,7 @@ "d3-array": "^3.2.4", "d3-scale": "^4.0.2", "date-fns": "^2.29.3", - "express": "^4.21.1", + "express": "^4.21.2", "file-saver": "^2.0.5", "github-buttons": "^2.27.0", "google-map-react": "^2.2.1", diff --git a/ngui/ui/pnpm-lock.yaml b/ngui/ui/pnpm-lock.yaml index 38331523..b471b76a 100644 --- a/ngui/ui/pnpm-lock.yaml +++ b/ngui/ui/pnpm-lock.yaml @@ -105,8 +105,8 @@ dependencies: specifier: ^2.29.3 version: 2.29.3 express: - specifier: ^4.21.1 - version: 4.21.1 + specifier: ^4.21.2 + version: 4.21.2 file-saver: specifier: ^2.0.5 version: 2.0.5 @@ -4640,7 +4640,7 @@ packages: ejs: 3.1.10 esbuild: 0.18.20 esbuild-plugin-alias: 0.2.1 - express: 4.21.1 + express: 4.21.2 find-cache-dir: 3.3.2 fs-extra: 11.2.0 process: 0.11.10 @@ -4676,7 +4676,7 @@ packages: '@types/find-cache-dir': 3.2.1 browser-assert: 1.2.1 es-module-lexer: 0.9.3 - express: 4.21.1 + express: 4.21.2 find-cache-dir: 3.3.2 fs-extra: 11.2.0 magic-string: 0.30.5 @@ -4735,7 +4735,7 @@ packages: detect-indent: 6.1.0 envinfo: 7.13.0 execa: 5.1.1 - express: 4.21.1 + express: 4.21.2 find-up: 5.0.0 fs-extra: 11.2.0 get-npm-tarball-url: 2.1.0 @@ -4900,7 +4900,7 @@ packages: cli-table3: 0.6.5 compression: 1.7.4 detect-port: 1.6.1 - express: 4.21.1 + express: 4.21.2 fs-extra: 11.2.0 globby: 11.1.0 lodash: 4.17.21 @@ -8619,8 +8619,8 @@ packages: jest-util: 29.7.0 dev: true - /express@4.21.1: - resolution: {integrity: sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==} + /express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} dependencies: accepts: 1.3.8 @@ -8642,7 +8642,7 @@ packages: methods: 1.1.2 on-finished: 2.4.1 parseurl: 1.3.3 - path-to-regexp: 0.1.10 + path-to-regexp: 0.1.12 proxy-addr: 2.0.7 qs: 6.13.0 range-parser: 1.2.1 @@ -11932,8 +11932,8 @@ packages: minipass: 7.0.4 dev: true - /path-to-regexp@0.1.10: - resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==} + /path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} From 8461af91c0e6be8870b7f18eb68b343dd7a80056 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Tue, 10 Dec 2024 08:26:16 +0300 Subject: [PATCH 18/65] OS-7993. abandoned_images for gcp --- .../bumiworker/modules/recommendations/abandoned_images.py | 4 ++-- tools/cloud_adapter/clouds/gcp.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bumiworker/bumiworker/modules/recommendations/abandoned_images.py b/bumiworker/bumiworker/modules/recommendations/abandoned_images.py index a0f0eb25..fdf27108 100644 --- a/bumiworker/bumiworker/modules/recommendations/abandoned_images.py +++ b/bumiworker/bumiworker/modules/recommendations/abandoned_images.py @@ -9,7 +9,7 @@ DEFAULT_DAYS_THRESHOLD = 7 BULK_SIZE = 1000 SUPPORTED_CLOUD_TYPES = [ - 'nebius' + 'gcp_cnr', 'nebius' ] @@ -90,7 +90,7 @@ def _get(self): 'cloud_account_id': image['cloud_account_id'], 'cloud_account_name': account['name'], 'cloud_type': account['type'], - 'folder_id': image['meta']['folder_id'], + 'folder_id': image['meta'].get('folder_id'), 'last_used': last_used_map.get( image['cloud_resource_id'], 0), 'first_seen': image['first_seen'], diff --git a/tools/cloud_adapter/clouds/gcp.py b/tools/cloud_adapter/clouds/gcp.py index 5a5994cd..3f78746a 100644 --- a/tools/cloud_adapter/clouds/gcp.py +++ b/tools/cloud_adapter/clouds/gcp.py @@ -369,6 +369,7 @@ def __init__(self, cloud_volume: compute.Disk, cloud_adapter): volume_type=type_, attached=attached, zone_id=zone_id, + image_id=cloud_volume.source_image_id, snapshot_id=cloud_volume.source_snapshot_id ) From 21aa278451f85efd2fb313fcdfc13ee44d023f8b Mon Sep 17 00:00:00 2001 From: ek-hystax <33006768+ek-hystax@users.noreply.github.com> Date: Tue, 10 Dec 2024 09:39:17 +0400 Subject: [PATCH 19/65] OS-8053. Add the GCP service to the "Abandoned Images" recommendation --- .../recommendations/AbandonedImages.tsx | 8 ++++---- ngui/ui/src/hooks/useOptscaleRecommendations.ts | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ngui/ui/src/containers/RecommendationsOverviewContainer/recommendations/AbandonedImages.tsx b/ngui/ui/src/containers/RecommendationsOverviewContainer/recommendations/AbandonedImages.tsx index cc0fc0c5..581b3668 100644 --- a/ngui/ui/src/containers/RecommendationsOverviewContainer/recommendations/AbandonedImages.tsx +++ b/ngui/ui/src/containers/RecommendationsOverviewContainer/recommendations/AbandonedImages.tsx @@ -1,9 +1,9 @@ import FormattedMoney from "components/FormattedMoney"; import RecommendationListItemResourceLabel from "components/RecommendationListItemResourceLabel"; import AbandonedImagesModal from "components/SideModalManager/SideModals/recommendations/AbandonedImagesModal"; -import { NEBIUS_SERVICE } from "hooks/useRecommendationServices"; +import { GCP_COMPUTE_ENGINE, NEBIUS_SERVICE } from "hooks/useRecommendationServices"; import { firstSeenOn, lastSeenUsed, possibleMonthlySavings, resource, resourceLocation } from "utils/columns"; -import { FORMATTED_MONEY_TYPES, NEBIUS } from "utils/constants"; +import { FORMATTED_MONEY_TYPES, GCP_CNR, NEBIUS } from "utils/constants"; import BaseRecommendation, { CATEGORY_COST } from "./BaseRecommendation"; const columns = [ @@ -43,9 +43,9 @@ class AbandonedImages extends BaseRecommendation { emptyMessageId = "noAbandonedImages"; - services = [NEBIUS_SERVICE]; + services = [NEBIUS_SERVICE, GCP_COMPUTE_ENGINE]; - appliedDataSources = [NEBIUS]; + appliedDataSources = [NEBIUS, GCP_CNR]; categories = [CATEGORY_COST]; diff --git a/ngui/ui/src/hooks/useOptscaleRecommendations.ts b/ngui/ui/src/hooks/useOptscaleRecommendations.ts index 7308aa4c..994cd810 100644 --- a/ngui/ui/src/hooks/useOptscaleRecommendations.ts +++ b/ngui/ui/src/hooks/useOptscaleRecommendations.ts @@ -26,7 +26,7 @@ import ShortLivingInstances from "containers/RecommendationsOverviewContainer/re import VolumesNotAttachedForLongTime from "containers/RecommendationsOverviewContainer/recommendations/VolumesNotAttachedForLongTime"; import { useIsNebiusConnectionEnabled } from "hooks/useIsNebiusConnectionEnabled"; -const NEBIUS_RECOMMENDATIONS = [AbandonedImages, CvocAgreementOpportunities, AbandonedNebiusS3Buckets, NebiusMigration]; +const NEBIUS_RECOMMENDATIONS = [CvocAgreementOpportunities, AbandonedNebiusS3Buckets, NebiusMigration]; export const NEBIUS_RECOMMENDATION_TYPES = NEBIUS_RECOMMENDATIONS.map((Recommendation) => new Recommendation().type); @@ -56,6 +56,7 @@ export const useOptscaleRecommendations = () => { AbandonedInstances, PublicS3Buckets, ObsoleteImages, + AbandonedImages, ...(isNebiusConnectionEnabled ? NEBIUS_RECOMMENDATIONS : []) ]; From 3bc0ca1fd33cc1ee5158b0c61dd262cd196a0b57 Mon Sep 17 00:00:00 2001 From: ek-hystax <33006768+ek-hystax@users.noreply.github.com> Date: Wed, 11 Dec 2024 10:01:20 +0400 Subject: [PATCH 20/65] OS-5865. Unify spacing between elements and their descriptions --- .../ui/src/components/ActionBar/ActionBar.tsx | 2 +- .../AssignmentRules/AssignmentRules.tsx | 31 +++-- .../ui/src/components/BIExports/BIExports.tsx | 19 ++- .../CloudAccountsOverview.tsx | 48 ++++---- .../CloudCostComparison.tsx | 12 +- .../CloudExpensesChart/CloudExpensesChart.tsx | 105 +++++++++-------- .../components/ClusterTypes/ClusterTypes.tsx | 41 +++---- .../components/Environments/Environments.tsx | 12 +- .../FormContentDescription.tsx | 15 +++ .../FormContentDescription/index.ts | 3 + .../InlineSeverityAlert.tsx | 2 +- .../components/InlineSeverityAlert/index.ts | 3 +- .../K8sRightsizing/K8sRightsizing.tsx | 23 ++-- .../LeaderboardForm/LeaderboardForm.tsx | 9 +- .../MlEditTaskMetrics/MlEditTaskMetrics.tsx | 25 ++-- .../ui/src/components/MlMetrics/MlMetrics.tsx | 49 ++++---- ngui/ui/src/components/MlTasks/MlTasks.tsx | 37 +++--- .../PageContentDescription.tsx | 15 +++ .../PageContentDescription/index.ts | 3 + .../PoolConstraints/PoolConstraints.tsx | 25 ++-- .../RecommendationDetails/Details/Details.tsx | 11 +- .../RegionExpensesMap/RegionExpensesMap.tsx | 54 +++++---- .../ResourceLifecycleGlobalPoolPolicies.tsx | 56 ++++----- ...urceLifecycleGlobalResourceConstraints.tsx | 49 ++++---- .../ResourcesPerspectives.tsx | 38 +++--- .../components/InformationWrapper.tsx | 13 ++- .../TrafficExpensesMap/TrafficExpensesMap.tsx | 110 ++++++++++-------- .../FormElements/InstancesField.tsx | 73 ++++++------ .../FormElements/EnvironmentSshKeyField.tsx | 9 +- .../CreateClusterTypeForm.tsx | 26 +++-- .../PerspectiveOverrideWarning.tsx | 17 ++- .../CreateSshKeyForm/CreateSshKeyForm.tsx | 46 ++++---- .../DataSourceBillingReimportForm.tsx | 15 +-- .../DisconnectCloudAccountForm.tsx | 26 +++-- .../FormElements/AliasesField.tsx | 25 ++-- .../MlRunsetTemplateForm.tsx | 15 ++- .../RenameDataSourceForm.tsx | 18 ++- .../UpdateDataSourceCredentialsForm.tsx | 46 ++++---- .../CleanExpensesBreakdownContainer.tsx | 3 + .../PoolAssignmentRulesContainer.tsx | 27 ++--- 40 files changed, 616 insertions(+), 540 deletions(-) create mode 100644 ngui/ui/src/components/FormContentDescription/FormContentDescription.tsx create mode 100644 ngui/ui/src/components/FormContentDescription/index.ts create mode 100644 ngui/ui/src/components/PageContentDescription/PageContentDescription.tsx create mode 100644 ngui/ui/src/components/PageContentDescription/index.ts diff --git a/ngui/ui/src/components/ActionBar/ActionBar.tsx b/ngui/ui/src/components/ActionBar/ActionBar.tsx index 048cb8e6..f96eda38 100644 --- a/ngui/ui/src/components/ActionBar/ActionBar.tsx +++ b/ngui/ui/src/components/ActionBar/ActionBar.tsx @@ -241,7 +241,7 @@ const ActionBar = ({ data, isPage = true }) => { return title || !isEmptyActions ? ( - + {showBreadcrumbs ? {breadcrumbs} : null} {title ? ( diff --git a/ngui/ui/src/components/AssignmentRules/AssignmentRules.tsx b/ngui/ui/src/components/AssignmentRules/AssignmentRules.tsx index faf9b0df..110a41fc 100644 --- a/ngui/ui/src/components/AssignmentRules/AssignmentRules.tsx +++ b/ngui/ui/src/components/AssignmentRules/AssignmentRules.tsx @@ -1,13 +1,12 @@ -import { Link, Stack } from "@mui/material"; +import { Link } from "@mui/material"; import { FormattedMessage } from "react-intl"; import { Link as RouterLink } from "react-router-dom"; import ActionBar from "components/ActionBar"; import AssignmentRulesTable from "components/AssignmentRulesTable"; import ContentBackdropLoader from "components/ContentBackdropLoader"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; +import PageContentDescription from "components/PageContentDescription/PageContentDescription"; import PageContentWrapper from "components/PageContentWrapper"; import { POOLS } from "urls"; -import { SPACING_2 } from "utils/layouts"; const actionBarDefinition = { breadcrumbs: [ @@ -26,19 +25,19 @@ const AssignmentRules = ({ rules, managedPools, onUpdatePriority, isLoadingProps - -
- -
-
- -
-
+ +
diff --git a/ngui/ui/src/components/BIExports/BIExports.tsx b/ngui/ui/src/components/BIExports/BIExports.tsx index c0e0faa4..5e1c6d20 100644 --- a/ngui/ui/src/components/BIExports/BIExports.tsx +++ b/ngui/ui/src/components/BIExports/BIExports.tsx @@ -1,16 +1,14 @@ import { Link } from "@mui/material"; -import { Stack } from "@mui/system"; import { FormattedMessage } from "react-intl"; import { Link as RouterLink } from "react-router-dom"; import ActionBar from "components/ActionBar"; import BIExportsTable from "components/BIExportsTable"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; +import PageContentDescription from "components/PageContentDescription"; import PageContentWrapper from "components/PageContentWrapper"; import TableLoader from "components/TableLoader"; import { INTEGRATIONS } from "urls"; -import { SPACING_2 } from "utils/layouts"; -const BIExports = ({ biExports, isLoading }) => ( +const BIExports = ({ biExports, isLoading = false }) => ( <> ( }} /> - -
{isLoading ? : }
-
- -
-
+ {isLoading ? : } +
); diff --git a/ngui/ui/src/components/CloudAccountsOverview/CloudAccountsOverview.tsx b/ngui/ui/src/components/CloudAccountsOverview/CloudAccountsOverview.tsx index 4448d984..05b2181c 100644 --- a/ngui/ui/src/components/CloudAccountsOverview/CloudAccountsOverview.tsx +++ b/ngui/ui/src/components/CloudAccountsOverview/CloudAccountsOverview.tsx @@ -1,4 +1,4 @@ -import Grid from "@mui/material/Grid"; +import { Stack } from "@mui/material"; import ActionBar from "components/ActionBar"; import CloudAccountsTable from "components/CloudAccountsTable"; import CloudExpensesChart from "components/CloudExpensesChart"; @@ -8,7 +8,7 @@ import SummaryGrid from "components/SummaryGrid"; import { useIsAllowed } from "hooks/useAllowedActions"; import { getSumByNestedObjectKey, isEmpty as isEmptyArray } from "utils/arrays"; import { SUMMARY_VALUE_COMPONENT_TYPES, SUMMARY_CARD_TYPES, AWS_CNR } from "utils/constants"; -import { SPACING_2, SPACING_3 } from "utils/layouts"; +import { SPACING_2 } from "utils/layouts"; import { getPercentageChangeModule } from "utils/math"; type SummaryProps = { @@ -127,34 +127,34 @@ const CloudAccountsOverview = ({ cloudAccounts, organizationLimit, isLoading = f <> - - + +
- - - - {(organizationLimit === 0 && totalForecast === 0) || totalExpenses === 0 ? null : ( - - - - )} - - {!isLoading && onlyAwsLinkedAccountsConnected && } - - - - - +
+
+ {(organizationLimit === 0 && totalForecast === 0) || totalExpenses === 0 ? null : ( + + )} +
+ {!isLoading && onlyAwsLinkedAccountsConnected && ( +
+ +
+ )} +
+ +
+
); diff --git a/ngui/ui/src/components/CloudCostComparison/CloudCostComparison.tsx b/ngui/ui/src/components/CloudCostComparison/CloudCostComparison.tsx index dd53d944..2934db91 100644 --- a/ngui/ui/src/components/CloudCostComparison/CloudCostComparison.tsx +++ b/ngui/ui/src/components/CloudCostComparison/CloudCostComparison.tsx @@ -2,7 +2,7 @@ import { Stack } from "@mui/material"; import ActionBar from "components/ActionBar"; import CloudCostComparisonTable from "components/CloudCostComparisonTable"; import CloudCostComparisonFiltersForm from "components/forms/CloudCostComparisonFiltersForm"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; +import PageContentDescription from "components/PageContentDescription/PageContentDescription"; import PageContentWrapper from "components/PageContentWrapper"; import TableLoader from "components/TableLoader"; import { SPACING_1 } from "utils/layouts"; @@ -18,10 +18,14 @@ const CloudCostComparison = ({ isLoading, relevantSizes, defaultFormValues, onFi <> + } + }} + /> -
- }} /> -
diff --git a/ngui/ui/src/components/CloudExpensesChart/CloudExpensesChart.tsx b/ngui/ui/src/components/CloudExpensesChart/CloudExpensesChart.tsx index bd926189..dc15cc52 100644 --- a/ngui/ui/src/components/CloudExpensesChart/CloudExpensesChart.tsx +++ b/ngui/ui/src/components/CloudExpensesChart/CloudExpensesChart.tsx @@ -98,56 +98,63 @@ const CloudExpensesChart = ({ cloudAccounts, limit, forecast, isLoading = false const renderChart = !isEmpty(cloudAccounts) ? ( - {limit > forecast ? ( - - ) : ( - - )} - {cloudData.map((data) => { - const { details: { cost = 0, forecast: cloudAccountForecast = 0 } = {}, id } = data; - return cost !== 0 ? ( - - {renderExpensesSegment(cost, data)} - {renderForecastSegment(cloudAccountForecast, cost, data)} - - ) : null; - })} - {limit > forecast ? ( - - ) : ( - - )} + + {limit > forecast ? ( + + ) : ( + + )} + {cloudData.map((data) => { + const { details: { cost = 0, forecast: cloudAccountForecast = 0 } = {}, id } = data; + return cost !== 0 ? ( + + {renderExpensesSegment(cost, data)} + {renderForecastSegment(cloudAccountForecast, cost, data)} + + ) : null; + })} + {limit > forecast ? ( + + ) : ( + + )} + ) : null; diff --git a/ngui/ui/src/components/ClusterTypes/ClusterTypes.tsx b/ngui/ui/src/components/ClusterTypes/ClusterTypes.tsx index 2fece905..b9a9404d 100644 --- a/ngui/ui/src/components/ClusterTypes/ClusterTypes.tsx +++ b/ngui/ui/src/components/ClusterTypes/ClusterTypes.tsx @@ -1,13 +1,11 @@ -import Grid from "@mui/material/Grid"; import Link from "@mui/material/Link"; import { FormattedMessage } from "react-intl"; import { Link as RouterLink } from "react-router-dom"; import ActionBar from "components/ActionBar"; import ClusterTypesTable from "components/ClusterTypesTable"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; +import PageContentDescription from "components/PageContentDescription/PageContentDescription"; import PageContentWrapper from "components/PageContentWrapper"; import { DOCS_HYSTAX_CLUSTERS, RESOURCES } from "urls"; -import { SPACING_2 } from "utils/layouts"; const actionBarDefinition = { breadcrumbs: [ @@ -21,32 +19,25 @@ const actionBarDefinition = { } }; -const ExplanationMessage = () => ( - ( - - {chunks} - - ) - }} - /> -); - const ClusterTypes = ({ clusterTypes, onUpdatePriority, isLoading = false }) => ( <> - - - - - - - - + + ( + + {chunks} + + ) + } + }} + /> ); diff --git a/ngui/ui/src/components/Environments/Environments.tsx b/ngui/ui/src/components/Environments/Environments.tsx index f83c4c64..cd58ed4c 100644 --- a/ngui/ui/src/components/Environments/Environments.tsx +++ b/ngui/ui/src/components/Environments/Environments.tsx @@ -7,7 +7,7 @@ import BookingsCalendar from "components/BookingsCalendar"; import ButtonGroup from "components/ButtonGroup"; import EnvironmentsTable from "components/EnvironmentsTable"; import Hidden from "components/Hidden"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; +import PageContentDescription from "components/PageContentDescription/PageContentDescription"; import PageContentWrapper from "components/PageContentWrapper"; import Selector, { Item, ItemContent } from "components/Selector"; import { ENVIRONMENTS_TOUR_IDS } from "components/Tour"; @@ -235,12 +235,16 @@ const Environments = ({ )} -
- -
+
); diff --git a/ngui/ui/src/components/FormContentDescription/FormContentDescription.tsx b/ngui/ui/src/components/FormContentDescription/FormContentDescription.tsx new file mode 100644 index 00000000..6f1c46c8 --- /dev/null +++ b/ngui/ui/src/components/FormContentDescription/FormContentDescription.tsx @@ -0,0 +1,15 @@ +import { FormControl } from "@mui/material"; +import InlineSeverityAlert, { InlineSeverityAlertProps } from "components/InlineSeverityAlert"; + +type TableDescriptionProps = { + fullWidth?: boolean; + alertProps: InlineSeverityAlertProps; +}; + +const FormContentDescription = ({ alertProps, fullWidth = false }: TableDescriptionProps) => ( + + + +); + +export default FormContentDescription; diff --git a/ngui/ui/src/components/FormContentDescription/index.ts b/ngui/ui/src/components/FormContentDescription/index.ts new file mode 100644 index 00000000..5bcbe7f3 --- /dev/null +++ b/ngui/ui/src/components/FormContentDescription/index.ts @@ -0,0 +1,3 @@ +import FormContentDescription from "./FormContentDescription"; + +export default FormContentDescription; diff --git a/ngui/ui/src/components/InlineSeverityAlert/InlineSeverityAlert.tsx b/ngui/ui/src/components/InlineSeverityAlert/InlineSeverityAlert.tsx index 386ba325..8ff83148 100644 --- a/ngui/ui/src/components/InlineSeverityAlert/InlineSeverityAlert.tsx +++ b/ngui/ui/src/components/InlineSeverityAlert/InlineSeverityAlert.tsx @@ -3,7 +3,7 @@ import { Alert, AlertProps, Typography } from "@mui/material"; import { FormattedMessage } from "react-intl"; import useStyles from "./InlineSeverityAlert.styles"; -type InlineSeverityAlertProps = { +export type InlineSeverityAlertProps = { messageId: string; messageValues?: Record; messageDataTestId?: string; diff --git a/ngui/ui/src/components/InlineSeverityAlert/index.ts b/ngui/ui/src/components/InlineSeverityAlert/index.ts index e8580eb2..197d0174 100644 --- a/ngui/ui/src/components/InlineSeverityAlert/index.ts +++ b/ngui/ui/src/components/InlineSeverityAlert/index.ts @@ -1,3 +1,4 @@ -import InlineSeverityAlert from "./InlineSeverityAlert"; +import InlineSeverityAlert, { InlineSeverityAlertProps } from "./InlineSeverityAlert"; export default InlineSeverityAlert; +export type { InlineSeverityAlertProps }; diff --git a/ngui/ui/src/components/K8sRightsizing/K8sRightsizing.tsx b/ngui/ui/src/components/K8sRightsizing/K8sRightsizing.tsx index 907505f0..763a4ea7 100644 --- a/ngui/ui/src/components/K8sRightsizing/K8sRightsizing.tsx +++ b/ngui/ui/src/components/K8sRightsizing/K8sRightsizing.tsx @@ -1,26 +1,19 @@ -import Grid from "@mui/material/Grid"; import ActionBar from "components/ActionBar"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; import K8sRightsizingTable from "components/K8sRightsizingTable"; +import PageContentDescription from "components/PageContentDescription/PageContentDescription"; import PageContentWrapper from "components/PageContentWrapper"; -import { SPACING_2 } from "utils/layouts"; const K8sRightsizing = ({ actionBarDefinition, namespaces, isLoading = false, tableActionBarDefinition }) => ( <> - - - - - - - - + + ); diff --git a/ngui/ui/src/components/LeaderboardForm/LeaderboardForm.tsx b/ngui/ui/src/components/LeaderboardForm/LeaderboardForm.tsx index 36286c89..5f7c6d54 100644 --- a/ngui/ui/src/components/LeaderboardForm/LeaderboardForm.tsx +++ b/ngui/ui/src/components/LeaderboardForm/LeaderboardForm.tsx @@ -3,7 +3,7 @@ import { FormControl, FormLabel, Typography } from "@mui/material"; import { FormProvider, useForm } from "react-hook-form"; import { FormattedMessage } from "react-intl"; import HtmlSymbol from "components/HtmlSymbol"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; +import PageContentDescription from "components/PageContentDescription/PageContentDescription"; import { FIELD_NAMES } from "./constants"; import { RunTagsField, @@ -47,7 +47,12 @@ const LeaderboardForm = ({ return ( - {isTemplate && } + {isTemplate && ( + + )}
{ const submitData = { diff --git a/ngui/ui/src/components/MlEditTaskMetrics/MlEditTaskMetrics.tsx b/ngui/ui/src/components/MlEditTaskMetrics/MlEditTaskMetrics.tsx index 0538ac52..12c77d8b 100644 --- a/ngui/ui/src/components/MlEditTaskMetrics/MlEditTaskMetrics.tsx +++ b/ngui/ui/src/components/MlEditTaskMetrics/MlEditTaskMetrics.tsx @@ -1,20 +1,19 @@ -import { Stack } from "@mui/material"; import ContentBackdropLoader from "components/ContentBackdropLoader"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; -import { SPACING_2 } from "utils/layouts"; +import PageContentDescription from "components/PageContentDescription/PageContentDescription"; import MlTaskMetricsTable from "./MlTaskMetricsTable"; const MlEditTaskMetrics = ({ metrics, onAttachChange, isLoading = false, isUpdateLoading = false }) => ( - -
- - - -
-
- -
-
+ <> + + + + + ); export default MlEditTaskMetrics; diff --git a/ngui/ui/src/components/MlMetrics/MlMetrics.tsx b/ngui/ui/src/components/MlMetrics/MlMetrics.tsx index 9ef0d6aa..05c6d3a5 100644 --- a/ngui/ui/src/components/MlMetrics/MlMetrics.tsx +++ b/ngui/ui/src/components/MlMetrics/MlMetrics.tsx @@ -2,10 +2,9 @@ import { useMemo } from "react"; import AddOutlinedIcon from "@mui/icons-material/AddOutlined"; import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined"; import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; -import { Stack } from "@mui/system"; import { useNavigate } from "react-router-dom"; import ActionBar from "components/ActionBar"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; +import PageContentDescription from "components/PageContentDescription/PageContentDescription"; import PageContentWrapper from "components/PageContentWrapper"; import { DeleteMlMetricModal } from "components/SideModalManager/SideModals"; import Table from "components/Table"; @@ -17,7 +16,6 @@ import { useOpenSideModal } from "hooks/useOpenSideModal"; import { ML_METRIC_CREATE, getEditMetricUrl } from "urls"; import { dynamicFractionDigitsValue, name, tendency, text } from "utils/columns"; import aggregateFunction from "utils/columns/aggregateFunction"; -import { SPACING_2 } from "utils/layouts"; const actionBarDefinition = { title: { @@ -108,29 +106,28 @@ const MlMetrics = ({ metrics, isLoading }) => { <> - -
- {isLoading ? ( - - ) : ( - - )} - -
- -
- + {isLoading ? ( + + ) : ( +
+ )} + ); diff --git a/ngui/ui/src/components/MlTasks/MlTasks.tsx b/ngui/ui/src/components/MlTasks/MlTasks.tsx index 45bf3ad0..2a8a898f 100644 --- a/ngui/ui/src/components/MlTasks/MlTasks.tsx +++ b/ngui/ui/src/components/MlTasks/MlTasks.tsx @@ -2,12 +2,11 @@ import { useEffect, useMemo, useState } from "react"; import PlayCircleOutlineOutlinedIcon from "@mui/icons-material/PlayCircleOutlineOutlined"; import RefreshOutlinedIcon from "@mui/icons-material/RefreshOutlined"; import SettingsIcon from "@mui/icons-material/Settings"; -import { Stack } from "@mui/system"; import { GET_ML_TASKS } from "api/restapi/actionTypes"; import ActionBar from "components/ActionBar"; import { ML_TASKS_FILTERS_NAMES } from "components/Filters/constants"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; import MlTasksTable from "components/MlTasksTable"; +import PageContentDescription from "components/PageContentDescription/PageContentDescription"; import PageContentWrapper from "components/PageContentWrapper"; import { ProfilingIntegrationModal } from "components/SideModalManager/SideModals"; import TableLoader from "components/TableLoader"; @@ -23,7 +22,6 @@ import { OWNER_ID_FILTER, EMPTY_UUID } from "utils/constants"; -import { SPACING_2 } from "utils/layouts"; import { getQueryParams, updateQueryParams } from "utils/network"; import { isEmpty as isEmptyObject } from "utils/objects"; @@ -187,23 +185,22 @@ const MlTasks = ({ tasks, isLoading }) => { <> - -
- {isLoading ? ( - - ) : ( - - )} -
-
- -
-
+ {isLoading ? ( + + ) : ( + + )} +
); diff --git a/ngui/ui/src/components/PageContentDescription/PageContentDescription.tsx b/ngui/ui/src/components/PageContentDescription/PageContentDescription.tsx new file mode 100644 index 00000000..51bf871e --- /dev/null +++ b/ngui/ui/src/components/PageContentDescription/PageContentDescription.tsx @@ -0,0 +1,15 @@ +import { Box } from "@mui/material"; +import InlineSeverityAlert, { InlineSeverityAlertProps } from "components/InlineSeverityAlert"; + +type TableDescriptionProps = { + position: "top" | "bottom"; + alertProps: InlineSeverityAlertProps; +}; + +const PageContentDescription = ({ position, alertProps }: TableDescriptionProps) => ( + + + +); + +export default PageContentDescription; diff --git a/ngui/ui/src/components/PageContentDescription/index.ts b/ngui/ui/src/components/PageContentDescription/index.ts new file mode 100644 index 00000000..a83bfef1 --- /dev/null +++ b/ngui/ui/src/components/PageContentDescription/index.ts @@ -0,0 +1,3 @@ +import PageContentDescription from "./PageContentDescription"; + +export default PageContentDescription; diff --git a/ngui/ui/src/components/PoolConstraints/PoolConstraints.tsx b/ngui/ui/src/components/PoolConstraints/PoolConstraints.tsx index ebad9b32..793753bf 100644 --- a/ngui/ui/src/components/PoolConstraints/PoolConstraints.tsx +++ b/ngui/ui/src/components/PoolConstraints/PoolConstraints.tsx @@ -1,13 +1,13 @@ -import { Box, Stack } from "@mui/material"; +import { Box } from "@mui/material"; import Link from "@mui/material/Link"; import EnabledConstraints from "components/EnabledConstraints"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; +import PageContentDescription from "components/PageContentDescription/PageContentDescription"; import PoolConstraintContainer from "containers/PoolConstraintContainer"; import { DOCS_HYSTAX_RESOURCE_CONSTRAINTS } from "urls"; import { SPACING_1, SPACING_2 } from "utils/layouts"; const PoolConstraints = ({ isLoading, policies, poolId }) => ( - + <> ( @@ -23,20 +23,21 @@ const PoolConstraints = ({ isLoading, policies, poolId }) => ( )} /> - - ( {chunks} ) - }} - /> - - + } + }} + /> + ); export default PoolConstraints; diff --git a/ngui/ui/src/components/RecommendationDetails/Details/Details.tsx b/ngui/ui/src/components/RecommendationDetails/Details/Details.tsx index c7a4dc6c..63c3a42f 100644 --- a/ngui/ui/src/components/RecommendationDetails/Details/Details.tsx +++ b/ngui/ui/src/components/RecommendationDetails/Details/Details.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from "react-intl"; import { GET_RESOURCE_ALLOWED_ACTIONS } from "api/auth/actionTypes"; import CloudLabel from "components/CloudLabel"; import IconButton from "components/IconButton"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; +import PageContentDescription from "components/PageContentDescription/PageContentDescription"; import ExcludePoolsFromRecommendationModal from "components/SideModalManager/SideModals/ExcludePoolsFromRecommendationModal"; import Table from "components/Table"; import TextWithDataTestId from "components/TextWithDataTestId"; @@ -222,9 +222,12 @@ const Details = ({ type, limit, status, data, dataSourceIds = [], withDownload } return ( <> {status === ACTIVE && ( - {chunks}, ...recommendation.descriptionMessageValues }} + {chunks}, ...recommendation.descriptionMessageValues } + }} /> )}
{ @@ -20,29 +22,35 @@ const RegionExpensesMap = ({ markers, defaultZoom, defaultCenter, startDateTimes const key = getEnvironmentVariable("VITE_GOOGLE_MAP_API_KEY"); return !isEmpty(markersWithClusters) ? ( -
- {!key && } - apiIsLoaded(map, maps, markersWithClusters)} - options={{ styles: theme.palette.googleMap, minZoom: 2, maxZoom: 6 }} - onZoomAnimationEnd={onZoomChange} - > - {markersWithClusters.map((marker) => ( - - ))} - -
+ + {!key && ( +
+ +
+ )} +
+ apiIsLoaded(map, maps, markersWithClusters)} + options={{ styles: theme.palette.googleMap, minZoom: 2, maxZoom: 6 }} + onZoomAnimationEnd={onZoomChange} + > + {markersWithClusters.map((marker) => ( + + ))} + +
+
) : null; }; diff --git a/ngui/ui/src/components/ResourceLifecycleGlobalPoolPolicies/ResourceLifecycleGlobalPoolPolicies.tsx b/ngui/ui/src/components/ResourceLifecycleGlobalPoolPolicies/ResourceLifecycleGlobalPoolPolicies.tsx index 5d02e26d..869fe6f7 100644 --- a/ngui/ui/src/components/ResourceLifecycleGlobalPoolPolicies/ResourceLifecycleGlobalPoolPolicies.tsx +++ b/ngui/ui/src/components/ResourceLifecycleGlobalPoolPolicies/ResourceLifecycleGlobalPoolPolicies.tsx @@ -1,13 +1,13 @@ import { useMemo, useState } from "react"; import AddOutlinedIcon from "@mui/icons-material/AddOutlined"; -import { CircularProgress, Stack } from "@mui/material"; +import { CircularProgress } from "@mui/material"; import Switch from "@mui/material/Switch"; import { Box } from "@mui/system"; import { FormattedMessage, useIntl } from "react-intl"; import { useNavigate } from "react-router-dom"; import { useFormatConstraintLimitMessage } from "components/ConstraintMessage/ConstraintLimitMessage"; import EditablePoolPolicyLimit from "components/EditablePoolPolicyLimit"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; +import PageContentDescription from "components/PageContentDescription/PageContentDescription"; import PoolLabel from "components/PoolLabel"; import Table from "components/Table"; import TableLoader from "components/TableLoader"; @@ -17,7 +17,6 @@ import PoolPolicyService from "services/PoolPolicyService"; import { RESOURCE_LIFECYCLE_CREATE_POOL_POLICY } from "urls"; import { SCOPE_TYPES } from "utils/constants"; import { CONSTRAINTS_TYPES, CONSTRAINT_MESSAGE_FORMAT } from "utils/constraints"; -import { SPACING_2 } from "utils/layouts"; const UpdatePoolPolicyActivityContainer = ({ policyId, poolId, active }) => { const { useUpdateGlobalPoolPolicyActivity } = PoolPolicyService(); @@ -241,31 +240,32 @@ const ResourceLifecycleGlobalPoolPolicies = ({ poolPolicies, isLoading = false } }; return ( - -
- {isLoading ? ( - - ) : ( -
- )} - -
- -
- + <> + {isLoading ? ( + + ) : ( +
+ )} + + ); }; diff --git a/ngui/ui/src/components/ResourceLifecycleGlobalResourceConstraints/ResourceLifecycleGlobalResourceConstraints.tsx b/ngui/ui/src/components/ResourceLifecycleGlobalResourceConstraints/ResourceLifecycleGlobalResourceConstraints.tsx index 09035e0b..8d5c5807 100644 --- a/ngui/ui/src/components/ResourceLifecycleGlobalResourceConstraints/ResourceLifecycleGlobalResourceConstraints.tsx +++ b/ngui/ui/src/components/ResourceLifecycleGlobalResourceConstraints/ResourceLifecycleGlobalResourceConstraints.tsx @@ -1,7 +1,6 @@ import { useMemo, useState } from "react"; import CreateOutlinedIcon from "@mui/icons-material/CreateOutlined"; import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined"; -import { Stack } from "@mui/material"; import Link from "@mui/material/Link"; import { Box } from "@mui/system"; import { FormattedMessage, useIntl } from "react-intl"; @@ -12,7 +11,7 @@ import CaptionedCell from "components/CaptionedCell"; import { useFormatConstraintLimitMessage } from "components/ConstraintMessage/ConstraintLimitMessage"; import EditResourceConstraintForm from "components/forms/EditResourceConstraintForm"; import IconButton from "components/IconButton"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; +import PageContentDescription from "components/PageContentDescription"; import PoolLabel from "components/PoolLabel"; import ResourceCell from "components/ResourceCell"; import { DeleteGlobalResourceConstraintModal } from "components/SideModalManager/SideModals"; @@ -27,7 +26,6 @@ import { import { RESOURCES } from "urls"; import { checkError } from "utils/api"; import { CONSTRAINTS_TYPES, CONSTRAINT_MESSAGE_FORMAT } from "utils/constraints"; -import { SPACING_2 } from "utils/layouts"; import { getResourceDisplayedName } from "utils/resources"; import { RESOURCE_ID_COLUMN_CELL_STYLE } from "utils/tables"; @@ -273,34 +271,33 @@ const ResourceLifecycleGlobalResourceConstraints = ({ constraints, isLoading = f }, [isAllowedToEditAnyResourcePolicy, openSideModal]); return ( - -
- {isLoading ? ( - - ) : ( -
- )} - -
- + {isLoading ? ( + + ) : ( +
+ )} + ( {chunks} ) - }} - /> - - + } + }} + /> + ); }; diff --git a/ngui/ui/src/components/ResourcesPerspectives/ResourcesPerspectives.tsx b/ngui/ui/src/components/ResourcesPerspectives/ResourcesPerspectives.tsx index 5d3f0bbb..eef4b165 100644 --- a/ngui/ui/src/components/ResourcesPerspectives/ResourcesPerspectives.tsx +++ b/ngui/ui/src/components/ResourcesPerspectives/ResourcesPerspectives.tsx @@ -1,14 +1,14 @@ import { useMemo } from "react"; import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined"; import PriorityHighOutlinedIcon from "@mui/icons-material/PriorityHighOutlined"; -import { Link, Stack } from "@mui/material"; +import { Link } from "@mui/material"; import { FormattedMessage, useIntl } from "react-intl"; import { Link as RouterLink } from "react-router-dom"; import Filters from "components/Filters"; import { RESOURCE_FILTERS } from "components/Filters/constants"; import IconLabel from "components/IconLabel"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; import KeyValueLabel from "components/KeyValueLabel/KeyValueLabel"; +import PageContentDescription from "components/PageContentDescription"; import DeletePerspectiveSideModal from "components/SideModalManager/SideModals/DeletePerspectiveSideModal"; import Table from "components/Table"; import TableCellActions from "components/TableCellActions"; @@ -20,7 +20,6 @@ import { useOpenSideModal } from "hooks/useOpenSideModal"; import { useOrganizationPerspectives } from "hooks/useOrganizationPerspectives"; import { getResourcesExpensesUrl } from "urls"; import { isEmpty as isEmptyArray } from "utils/arrays"; -import { SPACING_2 } from "utils/layouts"; const ResourcesPerspectives = () => { const isAllowedToDeletePerspectives = useIsAllowed({ requiredActions: ["EDIT_PARTNER"] }); @@ -214,22 +213,23 @@ const ResourcesPerspectives = () => { }, [validPerspectives, intl, invalidPerspectives]); return ( - -
-
- -
- -
- + <> +
+ + ); }; diff --git a/ngui/ui/src/components/SideModalManager/SideModals/recommendations/components/InformationWrapper.tsx b/ngui/ui/src/components/SideModalManager/SideModals/recommendations/components/InformationWrapper.tsx index 357f73d1..dff4ccb7 100644 --- a/ngui/ui/src/components/SideModalManager/SideModals/recommendations/components/InformationWrapper.tsx +++ b/ngui/ui/src/components/SideModalManager/SideModals/recommendations/components/InformationWrapper.tsx @@ -1,12 +1,13 @@ -import { Box } from "@mui/material"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; -import { SPACING_1 } from "utils/layouts"; +import PageContentDescription from "components/PageContentDescription"; const InformationWrapper = ({ children }) => ( <> - - - + {children} ); diff --git a/ngui/ui/src/components/TrafficExpensesMap/TrafficExpensesMap.tsx b/ngui/ui/src/components/TrafficExpensesMap/TrafficExpensesMap.tsx index f4e8f547..581eb71f 100644 --- a/ngui/ui/src/components/TrafficExpensesMap/TrafficExpensesMap.tsx +++ b/ngui/ui/src/components/TrafficExpensesMap/TrafficExpensesMap.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { GoogleMapsOverlay } from "@deck.gl/google-maps"; import { getViewStateForLocations } from "@flowmap.gl/data"; import { FlowmapLayer, PickingType } from "@flowmap.gl/layers"; +import { Stack } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import Typography from "@mui/material/Typography"; import GoogleMapReact from "google-map-react"; @@ -15,6 +16,7 @@ import TrafficMapMarker from "components/TrafficMapMarker"; import { isEmpty } from "utils/arrays"; import { EXPENSES_MAP_OBJECT_TYPES, FORMATTED_MONEY_TYPES } from "utils/constants"; import { getEnvironmentVariable } from "utils/env"; +import { SPACING_2 } from "utils/layouts"; import { TRAFFIC_EXPENSES_HEIGHT } from "utils/maps"; import FlowMapDataProvider from "./FlowMapDataProvider"; import useStyles from "./TrafficExpensesMap.styles"; @@ -231,60 +233,68 @@ const TrafficExpensesMap = ({ markers, defaultZoom, defaultCenter, onMapClick = const externalMarker = data?.externalLocations.length ? data?.externalLocations[0] : null; const interRegionMarker = data?.interRegion; + const key = getEnvironmentVariable("VITE_GOOGLE_MAP_API_KEY"); + return ( -
- {!key && } - { - const mapLegend = document.getElementById("map-legend"); - const mapTooltip = document.getElementById("map-tooltip"); - map.controls[maps.ControlPosition.BOTTOM_CENTER].push(mapLegend); - map.controls[maps.ControlPosition.TOP_LEFT].push(mapTooltip); - setLayers([]); - deckOverlay.finalize(); - deckOverlay = new GoogleMapsOverlay(); - deckOverlay.setMap(map); - refreshLayers(); - }} + + {!key && ( +
+ +
+ )} +
- {externalMarker && ( - - )} - {interRegionMarker && ( - - )} - -
- {legend} -
-
- {tooltip.content} + { + const mapLegend = document.getElementById("map-legend"); + const mapTooltip = document.getElementById("map-tooltip"); + map.controls[maps.ControlPosition.BOTTOM_CENTER].push(mapLegend); + map.controls[maps.ControlPosition.TOP_LEFT].push(mapTooltip); + setLayers([]); + deckOverlay.finalize(); + deckOverlay = new GoogleMapsOverlay(); + deckOverlay.setMap(map); + refreshLayers(); + }} + > + {externalMarker && ( + + )} + {interRegionMarker && ( + + )} + +
+ {legend} +
+
+ {tooltip.content} +
-
+
); }; diff --git a/ngui/ui/src/components/forms/AddInstancesToScheduleForm/FormElements/InstancesField.tsx b/ngui/ui/src/components/forms/AddInstancesToScheduleForm/FormElements/InstancesField.tsx index 8b93e8e1..c0805f5b 100644 --- a/ngui/ui/src/components/forms/AddInstancesToScheduleForm/FormElements/InstancesField.tsx +++ b/ngui/ui/src/components/forms/AddInstancesToScheduleForm/FormElements/InstancesField.tsx @@ -1,12 +1,11 @@ import { useMemo } from "react"; -import { FormControl, FormHelperText, Stack } from "@mui/material"; +import { FormControl, FormHelperText } from "@mui/material"; import { Controller, useFormContext } from "react-hook-form"; import { FormattedMessage, useIntl } from "react-intl"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; +import FormContentDescription from "components/FormContentDescription"; import Table from "components/Table"; import TableLoader from "components/TableLoader"; import { powerScheduleInstance, resourceLocation, resourcePoolOwner, size, tags } from "utils/columns"; -import { SPACING_1 } from "utils/layouts"; import { isEmpty as isEmptyObject } from "utils/objects"; import { FormValues } from "../types"; @@ -103,42 +102,44 @@ const InstancesField = ({ instances, instancesCountLimit, isLoading = false }) = const intl = useIntl(); return ( - - - isEmptyObject(value) ? : true - } - }} - render={({ field: { value, onChange } }) => ( - <> - {isLoading ? ( + + isEmptyObject(value) ? : true + } + }} + render={({ field: { value, onChange } }) => { + if (isLoading) { + return ( + - ) : ( - - {instances.length >= instancesCountLimit && ( -
- -
- )} -
- -
-
+
+ ); + } + + return ( + <> + {instances.length >= instancesCountLimit && ( + )} - {!!errors[FIELD_NAME] && {errors[FIELD_NAME].message}} + + + {!!errors[FIELD_NAME] && {errors[FIELD_NAME].message}} + - )} - /> -
+ ); + }} + /> ); }; diff --git a/ngui/ui/src/components/forms/BookEnvironmentForm/FormElements/EnvironmentSshKeyField.tsx b/ngui/ui/src/components/forms/BookEnvironmentForm/FormElements/EnvironmentSshKeyField.tsx index b1ae2a97..4fd22d7e 100644 --- a/ngui/ui/src/components/forms/BookEnvironmentForm/FormElements/EnvironmentSshKeyField.tsx +++ b/ngui/ui/src/components/forms/BookEnvironmentForm/FormElements/EnvironmentSshKeyField.tsx @@ -2,8 +2,8 @@ import { useState } from "react"; import { CircularProgress, FormControl } from "@mui/material"; import { useIntl } from "react-intl"; import ButtonGroup from "components/ButtonGroup"; +import FormContentDescription from "components/FormContentDescription"; import { Selector } from "components/forms/common/fields"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; import { ItemContent } from "components/Selector"; import { isEmpty } from "utils/arrays"; import CreateSshKeyNameField from "./CreateSshKeyNameField"; @@ -73,7 +73,12 @@ const EnvironmentSshKeyField = ({ sshKeys = [], isGetSshKeysReady, defaultKeyId )} {activeTab === ADD_KEY && ( <> - + diff --git a/ngui/ui/src/components/forms/CreateClusterTypeForm/CreateClusterTypeForm.tsx b/ngui/ui/src/components/forms/CreateClusterTypeForm/CreateClusterTypeForm.tsx index e722006b..87e33900 100644 --- a/ngui/ui/src/components/forms/CreateClusterTypeForm/CreateClusterTypeForm.tsx +++ b/ngui/ui/src/components/forms/CreateClusterTypeForm/CreateClusterTypeForm.tsx @@ -4,7 +4,7 @@ import { useForm, FormProvider } from "react-hook-form"; import { FormattedMessage } from "react-intl"; import { Link as RouterLink } from "react-router-dom"; import ActionBar from "components/ActionBar"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; +import FormContentDescription from "components/FormContentDescription"; import PageContentWrapper from "components/PageContentWrapper"; import { CLUSTER_TYPES, DOCS_HYSTAX_CLUSTERS, RESOURCES } from "urls"; import { SPACING_1 } from "utils/layouts"; @@ -41,21 +41,23 @@ const CreateClusterTypeForm = ({ onSubmit, onCancel, isSubmitLoading = false }: + {chunks}, + link: (chunks) => ( + + {chunks} + + ) + } + }} + /> - {chunks}, - link: (chunks) => ( - - {chunks} - - ) - }} - /> ); diff --git a/ngui/ui/src/components/forms/CreateResourcePerspectiveForm/FormElements/PerspectiveOverrideWarning.tsx b/ngui/ui/src/components/forms/CreateResourcePerspectiveForm/FormElements/PerspectiveOverrideWarning.tsx index 3a8e3a6e..01544f72 100644 --- a/ngui/ui/src/components/forms/CreateResourcePerspectiveForm/FormElements/PerspectiveOverrideWarning.tsx +++ b/ngui/ui/src/components/forms/CreateResourcePerspectiveForm/FormElements/PerspectiveOverrideWarning.tsx @@ -1,6 +1,5 @@ -import { FormControl } from "@mui/material"; import { useFormContext } from "react-hook-form"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; +import FormContentDescription from "components/FormContentDescription"; import { FIELD_NAMES } from "../constants"; type PerspectiveOverrideWarningProps = { @@ -13,15 +12,15 @@ const PerspectiveOverrideWarning = ({ perspectiveNames }: PerspectiveOverrideWar const perspectiveName = watch(FIELD_NAMES.NAME); return perspectiveNames.includes(perspectiveName) ? ( - - {chunks} - }} - /> - + } + }} + /> ) : null; }; diff --git a/ngui/ui/src/components/forms/CreateSshKeyForm/CreateSshKeyForm.tsx b/ngui/ui/src/components/forms/CreateSshKeyForm/CreateSshKeyForm.tsx index a47f286d..00aa44e0 100644 --- a/ngui/ui/src/components/forms/CreateSshKeyForm/CreateSshKeyForm.tsx +++ b/ngui/ui/src/components/forms/CreateSshKeyForm/CreateSshKeyForm.tsx @@ -1,7 +1,5 @@ -import Grid from "@mui/material/Grid"; import { FormProvider, useForm } from "react-hook-form"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; -import { SPACING_2 } from "utils/layouts"; +import PageContentDescription from "components/PageContentDescription/PageContentDescription"; import { FormButtons, SshKeyNameField, SshKeyValueField } from "./FormElements"; import { CreateSshKeyFormProps, FormValues } from "./types"; import { getDefaultValues } from "./utils"; @@ -14,26 +12,28 @@ const CreateSshKeyForm = ({ onSubmit, isSubmitLoading = false }: CreateSshKeyFor const { handleSubmit } = methods; return ( - - - - - - -
{ - onSubmit(data); - methods.reset(); // TODO: reset only on success - })} - noValidate - > - - - - -
-
-
+ <> + + +
{ + onSubmit(data); + methods.reset(); // TODO: reset only on success + })} + noValidate + > + + + + +
+ ); }; diff --git a/ngui/ui/src/components/forms/DataSourceBillingReimportForm/DataSourceBillingReimportForm.tsx b/ngui/ui/src/components/forms/DataSourceBillingReimportForm/DataSourceBillingReimportForm.tsx index 02a6ac8e..690423d4 100644 --- a/ngui/ui/src/components/forms/DataSourceBillingReimportForm/DataSourceBillingReimportForm.tsx +++ b/ngui/ui/src/components/forms/DataSourceBillingReimportForm/DataSourceBillingReimportForm.tsx @@ -1,7 +1,5 @@ -import { Stack } from "@mui/material"; import { FormProvider, useForm } from "react-hook-form"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; -import { SPACING_1 } from "utils/layouts"; +import FormContentDescription from "components/FormContentDescription"; import { FormButtons, ReimportFromDatePicker } from "./FormElements"; import { DataSourceBillingReimportFormProps, FormValues } from "./types"; import { getDefaultValues } from "./utils"; @@ -16,10 +14,13 @@ const DataSourceBillingReimportForm = ({ onSubmit, isSubmitLoading = false }: Da return (
- - - - + +
diff --git a/ngui/ui/src/components/forms/DisconnectCloudAccountForm/DisconnectCloudAccountForm.tsx b/ngui/ui/src/components/forms/DisconnectCloudAccountForm/DisconnectCloudAccountForm.tsx index 93dfd9c1..4348883b 100644 --- a/ngui/ui/src/components/forms/DisconnectCloudAccountForm/DisconnectCloudAccountForm.tsx +++ b/ngui/ui/src/components/forms/DisconnectCloudAccountForm/DisconnectCloudAccountForm.tsx @@ -1,10 +1,8 @@ -import { Box } from "@mui/material"; import { FormProvider, useForm } from "react-hook-form"; import DeleteEntity from "components/DeleteEntity"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; +import PageContentDescription from "components/PageContentDescription"; import { useDataSources } from "hooks/useDataSources"; import { AZURE_TENANT } from "utils/constants"; -import { SPACING_1 } from "utils/layouts"; import Survey from "./FormElements/Survey"; import { DisconnectCloudAccountFormProps, FormValues } from "./types"; import { getDefaultValues } from "./utils"; @@ -27,10 +25,24 @@ const DisconnectCloudAccountForm = ({
{(parentId || isAzureTenant) && ( - - {parentId && } - {isAzureTenant && } - + <> + {parentId && ( + + )} + {isAzureTenant && ( + + )} + )} ( - {chunks} + fullWidth + alertProps={{ + messageId: "conflictingAliasWarning", + messageValues: { + alias, + version: aliasToVersionMap[alias], + strong: (chunks) => {chunks} + } }} - sx={{ width: "100%" }} /> )); }; @@ -67,7 +68,7 @@ const AliasesField = ({ aliasToVersionMap, modelVersion }: AliasesFieldProps) => ); return ( - + <> )} /> - + ); }; diff --git a/ngui/ui/src/components/forms/MlRunsetTemplateForm/MlRunsetTemplateForm.tsx b/ngui/ui/src/components/forms/MlRunsetTemplateForm/MlRunsetTemplateForm.tsx index e228a39b..b5a43cee 100644 --- a/ngui/ui/src/components/forms/MlRunsetTemplateForm/MlRunsetTemplateForm.tsx +++ b/ngui/ui/src/components/forms/MlRunsetTemplateForm/MlRunsetTemplateForm.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import FormLabel from "@mui/material/FormLabel"; import { FormProvider, useForm } from "react-hook-form"; import { FormattedMessage } from "react-intl"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; +import PageContentDescription from "components/PageContentDescription/PageContentDescription"; import { useIsOptScaleModeEnabled } from "hooks/useIsOptScaleModeEnabled"; import { OPTSCALE_MODE } from "utils/constants"; import { FIELD_NAMES } from "./constants"; @@ -41,12 +41,21 @@ const MlRunsetTemplateForm = ({ tasks, dataSources, onSubmit, onCancel, isLoadin return ( - + {/* + /> */} { diff --git a/ngui/ui/src/components/forms/RenameDataSourceForm/RenameDataSourceForm.tsx b/ngui/ui/src/components/forms/RenameDataSourceForm/RenameDataSourceForm.tsx index aebcadf0..b149188f 100644 --- a/ngui/ui/src/components/forms/RenameDataSourceForm/RenameDataSourceForm.tsx +++ b/ngui/ui/src/components/forms/RenameDataSourceForm/RenameDataSourceForm.tsx @@ -1,7 +1,5 @@ -import { Stack } from "@mui/material"; import { FormProvider, useForm } from "react-hook-form"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; -import { SPACING_1 } from "utils/layouts"; +import FormContentDescription from "components/FormContentDescription"; import { FormButtons, NameField } from "./FormElements"; import { FormValues, RenameDataSourceFormProps } from "./types"; import { getDefaultValues } from "./utils"; @@ -23,14 +21,12 @@ const RenameDataSourceForm = ({ name, onSubmit, onCancel, isLoading = false }: R })} noValidate > - -
- -
-
- -
-
+ +
diff --git a/ngui/ui/src/components/forms/UpdateDataSourceCredentialsForm/UpdateDataSourceCredentialsForm.tsx b/ngui/ui/src/components/forms/UpdateDataSourceCredentialsForm/UpdateDataSourceCredentialsForm.tsx index f436f222..9439bc6f 100644 --- a/ngui/ui/src/components/forms/UpdateDataSourceCredentialsForm/UpdateDataSourceCredentialsForm.tsx +++ b/ngui/ui/src/components/forms/UpdateDataSourceCredentialsForm/UpdateDataSourceCredentialsForm.tsx @@ -1,6 +1,5 @@ import { Typography } from "@mui/material"; import Link from "@mui/material/Link"; -import { Stack } from "@mui/system"; import { FormProvider, useForm } from "react-hook-form"; import { FormattedMessage } from "react-intl"; import Button from "components/Button"; @@ -18,7 +17,7 @@ import { AWS_ROOT_USE_AWS_EDP_DISCOUNT_FIELD_NAMES } from "components/DataSourceCredentialFields"; import FormButtonsWrapper from "components/FormButtonsWrapper"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; +import FormContentDescription from "components/FormContentDescription"; import { FIELD_NAMES as NEBIUS_FIELD_NAMES } from "components/NebiusConfigFormElements"; import { DOCS_HYSTAX_AUTO_BILLING_AWS, @@ -43,7 +42,6 @@ import { AWS_ROOT_CONNECT_CUR_VERSION } from "utils/constants"; import { readFileAsText } from "utils/files"; -import { SPACING_1 } from "utils/layouts"; import { CredentialInputs } from "./FormElements"; import { AWS_POOL_UPDATE_DATA_EXPORT_PARAMETERS as AWS_ROOT_UPDATE_DATA_EXPORT_PARAMETERS } from "./FormElements/CredentialInputs"; @@ -191,7 +189,13 @@ const Description = ({ type, config }) => { }; const UpdateCredentialsWarning = ({ type }) => { - const renderUpdateWarning = () => ; + const renderUpdateWarning = () => ( + + ); switch (type) { case AWS_CNR: @@ -202,17 +206,17 @@ const UpdateCredentialsWarning = ({ type }) => { return renderUpdateWarning(); case KUBERNETES_CNR: return ( - ( - - {chunks} - - ) + ( + + {chunks} + + ) + } }} /> ); @@ -422,15 +426,9 @@ const UpdateDataSourceCredentialsForm = ({ id, type, config, onSubmit, onCancel, })} noValidate > - -
- - -
-
- -
-
+ + + { entities: intl.formatMessage({ id: "resources" }).toLocaleLowerCase(), count: limit }} + sx={{ + mb: 1 + }} /> ); }; diff --git a/ngui/ui/src/containers/PoolAssignmentRulesContainer/PoolAssignmentRulesContainer.tsx b/ngui/ui/src/containers/PoolAssignmentRulesContainer/PoolAssignmentRulesContainer.tsx index 3d090b46..543fefc1 100644 --- a/ngui/ui/src/containers/PoolAssignmentRulesContainer/PoolAssignmentRulesContainer.tsx +++ b/ngui/ui/src/containers/PoolAssignmentRulesContainer/PoolAssignmentRulesContainer.tsx @@ -1,34 +1,31 @@ -import { Stack } from "@mui/material"; import Link from "@mui/material/Link"; import { Link as RouterLink } from "react-router-dom"; import PoolAssignmentRulesTable from "components/AssignmentRulesTable/PoolAssignmentRulesTable"; -import InlineSeverityAlert from "components/InlineSeverityAlert"; +import PageContentDescription from "components/PageContentDescription"; import AssignmentRuleService from "services/AssignmentRuleService"; import { ASSIGNMENT_RULES } from "urls"; -import { SPACING_2 } from "utils/layouts"; const PoolAssignmentRulesContainer = ({ poolId }) => { const { useGet } = AssignmentRuleService(); const { isLoading, assignmentRules } = useGet({ poolId }); return ( - -
- -
-
- + + ( {chunks} ) - }} - /> -
-
+ } + }} + /> + ); }; From 469be4671cbe6c75c4121aec41c79d53bf39db17 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:49:59 +0300 Subject: [PATCH 21/65] OS-8058. Added md5_token param to profiling_token get api --- .../controllers/profiling/profiling_token.py | 7 ++- .../handlers/v2/profiling/profiling_tokens.py | 43 ++++++++++--------- .../tests/unittests/test_profiling_tokens.py | 9 +++- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/rest_api/rest_api_server/controllers/profiling/profiling_token.py b/rest_api/rest_api_server/controllers/profiling/profiling_token.py index 5d5e3256..af5c5f63 100644 --- a/rest_api/rest_api_server/controllers/profiling/profiling_token.py +++ b/rest_api/rest_api_server/controllers/profiling/profiling_token.py @@ -1,3 +1,4 @@ +import hashlib from tools.optscale_exceptions.common_exc import NotFoundException from rest_api.rest_api_server.exceptions import Err from rest_api.rest_api_server.models.models import ProfilingToken @@ -14,7 +15,11 @@ def _get_model_type(self): return ProfilingToken def get(self, organization_id, **kwargs): - return super().get_or_create_profiling_token(organization_id) + token = super().get_or_create_profiling_token(organization_id) + token = token.to_dict() + token['md5_token'] = hashlib.md5( + token['token'].encode('utf-8')).hexdigest() + return token def get_profiling_token_info(self, profiling_token): token = self.session.query(ProfilingToken).filter( diff --git a/rest_api/rest_api_server/handlers/v2/profiling/profiling_tokens.py b/rest_api/rest_api_server/handlers/v2/profiling/profiling_tokens.py index d6dbc102..950100e1 100644 --- a/rest_api/rest_api_server/handlers/v2/profiling/profiling_tokens.py +++ b/rest_api/rest_api_server/handlers/v2/profiling/profiling_tokens.py @@ -1,5 +1,4 @@ -from tools.optscale_exceptions.common_exc import NotFoundException -from tools.optscale_exceptions.http_exc import OptHTTPError +import json from rest_api.rest_api_server.controllers.profiling.profiling_token import ( ProfilingTokenAsyncController) from rest_api.rest_api_server.handlers.v1.base_async import ( @@ -20,10 +19,10 @@ async def get(self, organization_id, **url_params): """ --- description: | - Get list of organization profiling tokens + Get organization profiling token Required permission: INFO_ORGANIZATION tags: [profiling_tokens] - summary: List of organization profiling tokens + summary: Organization profiling token parameters: - name: organization_id in: path @@ -32,24 +31,28 @@ async def get(self, organization_id, **url_params): type: string responses: 200: - description: Organization profiling tokens list + description: Organization profiling token schema: type: object properties: - profiling_tokens: - type: array - items: - type: object - properties: - id: - type: string - description: Unique profiling token id - token: - type: string - description: Profiling token - organization_id: - type: string - description: Organization id + id: + type: string + description: Unique profiling token id + token: + type: string + description: Profiling token + organization_id: + type: string + description: Organization id + md5_token: + type: string + description: MD5 hash of profiling token + created_at: + type: integer + description: Created at timestamp + deleted_at: + type: integer + description: Deleted at timestamp 401: description: | Unauthorized: @@ -70,7 +73,7 @@ async def get(self, organization_id, **url_params): 'INFO_ORGANIZATION', 'organization', organization_id) res = await run_task(self.controller.get, organization_id=organization_id) - self.write(res.to_json()) + self.write(json.dumps(res)) class ProfilingTokenInfoAsyncItemHandler(BaseAsyncItemHandler, diff --git a/rest_api/rest_api_server/tests/unittests/test_profiling_tokens.py b/rest_api/rest_api_server/tests/unittests/test_profiling_tokens.py index 1a1e6df4..5a30e644 100644 --- a/rest_api/rest_api_server/tests/unittests/test_profiling_tokens.py +++ b/rest_api/rest_api_server/tests/unittests/test_profiling_tokens.py @@ -1,9 +1,12 @@ +import hashlib +from unittest.mock import patch, PropertyMock from rest_api.rest_api_server.tests.unittests.test_api_base import TestApiBase -from rest_api.rest_api_server.tests.unittests.test_profiling_base import ArceeMock +from rest_api.rest_api_server.tests.unittests.test_profiling_base import ( + ArceeMock +) from rest_api.rest_api_server.models.db_factory import DBFactory, DBType from rest_api.rest_api_server.models.db_base import BaseDB from rest_api.rest_api_server.models.models import ProfilingToken -from unittest.mock import patch, PropertyMock class TestProfilingTokensApi(TestApiBase): @@ -42,6 +45,8 @@ def test_get(self): code, resp = self.client.profiling_token_get(self.org['id']) self.assertEqual(code, 200) self.assertEqual(token1, resp) + self.assertEqual(resp['md5_token'], hashlib.md5( + resp['token'].encode('utf-8')).hexdigest()) def test_create_on_another_api(self): tokens = self._get_db_profiling_tokens(self.org['id']) From 1139bee35689d79b7f4a5508e494b3a5bfbaca2d Mon Sep 17 00:00:00 2001 From: ek-hystax <33006768+ek-hystax@users.noreply.github.com> Date: Wed, 11 Dec 2024 14:26:14 +0400 Subject: [PATCH 22/65] OS-7414. Publicly available run page --- ngui/ui/src/api/restapi/actionCreators.ts | 36 ++-- .../ArtifactsTable/ArtifactsTable.tsx | 2 +- .../DashboardControls/DashboardControls.tsx | 37 ++-- .../ExecutionBreakdown/ExecutionBreakdown.tsx | 25 ++- .../ExecutorLabel/ExecutorLabel.tsx | 10 +- .../components/MlArtifacts/MlArtifacts.tsx | 4 + .../MlExecutorsTable/MlExecutorsTable.tsx | 39 ++--- .../src/components/MlExecutorsTable/utils.ts | 23 +++ .../MlTaskRun/Components/Charts/Charts.tsx | 56 +++++++ .../MlTaskRun/Components/Charts}/utils.ts | 0 .../MlTaskRun/Components/Executors.tsx | 18 -- .../Components/Executors/Executors.tsx | 33 ++++ .../Components/{ => Overview}/Overview.tsx | 106 ++++++------ .../MlTaskRun/Components/Status.tsx | 63 ------- .../MlTaskRun/Components/Status/Status.tsx | 58 +++++++ .../components/MlTaskRun/Components/Tabs.tsx | 52 ++++++ .../components/MlTaskRun/Components/index.ts | 9 +- .../ui/src/components/MlTaskRun/MlTaskRun.tsx | 158 +++++++++--------- ngui/ui/src/components/MlTaskRun/index.ts | 3 +- .../components/ShareRunLink/ShareRunLink.tsx | 37 ++++ ngui/ui/src/components/ShareRunLink/index.ts | 3 + .../SelectStageOrMilestoneModal.tsx | 28 +++- .../SideModals/ShareRunLinkModal.tsx | 20 +++ .../SideModalManager/SideModals/index.ts | 4 +- .../ColumnSetsContainer.tsx | 6 +- .../CreateColumnSetFormContainer.tsx | 6 +- .../ExecutionBreakdownContainer.tsx | 28 ---- .../ExecutionBreakdownContainer/index.ts | 6 - .../MlArtifactsContainer.tsx | 18 +- .../MlCreateRunArtifactContainer.tsx | 2 +- .../MlEditRunArtifactContainer.tsx | 2 +- .../MlExecutorsContainer.tsx | 6 +- .../MlTaskExecutorsContainer.tsx | 6 +- .../MlTaskRunContainer/MlTaskRunContainer.tsx | 7 +- .../PublicMlRunContainer.tsx | 48 ++++++ .../containers/PublicMlRunContainer/index.ts | 3 + .../RunArtifactsContainer.tsx | 9 +- .../ShareRunLinkContainer.tsx | 18 ++ .../containers/ShareRunLinkContainer/index.ts | 3 + .../StagesAndMilestonesContainer.tsx | 40 ----- .../StagesAndMilestonesContainer/index.ts | 3 - .../src/graphql/api/restapi/queries/index.ts | 4 +- ngui/ui/src/hooks/useTaskRunChartState.ts | 32 +++- ngui/ui/src/pages/PublicMlRun/PublicMlRun.tsx | 5 + ngui/ui/src/pages/PublicMlRun/index.ts | 3 + ngui/ui/src/services/LayoutsService.ts | 20 +-- ngui/ui/src/services/MlArtifactsService.ts | 3 +- ngui/ui/src/services/MlExecutorsService.ts | 16 +- ngui/ui/src/services/MlProfilingService.ts | 4 +- ngui/ui/src/services/MlTasksService.ts | 16 +- ngui/ui/src/translations/en-US/app.json | 2 + ngui/ui/src/urls.ts | 12 ++ ngui/ui/src/utils/columns/executor.tsx | 10 +- .../src/utils/columns/mlExecutorLocation.tsx | 8 +- ngui/ui/src/utils/routes/index.ts | 2 + ngui/ui/src/utils/routes/publicMlRun.ts | 14 ++ 56 files changed, 771 insertions(+), 415 deletions(-) create mode 100644 ngui/ui/src/components/MlExecutorsTable/utils.ts create mode 100644 ngui/ui/src/components/MlTaskRun/Components/Charts/Charts.tsx rename ngui/ui/src/{containers/ExecutionBreakdownContainer => components/MlTaskRun/Components/Charts}/utils.ts (100%) delete mode 100644 ngui/ui/src/components/MlTaskRun/Components/Executors.tsx create mode 100644 ngui/ui/src/components/MlTaskRun/Components/Executors/Executors.tsx rename ngui/ui/src/components/MlTaskRun/Components/{ => Overview}/Overview.tsx (74%) delete mode 100644 ngui/ui/src/components/MlTaskRun/Components/Status.tsx create mode 100644 ngui/ui/src/components/MlTaskRun/Components/Status/Status.tsx create mode 100644 ngui/ui/src/components/MlTaskRun/Components/Tabs.tsx create mode 100644 ngui/ui/src/components/ShareRunLink/ShareRunLink.tsx create mode 100644 ngui/ui/src/components/ShareRunLink/index.ts create mode 100644 ngui/ui/src/components/SideModalManager/SideModals/ShareRunLinkModal.tsx delete mode 100644 ngui/ui/src/containers/ExecutionBreakdownContainer/ExecutionBreakdownContainer.tsx delete mode 100644 ngui/ui/src/containers/ExecutionBreakdownContainer/index.ts create mode 100644 ngui/ui/src/containers/PublicMlRunContainer/PublicMlRunContainer.tsx create mode 100644 ngui/ui/src/containers/PublicMlRunContainer/index.ts create mode 100644 ngui/ui/src/containers/ShareRunLinkContainer/ShareRunLinkContainer.tsx create mode 100644 ngui/ui/src/containers/ShareRunLinkContainer/index.ts delete mode 100644 ngui/ui/src/containers/StagesAndMilestonesContainer/StagesAndMilestonesContainer.tsx delete mode 100644 ngui/ui/src/containers/StagesAndMilestonesContainer/index.ts create mode 100644 ngui/ui/src/pages/PublicMlRun/PublicMlRun.tsx create mode 100644 ngui/ui/src/pages/PublicMlRun/index.ts create mode 100644 ngui/ui/src/utils/routes/publicMlRun.ts diff --git a/ngui/ui/src/api/restapi/actionCreators.ts b/ngui/ui/src/api/restapi/actionCreators.ts index 07f9211b..cf7633af 100644 --- a/ngui/ui/src/api/restapi/actionCreators.ts +++ b/ngui/ui/src/api/restapi/actionCreators.ts @@ -2240,24 +2240,30 @@ export const getMlTaskRunsBulk = (organizationId, taskId, runIds) => } }); -export const getMlRunDetails = (organizationId, runId) => +export const getMlRunDetails = (organizationId, runId, params = {}) => apiAction({ url: `${API_URL}/organizations/${organizationId}/runs/${runId}`, method: "GET", ttl: 5 * MINUTE, onSuccess: handleSuccess(SET_ML_RUN_DETAILS), hash: hashParams({ organizationId, runId }), - label: GET_ML_RUN_DETAILS + label: GET_ML_RUN_DETAILS, + params: { + token: params.arceeToken + } }); -export const getMlRunDetailsBreakdown = (organizationId, runId) => +export const getMlRunDetailsBreakdown = (organizationId, runId, params = {}) => apiAction({ url: `${API_URL}/organizations/${organizationId}/runs/${runId}/breakdown`, method: "GET", ttl: 5 * MINUTE, onSuccess: handleSuccess(SET_ML_RUN_DETAILS_BREAKDOWN), hash: hashParams({ organizationId, runId }), - label: GET_ML_RUN_DETAILS_BREAKDOWN + label: GET_ML_RUN_DETAILS_BREAKDOWN, + params: { + token: params.arceeToken + } }); export const createMlTask = (organizationId, params) => @@ -2420,7 +2426,8 @@ export const getMlExecutors = (organizationId, params) => onSuccess: handleSuccess(SET_ML_EXECUTORS), params: { task_id: params.taskIds, - run_id: params.runIds + run_id: params.runIds, + token: params.arceeToken } }); @@ -2449,7 +2456,8 @@ export const getMlArtifacts = (organizationId, params = {}) => text_like: params.textLike, created_at_gt: params.createdAtGt, created_at_lt: params.createdAtLt, - task_id: params.taskId + task_id: params.taskId, + token: params.arceeToken } }); @@ -2840,22 +2848,23 @@ export const removeInstancesFromSchedule = (powerScheduleId, instancesToRemove) } }); -export const getLayouts = (organizationId, { layoutType, entityId, includeShared }) => +export const getLayouts = (organizationId, { layoutType, entityId, includeShared, arceeToken }) => apiAction({ url: `${API_URL}/organizations/${organizationId}/layouts`, method: "GET", - hash: hashParams({ organizationId, layoutType, entityId, includeShared }), + hash: hashParams({ organizationId, layoutType, entityId, includeShared, token: arceeToken }), onSuccess: handleSuccess(SET_LAYOUTS), label: GET_LAYOUTS, ttl: 5 * MINUTE, params: { - type: layoutType, + layout_type: layoutType, entity_id: entityId, - include_shared: includeShared + include_shared: includeShared, + token: arceeToken } }); -export const getLayout = (organizationId, layoutId) => +export const getLayout = (organizationId, layoutId, params = {}) => apiAction({ url: `${API_URL}/organizations/${organizationId}/layouts/${layoutId}`, method: "GET", @@ -2863,7 +2872,10 @@ export const getLayout = (organizationId, layoutId) => onSuccess: handleSuccess(SET_LAYOUT), entityId: layoutId, label: GET_LAYOUT, - ttl: 5 * MINUTE + ttl: 5 * MINUTE, + params: { + token: params.arceeToken + } }); export const createLayout = (organizationId, params = {}) => diff --git a/ngui/ui/src/components/ArtifactsTable/ArtifactsTable.tsx b/ngui/ui/src/components/ArtifactsTable/ArtifactsTable.tsx index bdc2f2a7..fe0cc580 100644 --- a/ngui/ui/src/components/ArtifactsTable/ArtifactsTable.tsx +++ b/ngui/ui/src/components/ArtifactsTable/ArtifactsTable.tsx @@ -5,7 +5,7 @@ import { Stack } from "@mui/material"; import { FormattedMessage } from "react-intl"; import { useNavigate } from "react-router-dom"; import LinearSelector from "components/LinearSelector"; -import { TABS } from "components/MlTaskRun"; +import { TABS } from "components/MlTaskRun/Components/Tabs"; import { MlDeleteArtifactModal } from "components/SideModalManager/SideModals"; import Table from "components/Table"; import TableCellActions from "components/TableCellActions"; diff --git a/ngui/ui/src/components/ExecutionBreakdown/DashboardControls/DashboardControls.tsx b/ngui/ui/src/components/ExecutionBreakdown/DashboardControls/DashboardControls.tsx index 3004c7d7..09b76630 100644 --- a/ngui/ui/src/components/ExecutionBreakdown/DashboardControls/DashboardControls.tsx +++ b/ngui/ui/src/components/ExecutionBreakdown/DashboardControls/DashboardControls.tsx @@ -16,6 +16,7 @@ const DashboardControls = ({ updateDashboard, createDashboard, removeDashboard, + isPublicRun, isLoadingProps = {} }) => { const isOwnedDashboard = currentEmployeeId === dashboard.ownerId; @@ -49,22 +50,26 @@ const DashboardControls = ({ isLoading={isLoadingProps.isSetupLoading || isLoadingProps.isSelectNewLoading} /> -
- } - onClick={onSave} - isLoading={isLoadingProps.isSetupLoading || isLoadingProps.isSelectNewLoading} - /> -
-
- } - color="error" - onClick={onDelete} - disabled={!isOwnedDashboard || isDefaultDashboard(dashboard.id)} - isLoading={isLoadingProps.isSetupLoading || isLoadingProps.isSelectNewLoading} - /> -
+ {isPublicRun ? null : ( + <> +
+ } + onClick={onSave} + isLoading={isLoadingProps.isSetupLoading || isLoadingProps.isSelectNewLoading} + /> +
+
+ } + color="error" + onClick={onDelete} + disabled={!isOwnedDashboard || isDefaultDashboard(dashboard.id)} + isLoading={isLoadingProps.isSetupLoading || isLoadingProps.isSelectNewLoading} + /> +
+ + )} ); }; diff --git a/ngui/ui/src/components/ExecutionBreakdown/ExecutionBreakdown.tsx b/ngui/ui/src/components/ExecutionBreakdown/ExecutionBreakdown.tsx index 2ff70ce1..f84bef0a 100644 --- a/ngui/ui/src/components/ExecutionBreakdown/ExecutionBreakdown.tsx +++ b/ngui/ui/src/components/ExecutionBreakdown/ExecutionBreakdown.tsx @@ -8,7 +8,6 @@ import { useTheme } from "@mui/material/styles"; import { Box } from "@mui/system"; import { extent } from "d3-array"; import { FormattedNumber, useIntl } from "react-intl"; -import { useParams } from "react-router-dom"; import Button from "components/Button"; import DynamicFractionDigitsValue, { useFormatDynamicFractionDigitsValue } from "components/DynamicFractionDigitsValue"; import FormattedDigitalUnit, { IEC_UNITS, formatDigitalUnit } from "components/FormattedDigitalUnit"; @@ -59,7 +58,16 @@ const GridButton = ({ gridType, onClick }) => ( ); -const ExecutionBreakdown = ({ breakdown, milestones, reachedGoals = {}, taskId }) => { +const ExecutionBreakdown = ({ + organizationId, + isPublicRun = false, + arceeToken, + breakdown, + stages, + milestones, + reachedGoals = {}, + taskId +}) => { const milestonesGroupedByTimeTuples = getMilestoneTuplesGroupedByTime(milestones); const theme = useTheme(); @@ -230,18 +238,19 @@ const ExecutionBreakdown = ({ breakdown, milestones, reachedGoals = {}, taskId } }; const openSideModal = useOpenSideModal(); - const { runId } = useParams(); const [highlightedStage, setHighlightedStage] = useState(null); const [selectedSegment, setSelectedSegment] = useState(null); const onStageSelectClick = () => openSideModal(SelectStageOrMilestoneModal, { - runId, highlightedStage, setHighlightedStage, setSelectedSegment, - secondsTimeRange: xValuesRange + secondsTimeRange: xValuesRange, + stages, + milestones, + milestonesGroupedByTimeTuples }); const getSelectedSegment = () => selectedSegment ?? xValuesRange; @@ -269,9 +278,12 @@ const ExecutionBreakdown = ({ breakdown, milestones, reachedGoals = {}, taskId } updateGridType, isLoadingProps } = useTaskRunChartState({ + organizationId, + arceeToken, taskId, implementedMetricsBreakdownNames, - breakdownNames + breakdownNames, + isPublicRun }); const { @@ -353,6 +365,7 @@ const ExecutionBreakdown = ({ breakdown, milestones, reachedGoals = {}, taskId } updateDashboard={({ name, shared }) => updateDashboard({ name, shared })} createDashboard={({ name, shared }) => createDashboard({ name, shared })} removeDashboard={(id) => removeDashboard(id)} + isPublicRun={isPublicRun} isLoadingProps={isLoadingProps} /> diff --git a/ngui/ui/src/components/ExecutorLabel/ExecutorLabel.tsx b/ngui/ui/src/components/ExecutorLabel/ExecutorLabel.tsx index 3b757590..3b330967 100644 --- a/ngui/ui/src/components/ExecutorLabel/ExecutorLabel.tsx +++ b/ngui/ui/src/components/ExecutorLabel/ExecutorLabel.tsx @@ -9,24 +9,24 @@ const PLATFORM_TYPE_TO_DATA_SOURCE_TYPE = Object.freeze({ aws: AWS_CNR }); -const DiscoveredExecutorLabel = ({ resource }) => { +const DiscoveredExecutorLabel = ({ resource, disableLink }) => { const { _id: id, cloud_resource_id: cloudResourceId, cloud_account: { type } = {} } = resource; return ( } - label={} + label={} /> ); }; -const ExecutorLabel = ({ instanceId, platformType, discovered = false, resource }) => +const ExecutorLabel = ({ instanceId, platformType, discovered = false, resource, disableLink = false }) => discovered ? ( - + ) : ( } - label={} + label={} /> ); diff --git a/ngui/ui/src/components/MlArtifacts/MlArtifacts.tsx b/ngui/ui/src/components/MlArtifacts/MlArtifacts.tsx index a98f1e79..fea17f5d 100644 --- a/ngui/ui/src/components/MlArtifacts/MlArtifacts.tsx +++ b/ngui/ui/src/components/MlArtifacts/MlArtifacts.tsx @@ -5,9 +5,12 @@ import ActionBar from "components/ActionBar"; import ArtifactsTable from "components/ArtifactsTable"; import PageContentWrapper from "components/PageContentWrapper"; import MlArtifactsContainer from "containers/MlArtifactsContainer"; +import { useOrganizationInfo } from "hooks/useOrganizationInfo"; import { useRefetchApis } from "hooks/useRefetchApis"; const MlArtifacts = ({ tasks, isLoading = false }) => { + const { organizationId } = useOrganizationInfo(); + const refetch = useRefetchApis(); const actionBarDefinition = { @@ -33,6 +36,7 @@ const MlArtifacts = ({ tasks, isLoading = false }) => { ( { +const MlExecutorsTable = ({ + executors, + withExpenses = false, + disableExecutorLink = false, + disableLocationLink = false, + isLoading = false +}) => { const memoizedExecutors = useMemo(() => executors, [executors]); - const isFinOpsEnabled = useIsOptScaleModeEnabled(OPTSCALE_MODE.FINOPS); - const columns = useMemo( - () => [ - executor(), - mlExecutorLocation(), - ...(isFinOpsEnabled - ? [ - expenses({ - id: "expenses", - headerDataTestId: "lbl_expenses", - headerMessageId: "expenses", - accessorFn: (rowData) => rowData.resource?.total_cost - }) - ] - : []), - lastUsed({ headerDataTestId: "lbl_last_used", accessorFn: (rowData) => rowData.last_used }), - firstSeen({ headerDataTestId: "lbl_first_seen", accessorFn: (rowData) => rowData.resource?.first_seen }) - ], - [isFinOpsEnabled] + () => + getColumns({ + withExpenses, + disableExecutorLink, + disableLocationLink + }), + [disableExecutorLink, disableLocationLink, withExpenses] ); return isLoading ? ( diff --git a/ngui/ui/src/components/MlExecutorsTable/utils.ts b/ngui/ui/src/components/MlExecutorsTable/utils.ts new file mode 100644 index 00000000..0260bffe --- /dev/null +++ b/ngui/ui/src/components/MlExecutorsTable/utils.ts @@ -0,0 +1,23 @@ +import { lastUsed, firstSeen, mlExecutorLocation, expenses } from "utils/columns"; +import executor from "utils/columns/executor"; + +export const getColumns = ({ withExpenses = false, disableExecutorLink = false, disableLocationLink = false } = {}) => [ + executor({ + disableLink: disableExecutorLink + }), + mlExecutorLocation({ + disableLink: disableLocationLink + }), + ...(withExpenses + ? [ + expenses({ + id: "expenses", + headerDataTestId: "lbl_expenses", + headerMessageId: "expenses", + accessorFn: (rowData) => rowData.resource?.total_cost + }) + ] + : []), + lastUsed({ headerDataTestId: "lbl_last_used", accessorFn: (rowData) => rowData.last_used }), + firstSeen({ headerDataTestId: "lbl_first_seen", accessorFn: (rowData) => rowData.resource?.first_seen }) +]; diff --git a/ngui/ui/src/components/MlTaskRun/Components/Charts/Charts.tsx b/ngui/ui/src/components/MlTaskRun/Components/Charts/Charts.tsx new file mode 100644 index 00000000..d55bb370 --- /dev/null +++ b/ngui/ui/src/components/MlTaskRun/Components/Charts/Charts.tsx @@ -0,0 +1,56 @@ +import { useMemo } from "react"; +import { useParams } from "react-router-dom"; +import ExecutionBreakdown, { ExecutionBreakdownLoader } from "components/ExecutionBreakdown"; +import MlTasksService from "services/MlTasksService"; +import { getData } from "./utils"; + +const Charts = ({ + run, + organizationId, + arceeToken, + isPublicRun = false, + isTaskRunLoading = false, + isTaskRunDataReady = false +}) => { + const { useGetRunBreakdown } = MlTasksService(); + + const { runId } = useParams(); + + const runBreakdownParams = useMemo( + () => ({ + arceeToken + }), + [arceeToken] + ); + + const { + isLoading: isGetRunBreakdownLoading, + isDataReady: isGetRunBreakdownDataReady, + breakdown: apiBreakdown = {}, + milestones: apiMilestones = [], + stages: apiStages = [] + } = useGetRunBreakdown(organizationId, runId, runBreakdownParams); + + if (isGetRunBreakdownLoading || !isGetRunBreakdownDataReady || isTaskRunLoading || !isTaskRunDataReady) { + return ; + } + + const { breakdown, milestones, stages } = getData({ breakdown: apiBreakdown, milestones: apiMilestones, stages: apiStages }); + + const { reached_goals: reachedGoals = [], task: { id: taskId } = {} } = run; + + return ( + + ); +}; + +export default Charts; diff --git a/ngui/ui/src/containers/ExecutionBreakdownContainer/utils.ts b/ngui/ui/src/components/MlTaskRun/Components/Charts/utils.ts similarity index 100% rename from ngui/ui/src/containers/ExecutionBreakdownContainer/utils.ts rename to ngui/ui/src/components/MlTaskRun/Components/Charts/utils.ts diff --git a/ngui/ui/src/components/MlTaskRun/Components/Executors.tsx b/ngui/ui/src/components/MlTaskRun/Components/Executors.tsx deleted file mode 100644 index dc269c18..00000000 --- a/ngui/ui/src/components/MlTaskRun/Components/Executors.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useMemo } from "react"; -import { useParams } from "react-router-dom"; -import MlExecutorsTable from "components/MlExecutorsTable"; -import MlExecutorsService from "services/MlExecutorsService"; - -const Executors = () => { - const { runId } = useParams(); - - const { useGet } = MlExecutorsService(); - - const runIds = useMemo(() => [runId], [runId]); - - const { isLoading, executors } = useGet({ runIds }); - - return ; -}; - -export default Executors; diff --git a/ngui/ui/src/components/MlTaskRun/Components/Executors/Executors.tsx b/ngui/ui/src/components/MlTaskRun/Components/Executors/Executors.tsx new file mode 100644 index 00000000..92d6228a --- /dev/null +++ b/ngui/ui/src/components/MlTaskRun/Components/Executors/Executors.tsx @@ -0,0 +1,33 @@ +import { useMemo } from "react"; +import { useParams } from "react-router-dom"; +import MlExecutorsTable from "components/MlExecutorsTable"; +import MlExecutorsService from "services/MlExecutorsService"; + +type ExecutorsProps = { + organizationId: string; + withExpenses: boolean; + isPublicRun: boolean; + arceeToken: string; +}; + +const Executors = ({ organizationId, withExpenses, isPublicRun, arceeToken }: ExecutorsProps) => { + const { runId } = useParams(); + + const { useGet } = MlExecutorsService(); + + const runIds = useMemo(() => [runId], [runId]); + + const { isLoading, executors } = useGet({ runIds, organizationId, arceeToken }); + + return ( + + ); +}; + +export default Executors; diff --git a/ngui/ui/src/components/MlTaskRun/Components/Overview.tsx b/ngui/ui/src/components/MlTaskRun/Components/Overview/Overview.tsx similarity index 74% rename from ngui/ui/src/components/MlTaskRun/Components/Overview.tsx rename to ngui/ui/src/components/MlTaskRun/Components/Overview/Overview.tsx index 885cf0bf..0939ebdb 100644 --- a/ngui/ui/src/components/MlTaskRun/Components/Overview.tsx +++ b/ngui/ui/src/components/MlTaskRun/Components/Overview/Overview.tsx @@ -197,63 +197,67 @@ const StderrLog = ({ error, isLoading }) => { return ; }; -const Overview = ({ reachedGoals, dataset, git, tags, hyperparameters, command, console, isLoading = false }) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +const Overview = ({ run, isLoading = false }) => { + const { reached_goals: reachedGoals, dataset, tags, hyperparameters, git, command, console } = run; + + return ( + + + + + + + + - + - - - + - + - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - -); + ); +}; export default Overview; diff --git a/ngui/ui/src/components/MlTaskRun/Components/Status.tsx b/ngui/ui/src/components/MlTaskRun/Components/Status.tsx deleted file mode 100644 index cbf5a1e7..00000000 --- a/ngui/ui/src/components/MlTaskRun/Components/Status.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import FormattedDuration from "components/FormattedDuration"; -import MlRunStatus from "components/MlRunStatus"; -import SummaryGrid from "components/SummaryGrid"; -import { useIsOptScaleModeEnabled } from "hooks/useIsOptScaleModeEnabled"; -import { ML_RUN_STATUS, OPTSCALE_MODE, SUMMARY_VALUE_COMPONENT_TYPES } from "utils/constants"; - -const Status = ({ cost, status, duration, isLoading = false }) => { - const isFinOpsEnabled = useIsOptScaleModeEnabled(OPTSCALE_MODE.FINOPS); - - return ( - status !== undefined, - isLoading, - dataTestIds: { - cardTestId: "card_run_status" - } - }, - { - key: "duration", - valueComponentType: SUMMARY_VALUE_COMPONENT_TYPES.Custom, - CustomValueComponent: FormattedDuration, - valueComponentProps: { - durationInSeconds: duration - }, - renderCondition: () => status !== ML_RUN_STATUS.FAILED, - captionMessageId: "duration", - isLoading, - dataTestIds: { - cardTestId: "card_run_duration" - } - }, - { - key: "cost", - valueComponentType: SUMMARY_VALUE_COMPONENT_TYPES.FormattedMoney, - valueComponentProps: { - value: cost - }, - captionMessageId: "expenses", - dataTestIds: { - cardTestId: "card_expenses" - }, - isLoading, - renderCondition: () => isFinOpsEnabled - } - ]} - /> - ); -}; - -export default Status; diff --git a/ngui/ui/src/components/MlTaskRun/Components/Status/Status.tsx b/ngui/ui/src/components/MlTaskRun/Components/Status/Status.tsx new file mode 100644 index 00000000..fde292b0 --- /dev/null +++ b/ngui/ui/src/components/MlTaskRun/Components/Status/Status.tsx @@ -0,0 +1,58 @@ +import FormattedDuration from "components/FormattedDuration"; +import MlRunStatus from "components/MlRunStatus"; +import SummaryGrid from "components/SummaryGrid"; +import { ML_RUN_STATUS, SUMMARY_VALUE_COMPONENT_TYPES } from "utils/constants"; + +const StatusSummaryGrid = ({ cost, status, duration, withCost = false, isLoading = false }) => ( + status !== undefined, + isLoading, + dataTestIds: { + cardTestId: "card_run_status" + } + }, + { + key: "duration", + valueComponentType: SUMMARY_VALUE_COMPONENT_TYPES.Custom, + CustomValueComponent: FormattedDuration, + valueComponentProps: { + durationInSeconds: duration + }, + renderCondition: () => status !== ML_RUN_STATUS.FAILED, + captionMessageId: "duration", + isLoading, + dataTestIds: { + cardTestId: "card_run_duration" + } + }, + { + key: "cost", + valueComponentType: SUMMARY_VALUE_COMPONENT_TYPES.FormattedMoney, + valueComponentProps: { + value: cost + }, + captionMessageId: "expenses", + dataTestIds: { + cardTestId: "card_expenses" + }, + isLoading, + renderCondition: () => withCost + } + ]} + /> +); + +export default StatusSummaryGrid; diff --git a/ngui/ui/src/components/MlTaskRun/Components/Tabs.tsx b/ngui/ui/src/components/MlTaskRun/Components/Tabs.tsx new file mode 100644 index 00000000..7df28ec2 --- /dev/null +++ b/ngui/ui/src/components/MlTaskRun/Components/Tabs.tsx @@ -0,0 +1,52 @@ +import { useState } from "react"; +import TabsWrapper from "components/TabsWrapper"; + +export const TABS = Object.freeze({ + OVERVIEW: "overview", + ARTIFACTS: "artifacts", + CHARTS: "charts", + EXECUTORS: "executors" +}); + +const Tabs = ({ overviewTab, chartsTab, artifactsTab, executorsTab }) => { + const [activeTab, setActiveTab] = useState(); + + const tabs = [ + { + title: TABS.OVERVIEW, + dataTestId: "tab_overview", + node: overviewTab + }, + { + title: TABS.CHARTS, + dataTestId: "tab_charts", + node: chartsTab + }, + { + title: TABS.ARTIFACTS, + dataTestId: "tab_artifact", + node: artifactsTab + }, + { + title: TABS.EXECUTORS, + dataTestId: "tab_executors", + node: executorsTab + } + ]; + + return ( + { + setActiveTab(value); + } + }} + /> + ); +}; + +export default Tabs; diff --git a/ngui/ui/src/components/MlTaskRun/Components/index.ts b/ngui/ui/src/components/MlTaskRun/Components/index.ts index 96ae224f..34f94c6e 100644 --- a/ngui/ui/src/components/MlTaskRun/Components/index.ts +++ b/ngui/ui/src/components/MlTaskRun/Components/index.ts @@ -1,4 +1,7 @@ -import Executors from "./Executors"; -import Overview from "./Overview"; +import Charts from "./Charts/Charts"; +import Executors from "./Executors/Executors"; +import Overview from "./Overview/Overview"; +import Status from "./Status/Status"; +import Tabs from "./Tabs"; -export { Overview, Executors }; +export { Overview, Executors, Tabs, Status, Charts }; diff --git a/ngui/ui/src/components/MlTaskRun/MlTaskRun.tsx b/ngui/ui/src/components/MlTaskRun/MlTaskRun.tsx index 9a17e091..e604a8d1 100644 --- a/ngui/ui/src/components/MlTaskRun/MlTaskRun.tsx +++ b/ngui/ui/src/components/MlTaskRun/MlTaskRun.tsx @@ -1,96 +1,53 @@ -import { useState } from "react"; import RefreshOutlinedIcon from "@mui/icons-material/RefreshOutlined"; +import ShareOutlinedIcon from "@mui/icons-material/ShareOutlined"; import { Link, Stack, Typography } from "@mui/material"; import { FormattedMessage } from "react-intl"; import { Link as RouterLink } from "react-router-dom"; import { GET_ML_ARTIFACTS, GET_ML_EXECUTORS, GET_ML_RUN_DETAILS, GET_ML_RUN_DETAILS_BREAKDOWN } from "api/restapi/actionTypes"; import ActionBar from "components/ActionBar"; import PageContentWrapper from "components/PageContentWrapper"; -import TabsWrapper from "components/TabsWrapper"; -import ExecutionBreakdownContainer from "containers/ExecutionBreakdownContainer"; +import { ShareRunLinkModal } from "components/SideModalManager/SideModals"; import RunArtifactsContainer from "containers/RunArtifactsContainer"; +import { useOpenSideModal } from "hooks/useOpenSideModal"; import { useRefetchApis } from "hooks/useRefetchApis"; import { ML_TASKS, getMlTaskDetailsUrl } from "urls"; import { SPACING_2 } from "utils/layouts"; import { formatRunFullName } from "utils/ml"; -import { Executors, Overview } from "./Components"; -import Status from "./Components/Status"; +import { Charts, Executors, Overview, Status, Tabs } from "./Components"; -export const TABS = Object.freeze({ - OVERVIEW: "overview", - ARTIFACTS: "artifacts", - CHARTS: "charts", - EXECUTORS: "executors" -}); - -const Tabs = ({ run, isLoading = false }) => { - const [activeTab, setActiveTab] = useState(); - - const tabs = [ - { - title: TABS.OVERVIEW, - dataTestId: "tab_overview", - node: ( - - ) - }, - { - title: TABS.CHARTS, - dataTestId: "tab_charts", - node: - }, - { - title: TABS.ARTIFACTS, - dataTestId: "tab_artifact", - node: - }, - { - title: TABS.EXECUTORS, - dataTestId: "tab_executors", - node: - } - ]; - - return ( - { - setActiveTab(value); - } - }} - /> - ); -}; - -const MlTaskRun = ({ run, isLoading = false }) => { +const MlTaskRun = ({ + run, + organizationId, + arceeToken, + isFinOpsEnabled = false, + isPublicRun = false, + isLoading = false, + isDataReady = false +}) => { const { task: { id: taskId, name: taskName } = {}, name: runName, number } = run; const refetch = useRefetchApis(); + const openSideModal = useOpenSideModal(); + const actionBarDefinition = { breadcrumbs: [ - - - , - - {taskName} - , + isPublicRun ? ( + + + + ) : ( + + + + ), + isPublicRun ? ( + {taskName} + ) : ( + + {taskName} + + ), ], title: { @@ -105,20 +62,67 @@ const MlTaskRun = ({ run, isLoading = false }) => { dataTestId: "btn_refresh", type: "button", action: () => refetch([GET_ML_RUN_DETAILS, GET_ML_EXECUTORS, GET_ML_RUN_DETAILS_BREAKDOWN, GET_ML_ARTIFACTS]) - } + }, + ...(isPublicRun + ? [] + : [ + { + key: "btn-share", + icon: , + messageId: "share", + dataTestId: "btn_share", + type: "button", + isLoading, + action: () => { + openSideModal(ShareRunLinkModal, { + runId: run.id + }); + } + } + ]) ] }; + const overviewTab = ; + + const chartsTab = ( + + ); + + const artifactsTab = ; + + const executorsTab = ( + + ); + return ( <>
- +
- +
diff --git a/ngui/ui/src/components/MlTaskRun/index.ts b/ngui/ui/src/components/MlTaskRun/index.ts index d47d7e1b..f5510c69 100644 --- a/ngui/ui/src/components/MlTaskRun/index.ts +++ b/ngui/ui/src/components/MlTaskRun/index.ts @@ -1,4 +1,3 @@ -import MlTaskRun, { TABS } from "./MlTaskRun"; +import MlTaskRun from "./MlTaskRun"; -export { TABS }; export default MlTaskRun; diff --git a/ngui/ui/src/components/ShareRunLink/ShareRunLink.tsx b/ngui/ui/src/components/ShareRunLink/ShareRunLink.tsx new file mode 100644 index 00000000..2b7cb4f7 --- /dev/null +++ b/ngui/ui/src/components/ShareRunLink/ShareRunLink.tsx @@ -0,0 +1,37 @@ +import { Stack } from "@mui/material"; +import { FormattedMessage } from "react-intl"; +import CodeBlock from "components/CodeBlock"; +import Skeleton from "components/Skeleton"; +import { getMlPublicRunUrl } from "urls"; +import { SPACING_1 } from "utils/layouts"; + +type ShareRunLinkProps = { + runId: string; + arceeToken: string; + organizationId: string; + isLoading?: boolean; +}; + +const ShareRunLink = ({ runId, arceeToken, organizationId, isLoading = false }: ShareRunLinkProps) => { + const route = getMlPublicRunUrl(runId, { organizationId, arceeToken }); + const link = `${window.location.origin}${route}`; + + return ( + +
+ +
+
+ {isLoading ? ( + + + + ) : ( + + )} +
+
+ ); +}; + +export default ShareRunLink; diff --git a/ngui/ui/src/components/ShareRunLink/index.ts b/ngui/ui/src/components/ShareRunLink/index.ts new file mode 100644 index 00000000..3a3874d5 --- /dev/null +++ b/ngui/ui/src/components/ShareRunLink/index.ts @@ -0,0 +1,3 @@ +import ShareRunLink from "./ShareRunLink"; + +export default ShareRunLink; diff --git a/ngui/ui/src/components/SideModalManager/SideModals/SelectStageOrMilestoneModal.tsx b/ngui/ui/src/components/SideModalManager/SideModals/SelectStageOrMilestoneModal.tsx index 881a6024..edcd5c09 100644 --- a/ngui/ui/src/components/SideModalManager/SideModals/SelectStageOrMilestoneModal.tsx +++ b/ngui/ui/src/components/SideModalManager/SideModals/SelectStageOrMilestoneModal.tsx @@ -1,4 +1,4 @@ -import StagesAndMilestonesContainer from "containers/StagesAndMilestonesContainer"; +import StagesAndMilestones from "components/StagesAndMilestones"; import BaseSideModal from "./BaseSideModal"; class SelectStageOrMilestoneModal extends BaseSideModal { @@ -13,7 +13,31 @@ class SelectStageOrMilestoneModal extends BaseSideModal { dataTestId = "smodal_select_stage_or_milestone"; get content() { - return ; + const { + highlightedStage, + setHighlightedStage, + setSelectedSegment, + secondsTimeRange, + stages, + milestonesGroupedByTimeTuples + } = this.payload; + + return ( + { + setSelectedSegment([start, end]); + this.closeSideModal(); + }} + stages={stages} + highlightedStage={highlightedStage} + setHighlightedStage={(stage) => { + setHighlightedStage(stage); + this.closeSideModal(); + }} + secondsTimeRange={secondsTimeRange} + /> + ); } } diff --git a/ngui/ui/src/components/SideModalManager/SideModals/ShareRunLinkModal.tsx b/ngui/ui/src/components/SideModalManager/SideModals/ShareRunLinkModal.tsx new file mode 100644 index 00000000..74d3a471 --- /dev/null +++ b/ngui/ui/src/components/SideModalManager/SideModals/ShareRunLinkModal.tsx @@ -0,0 +1,20 @@ +import ShareRunLinkContainer from "containers/ShareRunLinkContainer"; +import BaseSideModal from "./BaseSideModal"; + +class ShareRunLinkModal extends BaseSideModal { + headerProps = { + messageId: "shareRunLinkTitle", + dataTestIds: { + title: "lbl_share_run_link", + closeButton: "btn_close" + } + }; + + dataTestId = "smodal_share_run_link"; + + get content() { + return ; + } +} + +export default ShareRunLinkModal; diff --git a/ngui/ui/src/components/SideModalManager/SideModals/index.ts b/ngui/ui/src/components/SideModalManager/SideModals/index.ts index dde56a10..2fb2948b 100644 --- a/ngui/ui/src/components/SideModalManager/SideModals/index.ts +++ b/ngui/ui/src/components/SideModalManager/SideModals/index.ts @@ -60,6 +60,7 @@ import S3DuplicateFinderSettingsModal from "./S3DuplicateFinderSettingsModal"; import SaveMlChartsDashboard from "./SaveMlChartsDashboard"; import SelectedBucketsInfoModal from "./SelectedBucketsInfoModal"; import SelectStageOrMilestoneModal from "./SelectStageOrMilestoneModal"; +import ShareRunLinkModal from "./ShareRunLinkModal"; import ShareSettingsModal from "./ShareSettingsModal"; import SlackIntegrationModal from "./SlackIntegrationModal"; import UnmarkEnvironmentModal from "./UnmarkEnvironmentModal"; @@ -135,5 +136,6 @@ export { EditModelPathModal, EditModelVersionTagsModal, MlDeleteArtifactModal, - DataSourceBillingReimportModal + DataSourceBillingReimportModal, + ShareRunLinkModal }; diff --git a/ngui/ui/src/containers/ColumnSetsContainer/ColumnSetsContainer.tsx b/ngui/ui/src/containers/ColumnSetsContainer/ColumnSetsContainer.tsx index 80d197ad..0ff43ca1 100644 --- a/ngui/ui/src/containers/ColumnSetsContainer/ColumnSetsContainer.tsx +++ b/ngui/ui/src/containers/ColumnSetsContainer/ColumnSetsContainer.tsx @@ -1,14 +1,18 @@ import { useCallback } from "react"; import ColumnSets from "components/ColumnSets"; import { getHideableColumns } from "components/Table/utils"; +import { useOrganizationInfo } from "hooks/useOrganizationInfo"; import LayoutsService from "services/LayoutsService"; import { LAYOUT_TYPES } from "utils/constants"; const ColumnSetsContainer = ({ tableContext, closeSideModal }) => { + const { organizationId } = useOrganizationInfo(); + const { useGetAll, useDelete, useGetOneOnDemand } = LayoutsService(); const { onDelete, isLoading: isDeleteLayoutLoading, entityId: deletionEntityId } = useDelete(); const { isLoading: isGetAllLayoutsLoading, layouts } = useGetAll({ + organizationId, layoutType: LAYOUT_TYPES.RESOURCE_RAW_EXPENSES_COLUMNS }); @@ -21,7 +25,7 @@ const ColumnSetsContainer = ({ tableContext, closeSideModal }) => { columnSets={layouts} tableContext={tableContext} onApply={(columnsSetId) => - onGet(columnsSetId).then(({ data }) => { + onGet(organizationId, columnsSetId).then(({ data }) => { const { columns: savedColumns } = JSON.parse(data); tableContext.setColumnVisibility( diff --git a/ngui/ui/src/containers/CreateColumnSetFormContainer/CreateColumnSetFormContainer.tsx b/ngui/ui/src/containers/CreateColumnSetFormContainer/CreateColumnSetFormContainer.tsx index 133af7ef..a62909b1 100644 --- a/ngui/ui/src/containers/CreateColumnSetFormContainer/CreateColumnSetFormContainer.tsx +++ b/ngui/ui/src/containers/CreateColumnSetFormContainer/CreateColumnSetFormContainer.tsx @@ -1,16 +1,20 @@ import CreateColumnSetForm from "components/forms/CreateColumnSetForm"; import { getVisibleColumnIds } from "components/Table/utils"; +import { useOrganizationInfo } from "hooks/useOrganizationInfo"; import LayoutsService from "services/LayoutsService"; import { LAYOUT_TYPES } from "utils/constants"; const CreateColumnSetFormContainer = ({ tableContext }) => { const { useCreate } = LayoutsService(); + + const { organizationId } = useOrganizationInfo(); + const { onCreate, isLoading: isCreateLayoutLoading } = useCreate(); const createColumnSet = (name: string) => { const visibleColumnIds = getVisibleColumnIds(tableContext); - return onCreate({ + return onCreate(organizationId, { name, data: JSON.stringify({ columns: visibleColumnIds diff --git a/ngui/ui/src/containers/ExecutionBreakdownContainer/ExecutionBreakdownContainer.tsx b/ngui/ui/src/containers/ExecutionBreakdownContainer/ExecutionBreakdownContainer.tsx deleted file mode 100644 index 3764d036..00000000 --- a/ngui/ui/src/containers/ExecutionBreakdownContainer/ExecutionBreakdownContainer.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useParams } from "react-router-dom"; -import ExecutionBreakdown, { ExecutionBreakdownLoader } from "components/ExecutionBreakdown"; -import MlTasksService from "services/MlTasksService"; -import { getData } from "./utils"; - -const ExecutionBreakdownContainer = ({ reachedGoals, ...rest }) => { - const { useGetRunBreakdown, useGetTaskRun } = MlTasksService(); - - const { taskId, runId } = useParams(); - - const { - isLoading: isGetRunBreakdownLoading, - isDataReady: isGetRunBreakdownDataReady, - breakdown = {}, - milestones = [], - stages = [] - } = useGetRunBreakdown(runId); - - const { isLoading: isTaskRunLoading, isDataReady: isTaskRunDataReady } = useGetTaskRun(runId); - - return isGetRunBreakdownLoading || !isGetRunBreakdownDataReady || isTaskRunLoading || !isTaskRunDataReady ? ( - - ) : ( - - ); -}; - -export default ExecutionBreakdownContainer; diff --git a/ngui/ui/src/containers/ExecutionBreakdownContainer/index.ts b/ngui/ui/src/containers/ExecutionBreakdownContainer/index.ts deleted file mode 100644 index 210a13f9..00000000 --- a/ngui/ui/src/containers/ExecutionBreakdownContainer/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import ExecutionBreakdownContainer from "./ExecutionBreakdownContainer"; -import { getData } from "./utils"; - -export { getData }; - -export default ExecutionBreakdownContainer; diff --git a/ngui/ui/src/containers/MlArtifactsContainer/MlArtifactsContainer.tsx b/ngui/ui/src/containers/MlArtifactsContainer/MlArtifactsContainer.tsx index c86cf900..1d0b9ece 100644 --- a/ngui/ui/src/containers/MlArtifactsContainer/MlArtifactsContainer.tsx +++ b/ngui/ui/src/containers/MlArtifactsContainer/MlArtifactsContainer.tsx @@ -54,8 +54,10 @@ export type TasksFilter = { }; type MlArtifactsContainerProps = { + organizationId: string; runId?: string | string[]; tasks?: { id: string; name: string }[]; + arceeToken?: string; isLoading?: boolean; render: (props: { artifacts: Artifact[]; @@ -99,7 +101,14 @@ const getRangeQueryParams = (minRange: number, maxRange: number) => { return [minRange, maxRange] as const; }; -const MlArtifactsContainer = ({ runId, tasks = [], isLoading = false, render }: MlArtifactsContainerProps) => { +const MlArtifactsContainer = ({ + organizationId, + runId, + tasks = [], + arceeToken, + isLoading = false, + render +}: MlArtifactsContainerProps) => { const { useGet } = MlArtifactsService(); const [pageIndex, setPageIndex] = useState(getDefaultPageIndexValue()); @@ -149,9 +158,10 @@ const MlArtifactsContainer = ({ runId, tasks = [], isLoading = false, render }: textLike: searchValue, createdAtGt: debouncedRange[0], createdAtLt: debouncedRange[1], - taskId: appliedFilters[TASKS_FILTER] + taskId: appliedFilters[TASKS_FILTER], + arceeToken }), - [pageIndex, runId, searchValue, debouncedRange, appliedFilters] + [pageIndex, runId, searchValue, debouncedRange, appliedFilters, arceeToken] ); const onSearchChange = (newSearch: string) => { @@ -177,7 +187,7 @@ const MlArtifactsContainer = ({ runId, tasks = [], isLoading = false, render }: }); }, [pageIndex, debouncedRange, searchValue, appliedFilters]); - const { isLoading: isGetArtifactsLoading, data } = useGet(params); + const { isLoading: isGetArtifactsLoading, data } = useGet(organizationId, params); const totalArtifactsCount = data?.total_count ?? 0; diff --git a/ngui/ui/src/containers/MlCreateRunArtifactContainer/MlCreateRunArtifactContainer.tsx b/ngui/ui/src/containers/MlCreateRunArtifactContainer/MlCreateRunArtifactContainer.tsx index fa980fd1..db5206f2 100644 --- a/ngui/ui/src/containers/MlCreateRunArtifactContainer/MlCreateRunArtifactContainer.tsx +++ b/ngui/ui/src/containers/MlCreateRunArtifactContainer/MlCreateRunArtifactContainer.tsx @@ -3,7 +3,7 @@ import { FormattedMessage } from "react-intl"; import { useParams, Link as RouterLink, useNavigate } from "react-router-dom"; import ActionBar from "components/ActionBar"; import { MlCreateArtifactForm } from "components/forms/MlArtifactForm"; -import { TABS } from "components/MlTaskRun"; +import { TABS } from "components/MlTaskRun/Components/Tabs"; import PageContentWrapper from "components/PageContentWrapper"; import MlArtifactsService from "services/MlArtifactsService"; import MlTasksService from "services/MlTasksService"; diff --git a/ngui/ui/src/containers/MlEditRunArtifactContainer/MlEditRunArtifactContainer.tsx b/ngui/ui/src/containers/MlEditRunArtifactContainer/MlEditRunArtifactContainer.tsx index a2ac9016..7b2d585e 100644 --- a/ngui/ui/src/containers/MlEditRunArtifactContainer/MlEditRunArtifactContainer.tsx +++ b/ngui/ui/src/containers/MlEditRunArtifactContainer/MlEditRunArtifactContainer.tsx @@ -3,7 +3,7 @@ import { FormattedMessage } from "react-intl"; import { useParams, Link as RouterLink, useNavigate } from "react-router-dom"; import ActionBar from "components/ActionBar"; import { MlEditArtifactForm } from "components/forms/MlArtifactForm"; -import { TABS } from "components/MlTaskRun"; +import { TABS } from "components/MlTaskRun/Components/Tabs"; import PageContentWrapper from "components/PageContentWrapper"; import MlArtifactsService from "services/MlArtifactsService"; import MlTasksService from "services/MlTasksService"; diff --git a/ngui/ui/src/containers/MlExecutorsContainer/MlExecutorsContainer.tsx b/ngui/ui/src/containers/MlExecutorsContainer/MlExecutorsContainer.tsx index 5c861110..19d98afb 100644 --- a/ngui/ui/src/containers/MlExecutorsContainer/MlExecutorsContainer.tsx +++ b/ngui/ui/src/containers/MlExecutorsContainer/MlExecutorsContainer.tsx @@ -1,5 +1,7 @@ import MlExecutorsTable from "components/MlExecutorsTable"; +import { useIsOptScaleModeEnabled } from "hooks/useIsOptScaleModeEnabled"; import MlExecutorsService from "services/MlExecutorsService"; +import { OPTSCALE_MODE } from "utils/constants"; import { inDateRange, secondsToMilliseconds } from "utils/datetime"; const MlExecutorsContainer = ({ dateRange }) => { @@ -9,7 +11,9 @@ const MlExecutorsContainer = ({ dateRange }) => { const { useGet } = MlExecutorsService(); const { isLoading, executors } = useGet(); - return ; + const isFinOpsEnabled = useIsOptScaleModeEnabled(OPTSCALE_MODE.FINOPS); + + return ; }; export default MlExecutorsContainer; diff --git a/ngui/ui/src/containers/MlTaskExecutorsContainer/MlTaskExecutorsContainer.tsx b/ngui/ui/src/containers/MlTaskExecutorsContainer/MlTaskExecutorsContainer.tsx index e1f08607..c8fabac7 100644 --- a/ngui/ui/src/containers/MlTaskExecutorsContainer/MlTaskExecutorsContainer.tsx +++ b/ngui/ui/src/containers/MlTaskExecutorsContainer/MlTaskExecutorsContainer.tsx @@ -1,7 +1,9 @@ import { useMemo } from "react"; import { useParams } from "react-router-dom"; import MlExecutorsTable from "components/MlExecutorsTable"; +import { useIsOptScaleModeEnabled } from "hooks/useIsOptScaleModeEnabled"; import MlExecutorsService from "services/MlExecutorsService"; +import { OPTSCALE_MODE } from "utils/constants"; const MlTaskExecutorsContainer = () => { const { taskId } = useParams(); @@ -12,6 +14,8 @@ const MlTaskExecutorsContainer = () => { const { isLoading, executors = [] } = useGet({ taskIds }); - return ; + const isFinOpsEnabled = useIsOptScaleModeEnabled(OPTSCALE_MODE.FINOPS); + + return ; }; export default MlTaskExecutorsContainer; diff --git a/ngui/ui/src/containers/MlTaskRunContainer/MlTaskRunContainer.tsx b/ngui/ui/src/containers/MlTaskRunContainer/MlTaskRunContainer.tsx index ff33cc87..4bb19810 100644 --- a/ngui/ui/src/containers/MlTaskRunContainer/MlTaskRunContainer.tsx +++ b/ngui/ui/src/containers/MlTaskRunContainer/MlTaskRunContainer.tsx @@ -1,15 +1,18 @@ import { useParams } from "react-router-dom"; import MlTaskRun from "components/MlTaskRun"; +import { useOrganizationInfo } from "hooks/useOrganizationInfo"; import MlTasksService from "services/MlTasksService"; const MlTaskRunContainer = () => { const { runId } = useParams(); + const { organizationId } = useOrganizationInfo(); + const { useGetTaskRun } = MlTasksService(); - const { isLoading, run } = useGetTaskRun(runId); + const { isLoading, isDataReady, run } = useGetTaskRun(organizationId, runId); - return ; + return ; }; export default MlTaskRunContainer; diff --git a/ngui/ui/src/containers/PublicMlRunContainer/PublicMlRunContainer.tsx b/ngui/ui/src/containers/PublicMlRunContainer/PublicMlRunContainer.tsx new file mode 100644 index 00000000..56e458f7 --- /dev/null +++ b/ngui/ui/src/containers/PublicMlRunContainer/PublicMlRunContainer.tsx @@ -0,0 +1,48 @@ +import { useMemo } from "react"; +import { useTheme } from "@mui/material/styles"; +import { useParams } from "react-router-dom"; +import MlTaskRun from "components/MlTaskRun"; +import MlTasksService from "services/MlTasksService"; +import { getQueryParams } from "utils/network"; + +const PublicMlRunContainer = () => { + const theme = useTheme(); + + const { useGetTaskRun } = MlTasksService(); + + const { runId } = useParams(); + + const { organizationId, token: arceeToken } = getQueryParams() as { + organizationId: string; + token: string; + }; + + const params = useMemo( + () => ({ + arceeToken + }), + [arceeToken] + ); + + const { isLoading, isDataReady, run } = useGetTaskRun(organizationId, runId, params); + + return ( +
+ +
+ ); +}; + +export default PublicMlRunContainer; diff --git a/ngui/ui/src/containers/PublicMlRunContainer/index.ts b/ngui/ui/src/containers/PublicMlRunContainer/index.ts new file mode 100644 index 00000000..d74b1b3b --- /dev/null +++ b/ngui/ui/src/containers/PublicMlRunContainer/index.ts @@ -0,0 +1,3 @@ +import PublicMlRunContainer from "./PublicMlRunContainer"; + +export default PublicMlRunContainer; diff --git a/ngui/ui/src/containers/RunArtifactsContainer/RunArtifactsContainer.tsx b/ngui/ui/src/containers/RunArtifactsContainer/RunArtifactsContainer.tsx index e39c12f7..62d227d8 100644 --- a/ngui/ui/src/containers/RunArtifactsContainer/RunArtifactsContainer.tsx +++ b/ngui/ui/src/containers/RunArtifactsContainer/RunArtifactsContainer.tsx @@ -2,12 +2,19 @@ import { useParams } from "react-router-dom"; import RunArtifactsTable from "components/RunArtifactsTable"; import MlArtifactsContainer from "containers/MlArtifactsContainer"; -const RunArtifactsContainer = () => { +type RunArtifactsContainerProps = { + organizationId: string; + arceeToken: string; +}; + +const RunArtifactsContainer = ({ organizationId, arceeToken }: RunArtifactsContainerProps) => { const { runId } = useParams() as { runId: string }; return ( ( )} diff --git a/ngui/ui/src/containers/ShareRunLinkContainer/ShareRunLinkContainer.tsx b/ngui/ui/src/containers/ShareRunLinkContainer/ShareRunLinkContainer.tsx new file mode 100644 index 00000000..4ab077ac --- /dev/null +++ b/ngui/ui/src/containers/ShareRunLinkContainer/ShareRunLinkContainer.tsx @@ -0,0 +1,18 @@ +import ShareRunLink from "components/ShareRunLink"; +import { useOrganizationInfo } from "hooks/useOrganizationInfo"; +import MlProfilingService from "services/MlProfilingService"; + +type ShareRunLinkContainerProps = { + runId: string; +}; + +const ShareRunLinkContainer = ({ runId }: ShareRunLinkContainerProps) => { + const { useGetToken } = MlProfilingService(); + const { isLoading, md5Token } = useGetToken(); + + const { organizationId } = useOrganizationInfo(); + + return ; +}; + +export default ShareRunLinkContainer; diff --git a/ngui/ui/src/containers/ShareRunLinkContainer/index.ts b/ngui/ui/src/containers/ShareRunLinkContainer/index.ts new file mode 100644 index 00000000..abffa3ed --- /dev/null +++ b/ngui/ui/src/containers/ShareRunLinkContainer/index.ts @@ -0,0 +1,3 @@ +import ShareRunLinkContainer from "./ShareRunLinkContainer"; + +export default ShareRunLinkContainer; diff --git a/ngui/ui/src/containers/StagesAndMilestonesContainer/StagesAndMilestonesContainer.tsx b/ngui/ui/src/containers/StagesAndMilestonesContainer/StagesAndMilestonesContainer.tsx deleted file mode 100644 index be42119c..00000000 --- a/ngui/ui/src/containers/StagesAndMilestonesContainer/StagesAndMilestonesContainer.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { getMilestoneTuplesGroupedByTime } from "components/ExecutionBreakdown/utils"; -import StagesAndMilestones from "components/StagesAndMilestones"; -import { getData } from "containers/ExecutionBreakdownContainer/utils"; -import MlTasksService from "services/MlTasksService"; - -const StagesAndMilestonesContainer = ({ - runId, - closeSideModal, - highlightedStage, - setHighlightedStage, - setSelectedSegment, - secondsTimeRange -}) => { - const { useGetRunBreakdown } = MlTasksService(); - - const { breakdown = {}, milestones: milestonesApi = [], stages: stagesApi = [] } = useGetRunBreakdown(runId); - - const { stages, milestones } = getData({ breakdown, milestones: milestonesApi, stages: stagesApi }); - - const milestonesGroupedByTimeTuples = getMilestoneTuplesGroupedByTime(milestones); - - return ( - { - setSelectedSegment([start, end]); - closeSideModal(); - }} - stages={stages} - highlightedStage={highlightedStage} - setHighlightedStage={(stage) => { - setHighlightedStage(stage); - closeSideModal(); - }} - secondsTimeRange={secondsTimeRange} - /> - ); -}; - -export default StagesAndMilestonesContainer; diff --git a/ngui/ui/src/containers/StagesAndMilestonesContainer/index.ts b/ngui/ui/src/containers/StagesAndMilestonesContainer/index.ts deleted file mode 100644 index 5643b4a2..00000000 --- a/ngui/ui/src/containers/StagesAndMilestonesContainer/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import StagesAndMilestonesContainer from "./StagesAndMilestonesContainer"; - -export default StagesAndMilestonesContainer; diff --git a/ngui/ui/src/graphql/api/restapi/queries/index.ts b/ngui/ui/src/graphql/api/restapi/queries/index.ts index ee27c5be..8140da05 100644 --- a/ngui/ui/src/graphql/api/restapi/queries/index.ts +++ b/ngui/ui/src/graphql/api/restapi/queries/index.ts @@ -1,3 +1,3 @@ -import { GET_DATA_SOURCE } from "./restapi.queries"; +import { GET_DATA_SOURCE, GET_LAYOUTS, GET_ML_EXECUTORS, GET_ML_RUN, GET_ML_RUN_BREAKDOWN } from "./restapi.queries"; -export { GET_DATA_SOURCE }; +export { GET_DATA_SOURCE, GET_ML_RUN, GET_ML_EXECUTORS, GET_ML_RUN_BREAKDOWN, GET_LAYOUTS }; diff --git a/ngui/ui/src/hooks/useTaskRunChartState.ts b/ngui/ui/src/hooks/useTaskRunChartState.ts index 6e7646e9..714a3ddf 100644 --- a/ngui/ui/src/hooks/useTaskRunChartState.ts +++ b/ngui/ui/src/hooks/useTaskRunChartState.ts @@ -197,7 +197,14 @@ const useGridTypeActions = ({ setSaved, setDashboard }) => { return { updateGridType }; }; -export const useTaskRunChartState = ({ taskId, implementedMetricsBreakdownNames, breakdownNames }) => { +export const useTaskRunChartState = ({ + organizationId, + arceeToken, + taskId, + implementedMetricsBreakdownNames, + breakdownNames, + isPublicRun = false +}) => { const { dashboardId: selectedDashboardId, setDashboardId: setSelectedDashboardId } = useTaskRunsDashboardState(taskId); const { useGetAll, useGetOneOnDemand, useUpdate, useDelete, useCreate } = LayoutsService(); @@ -206,7 +213,14 @@ export const useTaskRunChartState = ({ taskId, implementedMetricsBreakdownNames, const { onCreate } = useCreate(); const { onUpdate } = useUpdate(); - const [saved, setSaved] = useState(true); + const [saved, setSavedState] = useState(true); + + const setSaved = (newState: boolean) => { + if (isPublicRun) { + return; + } + setSavedState(newState); + }; const initializeDashboardState = useCallback( (dashboard) => { @@ -241,11 +255,13 @@ export const useTaskRunChartState = ({ taskId, implementedMetricsBreakdownNames, const getAllLayoutsApiParams = useMemo( () => ({ + organizationId, + arceeToken, layoutType: LAYOUT_TYPES.ML_RUN_CHARTS_DASHBOARD, entityId: taskId, includeShared: true }), - [taskId] + [arceeToken, organizationId, taskId] ); const onSuccessGetAllLayouts = useCallback( @@ -254,12 +270,14 @@ export const useTaskRunChartState = ({ taskId, implementedMetricsBreakdownNames, setDashboard(initializeDashboardState(DEFAULT_DASHBOARD)); } if (apiLayouts.find(({ id }) => id === selectedDashboardId)) { - onGet(selectedDashboardId).then((dashboardInfo) => { + onGet(organizationId, selectedDashboardId, { + arceeToken + }).then((dashboardInfo) => { setDashboard(initializeDashboardState(dashboardInfo)); }); } }, - [initializeDashboardState, onGet, selectedDashboardId] + [arceeToken, initializeDashboardState, onGet, organizationId, selectedDashboardId] ); const { layouts, currentEmployeeId, isLoading: isGetAllLoading } = useGetAll(getAllLayoutsApiParams, onSuccessGetAllLayouts); @@ -271,7 +289,9 @@ export const useTaskRunChartState = ({ taskId, implementedMetricsBreakdownNames, setDashboard(initializeDashboardState(DEFAULT_DASHBOARD)); setSelectedDashboardId(newDashboardId); } else { - onGet(newDashboardId).then((dashboardInfo) => { + onGet(organizationId, newDashboardId, { + arceeToken + }).then((dashboardInfo) => { setDashboard(initializeDashboardState(dashboardInfo)); setSelectedDashboardId(newDashboardId); }); diff --git a/ngui/ui/src/pages/PublicMlRun/PublicMlRun.tsx b/ngui/ui/src/pages/PublicMlRun/PublicMlRun.tsx new file mode 100644 index 00000000..278d9a4b --- /dev/null +++ b/ngui/ui/src/pages/PublicMlRun/PublicMlRun.tsx @@ -0,0 +1,5 @@ +import PublicMlRunContainer from "containers/PublicMlRunContainer"; + +const PublicMlRun = () => ; + +export default PublicMlRun; diff --git a/ngui/ui/src/pages/PublicMlRun/index.ts b/ngui/ui/src/pages/PublicMlRun/index.ts new file mode 100644 index 00000000..87c265d7 --- /dev/null +++ b/ngui/ui/src/pages/PublicMlRun/index.ts @@ -0,0 +1,3 @@ +import PublicMlRun from "./PublicMlRun"; + +export default PublicMlRun; diff --git a/ngui/ui/src/services/LayoutsService.ts b/ngui/ui/src/services/LayoutsService.ts index 088838cb..9d9dc222 100644 --- a/ngui/ui/src/services/LayoutsService.ts +++ b/ngui/ui/src/services/LayoutsService.ts @@ -20,9 +20,11 @@ type LayoutData = { const useGetAll = ( params: { + organizationId: string; layoutType: (typeof LAYOUT_TYPES)[keyof typeof LAYOUT_TYPES]; entityId?: string; includeShared?: boolean; + arceeToken?: string; }, onSuccess?: (layout: LayoutData) => void ): { @@ -32,9 +34,7 @@ const useGetAll = ( } => { const dispatch = useDispatch(); - const { layoutType, entityId, includeShared } = params; - - const { organizationId } = useOrganizationInfo(); + const { organizationId, layoutType, entityId, includeShared, arceeToken } = params; const { isLoading, shouldInvoke } = useApiState(GET_LAYOUTS, { organizationId, @@ -50,7 +50,7 @@ const useGetAll = ( useEffect(() => { if (shouldInvoke) { dispatch((_, getState) => { - dispatch(getLayouts(organizationId, { layoutType, entityId, includeShared })).then(() => { + dispatch(getLayouts(organizationId, { layoutType, entityId, includeShared, arceeToken })).then(() => { if (!isError(GET_LAYOUTS, getState())) { const apiData = getState()[RESTAPI][GET_LAYOUTS]; if (typeof onSuccess === "function") { @@ -60,7 +60,7 @@ const useGetAll = ( }); }); } - }, [shouldInvoke, dispatch, organizationId, entityId, includeShared, layoutType, onSuccess]); + }, [shouldInvoke, dispatch, organizationId, entityId, includeShared, layoutType, onSuccess, arceeToken]); return { isLoading, @@ -99,12 +99,10 @@ const useGetOneOnDemand = (): { isLoading: boolean; entityId: string; layout: LayoutData; - onGet: (layoutId: string) => Promise; + onGet: (organizationId: string, layoutId: string, params: { arceeToken?: string }) => Promise; } => { const dispatch = useDispatch(); - const { organizationId } = useOrganizationInfo(); - const { isLoading, entityId } = useApiState(GET_LAYOUT); const { apiData: layout } = useApiData(GET_LAYOUT); @@ -114,10 +112,10 @@ const useGetOneOnDemand = (): { entityId, layout, onGet: useCallback( - (layoutId) => + (organizationId, layoutId, params) => new Promise((resolve, reject) => { dispatch((_, getState) => { - dispatch(getLayout(organizationId, layoutId)).then(() => { + dispatch(getLayout(organizationId, layoutId, params)).then(() => { if (!isError(GET_LAYOUT, getState())) { const apiData = getState()[RESTAPI][GET_LAYOUT]; @@ -127,7 +125,7 @@ const useGetOneOnDemand = (): { }); }); }), - [dispatch, organizationId] + [dispatch] ) }; }; diff --git a/ngui/ui/src/services/MlArtifactsService.ts b/ngui/ui/src/services/MlArtifactsService.ts index 1702df8e..425cf19f 100644 --- a/ngui/ui/src/services/MlArtifactsService.ts +++ b/ngui/ui/src/services/MlArtifactsService.ts @@ -53,6 +53,7 @@ type CreateApplicationApiParams = { }; const useGet = ( + organizationId: string, params?: GetApiParams ): { isLoading: boolean; @@ -65,8 +66,6 @@ const useGet = ( } => { const dispatch = useDispatch(); - const { organizationId } = useOrganizationInfo(); - const { apiData } = useApiData(GET_ML_ARTIFACTS); const { isLoading, shouldInvoke } = useApiState(GET_ML_ARTIFACTS, { organizationId, ...params }); diff --git a/ngui/ui/src/services/MlExecutorsService.ts b/ngui/ui/src/services/MlExecutorsService.ts index aaed3e76..728b63f6 100644 --- a/ngui/ui/src/services/MlExecutorsService.ts +++ b/ngui/ui/src/services/MlExecutorsService.ts @@ -6,25 +6,31 @@ import { useApiData } from "hooks/useApiData"; import { useApiState } from "hooks/useApiState"; import { useOrganizationInfo } from "hooks/useOrganizationInfo"; -const useGet = ({ taskIds, runIds } = {}) => { +const useGet = ({ taskIds, runIds, organizationId, arceeToken } = {}) => { const dispatch = useDispatch(); - const { organizationId } = useOrganizationInfo(); + const { apiData: { executors = [] } } = useApiData(GET_ML_EXECUTORS); - const { isLoading, shouldInvoke } = useApiState(GET_ML_EXECUTORS, { organizationId, taskIds, runIds }); + const { isLoading, shouldInvoke } = useApiState(GET_ML_EXECUTORS, { + organizationId, + taskIds, + runIds, + arceeToken + }); useEffect(() => { if (shouldInvoke) { dispatch( getMlExecutors(organizationId, { taskIds, - runIds + runIds, + arceeToken }) ); } - }, [taskIds, dispatch, organizationId, runIds, shouldInvoke]); + }, [taskIds, dispatch, organizationId, runIds, arceeToken, shouldInvoke]); return { isLoading, executors }; }; diff --git a/ngui/ui/src/services/MlProfilingService.ts b/ngui/ui/src/services/MlProfilingService.ts index dbab9818..fcf5b83c 100644 --- a/ngui/ui/src/services/MlProfilingService.ts +++ b/ngui/ui/src/services/MlProfilingService.ts @@ -12,7 +12,7 @@ const useGetToken = () => { const { organizationId } = useOrganizationInfo(); const { - apiData: { token = "" } + apiData: { token = "", md5_token: md5Token = "" } } = useApiData(GET_PROFILING_TOKEN); const { isLoading, shouldInvoke } = useApiState(GET_PROFILING_TOKEN, { organizationId }); @@ -23,7 +23,7 @@ const useGetToken = () => { } }, [shouldInvoke, dispatch, organizationId]); - return { isLoading, token }; + return { isLoading, token, md5Token }; }; function MlProfilingService() { diff --git a/ngui/ui/src/services/MlTasksService.ts b/ngui/ui/src/services/MlTasksService.ts index 698d7a2d..6d3a6b14 100644 --- a/ngui/ui/src/services/MlTasksService.ts +++ b/ngui/ui/src/services/MlTasksService.ts @@ -276,20 +276,18 @@ const useGetTaskRunsBulk = (taskId, runIds) => { return { isLoading, isDataReady, runs }; }; -const useGetTaskRun = (runId) => { +const useGetTaskRun = (organizationId, runId, params) => { const dispatch = useDispatch(); - const { organizationId } = useOrganizationInfo(); - const { isLoading, isDataReady, shouldInvoke } = useApiState(GET_ML_RUN_DETAILS, { organizationId, runId }); const { apiData } = useApiData(GET_ML_RUN_DETAILS); useEffect(() => { if (shouldInvoke) { - dispatch(getMlRunDetails(organizationId, runId)); + dispatch(getMlRunDetails(organizationId, runId, params)); } - }, [dispatch, organizationId, runId, shouldInvoke]); + }, [dispatch, organizationId, runId, shouldInvoke, params]); return { isLoading, isDataReady, run: apiData }; }; @@ -314,11 +312,9 @@ const useGetTaskTags = (taskId) => { return { isLoading, tags }; }; -const useGetRunBreakdown = (runId) => { +const useGetRunBreakdown = (organizationId, runId, params) => { const dispatch = useDispatch(); - const { organizationId } = useOrganizationInfo(); - const { isLoading, isDataReady, shouldInvoke } = useApiState(GET_ML_RUN_DETAILS_BREAKDOWN, { organizationId, runId @@ -330,9 +326,9 @@ const useGetRunBreakdown = (runId) => { useEffect(() => { if (shouldInvoke) { - dispatch(getMlRunDetailsBreakdown(organizationId, runId)); + dispatch(getMlRunDetailsBreakdown(organizationId, runId, params)); } - }, [dispatch, organizationId, runId, shouldInvoke]); + }, [dispatch, organizationId, runId, shouldInvoke, params]); return { isLoading, isDataReady, breakdown, stages, milestones }; }; diff --git a/ngui/ui/src/translations/en-US/app.json b/ngui/ui/src/translations/en-US/app.json index 0746c0d5..22b2b47f 100644 --- a/ngui/ui/src/translations/en-US/app.json +++ b/ngui/ui/src/translations/en-US/app.json @@ -2020,6 +2020,8 @@ "shareLinkGoogleSingleHeader": "Single day expenses", "shareLinkGoogleSingleTip": "Expenses for a single day will be returned if just start_date is passed.", "shareLinkRemoveTip": "By toggling off that switch you will make current link obsolete. Each link is unique and could not be restored!", + "shareRunLinkDescription": "This link allows you to share the run with others. Anyone with the link will have access to view and analyze the run. Please ensure sharing complies with your organization's data-sharing policies.", + "shareRunLinkTitle": "Share Run Link", "shared": "Shared", "shortLivingInstances": "Short living instances", "shortLivingInstancesDescription": "Some of the instances you have been running for the last {daysThreshold} {daysThreshold, plural,\n =1 {day}\n other {days}\n} have existed for less than 6 hours and were not created as Spot (or Preemptible) Instances. Consider using Spot (Preemptible) Instances.", diff --git a/ngui/ui/src/urls.ts b/ngui/ui/src/urls.ts index 3f30f914..080be4c0 100644 --- a/ngui/ui/src/urls.ts +++ b/ngui/ui/src/urls.ts @@ -334,6 +334,18 @@ const ML_LAUNCH_BASE = "launch"; export const ML_RUNSETS_BASE = "runsets"; export const ML_RUN_BASE = "run"; +export const ML_PUBLIC_RUN_BASE = "run"; +export const ML_PUBLIC_RUN = concatenateUrl([ML_PUBLIC_RUN_BASE, ML_TASK_RUN_IDENTIFIER]); +export const getMlPublicRunUrl = (runId, { organizationId, arceeToken }) => { + const urlBase = ML_PUBLIC_RUN.replace(ML_TASK_RUN_IDENTIFIER, runId); + const searchParams = new URLSearchParams({ + organizationId, + token: arceeToken + }); + + return `${urlBase}?${searchParams.toString()}`; +}; + export const ML_TASKS = concatenateUrl([ML_TASKS_BASE]); export const ML_TASK_CREATE = concatenateUrl([ML_TASKS_BASE, CREATE]); diff --git a/ngui/ui/src/utils/columns/executor.tsx b/ngui/ui/src/utils/columns/executor.tsx index d5e092f8..a7ec20a0 100644 --- a/ngui/ui/src/utils/columns/executor.tsx +++ b/ngui/ui/src/utils/columns/executor.tsx @@ -4,7 +4,7 @@ import ExecutorLabel from "components/ExecutorLabel"; import KeyValueLabel from "components/KeyValueLabel/KeyValueLabel"; import TextWithDataTestId from "components/TextWithDataTestId"; -const executor = () => ({ +const executor = ({ disableLink = false }) => ({ header: ( @@ -38,7 +38,13 @@ const executor = () => ({ } ]} > - + ); } diff --git a/ngui/ui/src/utils/columns/mlExecutorLocation.tsx b/ngui/ui/src/utils/columns/mlExecutorLocation.tsx index 6504f2d1..a1d182a1 100644 --- a/ngui/ui/src/utils/columns/mlExecutorLocation.tsx +++ b/ngui/ui/src/utils/columns/mlExecutorLocation.tsx @@ -5,13 +5,13 @@ import QuestionMark from "components/QuestionMark"; import TextWithDataTestId from "components/TextWithDataTestId"; import { useOrganizationInfo } from "hooks/useOrganizationInfo"; -const Cell = ({ discovered, resource }) => { +const Cell = ({ discovered, resource, disableLink = false }) => { const { isDemo } = useOrganizationInfo(); if (discovered) { const { cloud_account: { id, name, type } = {} } = resource ?? {}; - return ; + return ; } return ( @@ -24,7 +24,7 @@ const Cell = ({ discovered, resource }) => { ); }; -const mlExecutorLocation = ({ headerDataTestId = "lbl_location", headerMessageId = "location" } = {}) => ({ +const mlExecutorLocation = ({ headerDataTestId = "lbl_location", headerMessageId = "location", disableLink = false } = {}) => ({ header: ( @@ -35,7 +35,7 @@ const mlExecutorLocation = ({ headerDataTestId = "lbl_location", headerMessageId row: { original: { discovered, resource } } - }) => + }) => }); export default mlExecutorLocation; diff --git a/ngui/ui/src/utils/routes/index.ts b/ngui/ui/src/utils/routes/index.ts index ccd826f9..2ad92cdf 100644 --- a/ngui/ui/src/utils/routes/index.ts +++ b/ngui/ui/src/utils/routes/index.ts @@ -77,6 +77,7 @@ import poolsRoute from "./poolsRoute"; import poolTtlAnalysisRoute from "./poolTtlAnalysisRoute"; import powerScheduleDetailsRoute from "./powerScheduleDetailsRoute"; import powerSchedulesRoute from "./powerSchedulesRoute"; +import publicMlRun from "./publicMlRun"; import quotaRoute from "./quotaRoute"; import quotasRoute from "./quotasRoute"; import recommendationsRoute from "./recommendationsRoute"; @@ -192,6 +193,7 @@ export const routes = [ mlCreateRunArtifactRoute, mlEditArtifactRoute, emailVerificationRoute, + publicMlRun, // React router 6.x does not require the not found route (*) to be at the end, // but the matchPath hook that is used in the DocsPanel component seems to honor the order. // Moving it to the bottom for "safety" reasons. diff --git a/ngui/ui/src/utils/routes/publicMlRun.ts b/ngui/ui/src/utils/routes/publicMlRun.ts new file mode 100644 index 00000000..9fbe316d --- /dev/null +++ b/ngui/ui/src/utils/routes/publicMlRun.ts @@ -0,0 +1,14 @@ +import { ML_PUBLIC_RUN } from "urls"; +import BaseRoute from "./baseRoute"; + +class PublicMlRun extends BaseRoute { + isTokenRequired = false; + + page = "PublicMlRun"; + + link = ML_PUBLIC_RUN; + + layout = null; +} + +export default new PublicMlRun(); From 96e35d232110df47f98108e9db7642e5769cce2b Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:41:08 +0300 Subject: [PATCH 23/65] OS-2372. Not send slack message to channels the bot is removed from --- .../controllers/send_message.py | 8 ++++- slacker/slacker_server/controllers/slack.py | 32 +++---------------- slacker/slacker_server/exceptions.py | 8 ++++- .../handlers/v2/send_message.py | 10 ++++-- slacker/slacker_server/slack_client.py | 24 ++++++++++++-- slacker/slacker_server/utils.py | 26 +++++++++++++++ 6 files changed, 75 insertions(+), 33 deletions(-) diff --git a/slacker/slacker_server/controllers/send_message.py b/slacker/slacker_server/controllers/send_message.py index 8f487e4f..334227f5 100644 --- a/slacker/slacker_server/controllers/send_message.py +++ b/slacker/slacker_server/controllers/send_message.py @@ -13,7 +13,8 @@ from slacker.slacker_server.message_templates.env_alerts import ( get_property_updated_message, get_message_changed_active_state, get_message_acquired, get_message_released) -from slacker.slacker_server.message_templates.warnings import get_archived_message_block +from slacker.slacker_server.message_templates.warnings import ( + get_archived_message_block) LOG = logging.getLogger(__name__) @@ -49,6 +50,11 @@ def send_message(self, **kwargs): auth_user_id]) team_id = user.slack_team_id channel_id = user.slack_channel_id + if channel_id.startswith('C'): + # public or private channel, not direct message + channels = self.app.client.get_bot_conversations(team_id=team_id) + if channel_id not in [x['id'] for x in channels]: + raise NotFoundException(Err.OS0020, [channel_id]) template_func = self.MESSAGE_TEMPLATES.get(type_) if template_func is None: diff --git a/slacker/slacker_server/controllers/slack.py b/slacker/slacker_server/controllers/slack.py index 4843c860..091de7d3 100644 --- a/slacker/slacker_server/controllers/slack.py +++ b/slacker/slacker_server/controllers/slack.py @@ -5,7 +5,6 @@ from datetime import datetime, timedelta from requests import HTTPError -from retrying import Retrying from slack_sdk.errors import SlackApiError from sqlalchemy.exc import IntegrityError @@ -16,7 +15,8 @@ get_add_constraint_envs_alert_modal) from slacker.slacker_server.message_templates.bookings import ( get_add_bookings_form, get_booking_details_message) -from slacker.slacker_server.message_templates.connect import get_welcome_message +from slacker.slacker_server.message_templates.connect import ( + get_welcome_message) from slacker.slacker_server.message_templates.constraints import ( get_update_ttl_form, get_constraint_updated) from slacker.slacker_server.message_templates.disconnect import ( @@ -24,45 +24,23 @@ from slacker.slacker_server.message_templates.envs import get_envs_message from slacker.slacker_server.message_templates.org import ( get_org_switch_message, get_org_switch_completed_message) -from slacker.slacker_server.message_templates.resources import get_resources_message +from slacker.slacker_server.message_templates.resources import ( + get_resources_message) from slacker.slacker_server.message_templates.resource_details import ( get_resource_details_message) from slacker.slacker_server.message_templates.errors import ( get_ca_not_connected_message, get_not_have_slack_permissions_message) from slacker.slacker_server.models.models import User -from slacker.slacker_server.utils import gen_id +from slacker.slacker_server.utils import gen_id, retry_too_many_requests from tools.optscale_time import utcfromtimestamp, utcnow_timestamp LOG = logging.getLogger(__name__) TTL_LIMIT_TO_SHOW = 72 EXPENSE_LIMIT_TO_SHOW = 0.9 -MS_IN_SEC = 1000 SEC_IN_HRS = 3600 MAX_MSG_ENVS_LENGTH = 10 -def retry_too_many_requests(f, *args, **kwargs): - try: - return f(*args, **kwargs) - except Exception as exc: - if retriable_slack_api_error(exc): - f_retry = Retrying( - retry_on_exception=retriable_slack_api_error, - wait_fixed=int(exc.response.headers['Retry-After']) * MS_IN_SEC, - stop_max_attempt_number=5) - res = f_retry.call(f, *args, **kwargs) - return res - else: - raise exc - - -def retriable_slack_api_error(exc): - if (isinstance(exc, SlackApiError) and - exc.response.headers.get('Retry-After')): - return True - return False - - class MetaSlackController: """ Using it to keep common logic between handler controllers and slack event diff --git a/slacker/slacker_server/exceptions.py b/slacker/slacker_server/exceptions.py index deddcdb5..9c8b629a 100644 --- a/slacker/slacker_server/exceptions.py +++ b/slacker/slacker_server/exceptions.py @@ -56,7 +56,8 @@ class Err(enum.Enum): OS0016 = [ "User with %s %s were not found", ["auth_user_id", "02430e6b-6975-4535-8bc6-7a7b52938014"], - ["User with auth_user_id 02430e6b-6975-4535-8bc6-7a7b52938014 were not found"] + ["User with auth_user_id 02430e6b-6975-4535-8bc6-7a7b52938014 were " + "not found"] ] OS0017 = [ "%s should provide only with %s", @@ -71,3 +72,8 @@ class Err(enum.Enum): ['channel_id'], ['Target slack channel FFFFFFFFF is archived'] ] + OS0020 = [ + "Slack app is not added to channel %s", + ['channel_id'], + ['Slack app is not added to channel C000000000'] + ] diff --git a/slacker/slacker_server/handlers/v2/send_message.py b/slacker/slacker_server/handlers/v2/send_message.py index b8866019..d857fb7c 100644 --- a/slacker/slacker_server/handlers/v2/send_message.py +++ b/slacker/slacker_server/handlers/v2/send_message.py @@ -1,6 +1,9 @@ from tools.optscale_exceptions.http_exc import OptHTTPError +from tools.optscale_exceptions.common_exc import NotFoundException -from slacker.slacker_server.controllers.send_message import SendMessageAsyncController +from slacker.slacker_server.controllers.send_message import ( + SendMessageAsyncController +) from slacker.slacker_server.exceptions import Err from slacker.slacker_server.handlers.v2.base import BaseHandler @@ -150,7 +153,10 @@ async def post(self, **kwargs): data = self._request_body() data.update(kwargs) await self.validate_params(**data) - await self.controller.send_message(**data) + try: + await self.controller.send_message(**data) + except NotFoundException as exc: + raise OptHTTPError.from_opt_exception(404, exc) self.write_json({}) self.set_status(201) diff --git a/slacker/slacker_server/slack_client.py b/slacker/slacker_server/slack_client.py index 1d0eadc2..55c4f746 100644 --- a/slacker/slacker_server/slack_client.py +++ b/slacker/slacker_server/slack_client.py @@ -1,6 +1,8 @@ import logging from slack_sdk.web.client import WebClient +from slacker.slacker_server.utils import retry_too_many_requests + LOG = logging.getLogger(__name__) @@ -9,8 +11,26 @@ def __init__(self, installation_store, **kwargs): self._installation_store = installation_store super().__init__(**kwargs) - def chat_post(self, *, channel_id=None, team_id=None, **kwargs): + def get_client(self, team_id=None): bot = self._installation_store.find_bot( team_id=team_id, enterprise_id=None) - client = WebClient(token=bot.bot_token) + return WebClient(token=bot.bot_token) + + def get_bot_conversations(self, team_id=None, exclude_archived=True, + types='public_channel, private_channel'): + client = self.get_client(team_id=team_id) + conversation_list = [] + cursor = '' + while True: + resp = retry_too_many_requests( + client.users_conversations, cursor=cursor, team_id=team_id, + types=types, limit=1000, exclude_archived=exclude_archived) + cursor = resp['response_metadata']['next_cursor'] + conversation_list.extend(resp['channels']) + if not cursor: + break + return conversation_list + + def chat_post(self, *, channel_id=None, team_id=None, **kwargs): + client = self.get_client(team_id=team_id) return client.chat_postMessage(channel=channel_id, **kwargs) diff --git a/slacker/slacker_server/utils.py b/slacker/slacker_server/utils.py index 0d850f4d..9f65ddcd 100644 --- a/slacker/slacker_server/utils.py +++ b/slacker/slacker_server/utils.py @@ -5,8 +5,11 @@ import json import logging import uuid +from retrying import Retrying +from slack_sdk.errors import SlackApiError +MS_IN_SEC = 1000 LOG = logging.getLogger(__name__) tp_executor = ThreadPoolExecutor(30) @@ -34,3 +37,26 @@ def default(self, obj): def gen_id(): return str(uuid.uuid4()) + + +def retriable_slack_api_error(exc): + if (isinstance(exc, SlackApiError) and + exc.response.headers.get('Retry-After')): + return True + return False + + +def retry_too_many_requests(f, *args, **kwargs): + try: + return f(*args, **kwargs) + except Exception as exc: + if retriable_slack_api_error(exc): + f_retry = Retrying( + retry_on_exception=retriable_slack_api_error, + wait_fixed=int( + exc.response.headers['Retry-After']) * MS_IN_SEC, + stop_max_attempt_number=5) + res = f_retry.call(f, *args, **kwargs) + return res + else: + raise exc From 680a2aab37883ddf0155d3c3565ba9c5f2bd3843 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:43:38 +0300 Subject: [PATCH 24/65] OS-1663. Use short cloud_resource_id in slack message --- slacker/slacker_server/message_templates/resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slacker/slacker_server/message_templates/resources.py b/slacker/slacker_server/message_templates/resources.py index 4ef76a65..1903ec75 100644 --- a/slacker/slacker_server/message_templates/resources.py +++ b/slacker/slacker_server/message_templates/resources.py @@ -7,7 +7,7 @@ def get_resource_blocks(resource_data, public_ip, org_id, currency='USD'): c_sign = CURRENCY_MAP.get(currency, '') r_id = resource_data['resource_id'] - r_cid = resource_data['cloud_resource_id'] + r_cid = resource_data['cloud_resource_id'].split('/')[-1] short_id = r_id[:4] r_name = resource_data.get('resource_name', '') r_ttl_constr = resource_data.get('ttl') From 624b3751194cfa54f31b698029538a317239da84 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:45:10 +0300 Subject: [PATCH 25/65] OS-2587. Show env_properties in resource_details slack message --- .../message_templates/resource_details.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/slacker/slacker_server/message_templates/resource_details.py b/slacker/slacker_server/message_templates/resource_details.py index 29d32bfd..68feeb44 100644 --- a/slacker/slacker_server/message_templates/resource_details.py +++ b/slacker/slacker_server/message_templates/resource_details.py @@ -79,6 +79,7 @@ def get_resource_details_message( total_cost = details.get('total_cost', 0) month_cost = details.get('cost', 0) tags = resource.get('tags', {}) + env_properties = resource.get('env_properties') constraint_types = ['ttl', 'daily_expense_limit'] if total_expense_limit_enabled: @@ -276,6 +277,24 @@ def get_resource_details_message( } } ] + env_prop_block = [] + if env_properties: + env_prop_block = [{ + "type": "section", + "text": { + "type": "mrkdwn", + "text": "\n*Environment properties:*" + } + }] + env_prop_block.extend( + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"{k}: {v}" + } + } for k, v in env_properties.items() + ) footer_blocks = [{ "type": "divider" @@ -296,6 +315,6 @@ def get_resource_details_message( return { "text": "Here are the details of the resource you asked", "blocks": (header_blocks + tags_blocks + resource_blocks + - booking_blocks + footer_blocks), + env_prop_block + booking_blocks + footer_blocks), "unfurl_links": False } From fb691e09ba7cdec17d6d23abd5a9bd6382e429f5 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:46:01 +0300 Subject: [PATCH 26/65] OS-3856. Fixed incorrect constraints in resource_details msg --- slacker/slacker_server/controllers/slack.py | 39 +++++++++++----- .../message_templates/resource_details.py | 44 ++++++++++--------- 2 files changed, 51 insertions(+), 32 deletions(-) diff --git a/slacker/slacker_server/controllers/slack.py b/slacker/slacker_server/controllers/slack.py index 091de7d3..6b6c3366 100644 --- a/slacker/slacker_server/controllers/slack.py +++ b/slacker/slacker_server/controllers/slack.py @@ -304,7 +304,8 @@ def _check_expense_limit(expense_constr): def resource_details(self, ack, say, action, body, logger): slack_user_id = body['user']['id'] user = self.get_user(slack_user_id) - if user is None or user.auth_user_id is None or user.organization_id is None: + if (user is None or user.auth_user_id is None + or user.organization_id is None): ack() return target_resource_id = action['value'] @@ -314,20 +315,35 @@ def resource_details(self, ack, say, action, body, logger): _, resource = rest_cl.cloud_resource_get( target_resource_id, details=True) _, org = rest_cl.organization_get(user.organization_id) + _, response = rest_cl.resource_limit_hits_list(target_resource_id) + limit_hits = response.get('limit_hits', []) + tel_enabled = self.total_expense_limit_enabled(user.organization_id) constraint_types = ['ttl', 'daily_expense_limit'] if tel_enabled: constraint_types.append('total_expense_limit') + constraints = {} - for constraint in constraint_types: - if resource['details']['constraints'].get(constraint): - constraints[constraint] = resource['details']['constraints'][ - constraint] - constraints[constraint]['constraint_type'] = 'resource specific' - elif resource['details']['policies'].get(constraint, {}).get('active'): - constraints[constraint] = resource['details']['policies'][ - constraint] - constraints[constraint]['constraint_type'] = 'pool policy' + for constraint_type in constraint_types: + constraint = {} + last_hit = next((x for x in limit_hits + if x['type'] == constraint_type), None) + if constraint_type in resource['details']['constraints']: + constraint = resource['details']['constraints'][ + constraint_type] + constraint['constraint_type'] = 'resource specific' + if (last_hit and not last_hit['pool_id'] + and last_hit['state'] == 'red'): + constraint['last_hit'] = last_hit + elif resource['details']['policies'].get(constraint_type, {}).get( + 'active'): + constraint = resource['details']['policies'][constraint_type] + constraint['constraint_type'] = 'pool policy' + if (last_hit and last_hit['pool_id'] == resource['pool_id'] + and last_hit['state'] == 'red'): + constraint['last_hit'] = last_hit + constraints[constraint_type] = constraint + resource['constraints'] = constraints current_booking = None if resource['details'].get('shareable_bookings'): @@ -345,7 +361,8 @@ def resource_details(self, ack, say, action, body, logger): say(get_resource_details_message( resource=resource, org_id=user.organization_id, public_ip=self.config_cl.public_ip(), booking=current_booking, - currency=org['currency'], total_expense_limit_enabled=tel_enabled)) + currency=org['currency'], total_expense_limit_enabled=tel_enabled + )) def create_update_ttl_view(self, ack, action, client, body, say, logger): slack_user_id = body['user']['id'] diff --git a/slacker/slacker_server/message_templates/resource_details.py b/slacker/slacker_server/message_templates/resource_details.py index 68feeb44..4680b149 100644 --- a/slacker/slacker_server/message_templates/resource_details.py +++ b/slacker/slacker_server/message_templates/resource_details.py @@ -54,16 +54,18 @@ def get_resource_details_block(resource, org_id, public_ip): def _get_expense_limit_msg(c_sign, total_cost, expense): expense_msg = "Not set" if expense: - if expense['limit'] < total_cost: - expense_msg = ":exclamation:*{0}{1}*".format( - c_sign, expense['limit']) - elif total_cost / expense['limit'] >= EXPENSE_LIMIT_TO_SHOW: - expense_msg = ":warning:{0}{1}".format( - c_sign, expense['limit']) - elif total_cost / expense['limit'] < EXPENSE_LIMIT_TO_SHOW: - expense_msg = "{0}{1}".format(c_sign, expense['limit']) + last_hit = expense.get('last_hit', {}) + if last_hit and last_hit['state'] == 'red': + if expense['limit'] < total_cost: + expense_msg = ":exclamation:*{0}{1}*".format( + c_sign, expense['limit']) + elif total_cost / expense['limit'] >= EXPENSE_LIMIT_TO_SHOW: + expense_msg = ":warning:{0}{1}".format( + c_sign, expense['limit']) elif expense['limit'] == 0: expense_msg = ":warning:No limit" + else: + expense_msg = "{0}{1}".format(c_sign, expense['limit']) return expense_msg @@ -84,17 +86,8 @@ def get_resource_details_message( constraint_types = ['ttl', 'daily_expense_limit'] if total_expense_limit_enabled: constraint_types.append('total_expense_limit') - constraints = {} - for constraint in constraint_types: - if details['constraints'].get(constraint): - constraints[constraint] = details['constraints'][constraint] - constraints[constraint]['constraint_type'] = '_(resource specific)_' - elif details['policies'].get(constraint, {}).get('active'): - constraints[constraint] = details['policies'][constraint] - constraints[constraint]['constraint_type'] = '_(pool policy)_' - else: - constraints[constraint] = {} + constraints = resource.get('constraints', {}) ttl = constraints.get('ttl') if ttl: hrs = (ttl['limit'] - utcnow_timestamp()) / SEC_IN_HRS @@ -114,11 +107,18 @@ def get_resource_details_message( else: ttl_msg = 'Not set' + ttl_constraint_type = constraints.get('ttl', {}).get('constraint_type', '') + if ttl_constraint_type: + ttl_constraint_type = f"_({ttl_constraint_type})_" + daily_expense = constraints.get('daily_expense_limit') daily_expense_msg = _get_expense_limit_msg(c_sign, total_cost, daily_expense) - daily_constaint_type = constraints['daily_expense_limit'].get( + + daily_constaint_type = constraints.get('daily_expense_limit', {}).get( 'constraint_type', '') + if daily_constaint_type: + daily_constaint_type = f"_({daily_constaint_type})_" header_blocks = [{ "type": "section", @@ -189,7 +189,7 @@ def get_resource_details_message( "text": { "type": "mrkdwn", "text": f"TTL\t\t\t\t\t\t\t{ttl_msg} " - f"{constraints['ttl'].get('constraint_type', '')}" + f"{ttl_constraint_type}" }, "accessory": { "type": "button", @@ -216,8 +216,10 @@ def get_resource_details_message( total_expense = constraints.get('total_expense_limit') total_expense_msg = _get_expense_limit_msg(c_sign, total_cost, total_expense) - total_constaint_type = constraints['total_expense_limit'].get( + total_constaint_type = constraints.get('total_expense_limit', {}).get( 'constraint_type', '') + if total_constaint_type: + total_constaint_type = f"_({total_constaint_type})_" resource_blocks.append( { "type": "section", From 37346f495ceab33adf47ce2c005f846b529b318b Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:49:31 +0300 Subject: [PATCH 27/65] OS-1714. Changed error text for slacker --- slacker/slacker_server/exceptions.py | 6 +++--- slacker/slacker_server/handlers/v2/send_message.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/slacker/slacker_server/exceptions.py b/slacker/slacker_server/exceptions.py index 9c8b629a..d9297a82 100644 --- a/slacker/slacker_server/exceptions.py +++ b/slacker/slacker_server/exceptions.py @@ -54,10 +54,10 @@ class Err(enum.Enum): ["channel_id and auth_user_id could not be provided at the same time"] ] OS0016 = [ - "User with %s %s were not found", + "User with %s %s was not found", ["auth_user_id", "02430e6b-6975-4535-8bc6-7a7b52938014"], - ["User with auth_user_id 02430e6b-6975-4535-8bc6-7a7b52938014 were " - "not found"] + ["User with auth_user_id 02430e6b-6975-4535-8bc6-7a7b52938014 was not " + "found"] ] OS0017 = [ "%s should provide only with %s", diff --git a/slacker/slacker_server/handlers/v2/send_message.py b/slacker/slacker_server/handlers/v2/send_message.py index d857fb7c..d1c32695 100644 --- a/slacker/slacker_server/handlers/v2/send_message.py +++ b/slacker/slacker_server/handlers/v2/send_message.py @@ -135,7 +135,7 @@ async def post(self, **kwargs): - OS0012: Duplicated parameters in path and body - OS0014: channel_id with team_id or auth_user_id should be provided - OS0015: channel_id and auth_user_id could not be provided at the same time - - OS0016: User not found + - OS0016: User with auth_user_id was not found - OS0017: channel_id should provide only with team_id - OS0019: Target slack channel is archived 401: From 0027a14adb6f8a8efea2955b3330d9adb5ba72e7 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:11:21 +0300 Subject: [PATCH 28/65] OS-2371. Fixed 500 on constraint_alert msg if user added to several spaces --- .../controllers/send_message.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/slacker/slacker_server/controllers/send_message.py b/slacker/slacker_server/controllers/send_message.py index 334227f5..6b8caebe 100644 --- a/slacker/slacker_server/controllers/send_message.py +++ b/slacker/slacker_server/controllers/send_message.py @@ -40,17 +40,20 @@ def send_message(self, **kwargs): parameters = kwargs.get('parameters', {}) warning = parameters.pop('warning', None) warning_params = parameters.pop('warning_params', None) + teams_channels = set() if auth_user_id: - user = self.session.query(User).filter( + users = self.session.query(User).filter( User.auth_user_id == auth_user_id, User.deleted.is_(False), - ).one_or_none() - if not user: + ).all() + if not users: raise NotFoundException(Err.OS0016, ['auth_user_id', auth_user_id]) - team_id = user.slack_team_id - channel_id = user.slack_channel_id - if channel_id.startswith('C'): + for user in users: + teams_channels.add((user.slack_channel_id, user.slack_team_id)) + if team_id or channel_id: + teams_channels.add((channel_id, team_id)) + if channel_id and channel_id.startswith('C'): # public or private channel, not direct message channels = self.app.client.get_bot_conversations(team_id=team_id) if channel_id not in [x['id'] for x in channels]: @@ -70,9 +73,11 @@ def send_message(self, **kwargs): **warning_params) + message['blocks'] try: - self.app.client.chat_post( - channel_id=channel_id, team_id=team_id, - **message) + for data in teams_channels: + channel_id, team_id = data + self.app.client.chat_post( + channel_id=channel_id, team_id=team_id, + **message) except TypeError as exc: LOG.error('Failed to send message: %s', exc) raise WrongArgumentsException(Err.OS0011, ['parameters']) From 8ee0fdda49634e9ee316d95ca3e14d454a9c977d Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Fri, 13 Dec 2024 10:37:34 +0300 Subject: [PATCH 29/65] OS-4897. Discover SGs as strings in AWS --- .../modules/recommendations/insecure_security_groups.py | 3 +-- tools/cloud_adapter/clouds/aws.py | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bumiworker/bumiworker/modules/recommendations/insecure_security_groups.py b/bumiworker/bumiworker/modules/recommendations/insecure_security_groups.py index 330d111d..b1a367b1 100644 --- a/bumiworker/bumiworker/modules/recommendations/insecure_security_groups.py +++ b/bumiworker/bumiworker/modules/recommendations/insecure_security_groups.py @@ -159,8 +159,7 @@ def _get_aws_insecure(self, config, resources, excluded_pools, if s_groups is None: continue security_groups_map = region_sg_map[region] - for group in s_groups: - group_id = group['GroupId'] + for group_id in s_groups: instances = security_groups_map.get(group_id, []) instances.append(instance) security_groups_map[group_id] = instances diff --git a/tools/cloud_adapter/clouds/aws.py b/tools/cloud_adapter/clouds/aws.py index 6d4f30bf..0b6a8b28 100644 --- a/tools/cloud_adapter/clouds/aws.py +++ b/tools/cloud_adapter/clouds/aws.py @@ -278,6 +278,8 @@ def discover_region_instances(self, region): next_token = described.get('NextToken') for reservation in described['Reservations']: for instance in reservation['Instances']: + sg_ids = [x['GroupId'] for x in instance.get( + 'SecurityGroups', [])] dates = [x['Ebs']['AttachTime'] for x in instance[ 'BlockDeviceMappings'] if 'Ebs' in x] dates.extend(list(map( @@ -293,7 +295,7 @@ def discover_region_instances(self, region): region=region, name=self._extract_tag(instance, 'Name'), flavor=instance['InstanceType'], - security_groups=instance.get('SecurityGroups', []), + security_groups=sg_ids, organization_id=self.organization_id, tags=self._extract_tags(instance), spotted=spotted, From 3f6b6b7b69b592ebbda203aff027a7da71a0a074 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Fri, 13 Dec 2024 10:38:24 +0300 Subject: [PATCH 30/65] OS-4038. Not raise error in available_filters on missing cloud accounts --- .../controllers/available_filters.py | 14 +++++++++++++- .../handlers/v2/available_filters.py | 4 ---- .../tests/unittests/test_available_filters.py | 19 +++++++++++++++++-- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/rest_api/rest_api_server/controllers/available_filters.py b/rest_api/rest_api_server/controllers/available_filters.py index c65b66ff..c2d36395 100644 --- a/rest_api/rest_api_server/controllers/available_filters.py +++ b/rest_api/rest_api_server/controllers/available_filters.py @@ -1,6 +1,11 @@ import logging from collections import defaultdict -from rest_api.rest_api_server.controllers.base_async import BaseAsyncControllerWrapper +from tools.optscale_exceptions.common_exc import ( + FailedDependency +) +from rest_api.rest_api_server.controllers.base_async import ( + BaseAsyncControllerWrapper +) from rest_api.rest_api_server.controllers.expense import CleanExpenseController from rest_api.rest_api_server.utils import encode_string, get_nil_uuid @@ -241,6 +246,13 @@ def _aggregate_resource_data(self, match_query, **kwargs): {'$group': group_stage} ], allowDiskUse=True) + def get(self, organization_id, **params): + try: + self.get_organization_and_cloud_accs(organization_id) + except FailedDependency: + return self._get_base_result({}) + return super().get(organization_id, **params) + class AvailableFiltersAsyncController(BaseAsyncControllerWrapper): diff --git a/rest_api/rest_api_server/handlers/v2/available_filters.py b/rest_api/rest_api_server/handlers/v2/available_filters.py index af88fcaa..8ab653ac 100644 --- a/rest_api/rest_api_server/handlers/v2/available_filters.py +++ b/rest_api/rest_api_server/handlers/v2/available_filters.py @@ -463,10 +463,6 @@ async def get(self, organization_id, **url_params): description: | Not found: - OE0002: Organization not found - 424: - description: | - Failed dependency: - - OE0445: Organization doesn't have any cloud accounts connected security: - token: [] - secret: [] diff --git a/rest_api/rest_api_server/tests/unittests/test_available_filters.py b/rest_api/rest_api_server/tests/unittests/test_available_filters.py index 0046d489..eb760652 100644 --- a/rest_api/rest_api_server/tests/unittests/test_available_filters.py +++ b/rest_api/rest_api_server/tests/unittests/test_available_filters.py @@ -101,8 +101,7 @@ def test_available_filters_limit(self): self.assertEqual(response['error']['error_code'], 'OE0212') def test_invalid_organization(self): - day_in_month = datetime(2020, 1, 14) - + day_in_month = datetime(2020, 1, 14, tzinfo=timezone.utc) time = int(day_in_month.timestamp()) valid_aws_cloud_acc = { 'name': 'my cloud_acc', @@ -118,6 +117,12 @@ def test_invalid_organization(self): self.assertEqual(code, 201) _, organization2 = self.client.organization_create( {'name': "organization2"}) + _, employee2 = self.client.employee_create( + organization2['id'], + {'name': 'name2', 'auth_user_id': self.auth_user_id_1}) + code, cloud_acc2 = self.create_cloud_account( + organization2['id'], valid_aws_cloud_acc) + self.assertEqual(code, 201) filters = { 'cloud_account_id': [cloud_acc1['id']] } @@ -150,3 +155,13 @@ def test_available_filters_default_values_if_filtered_by_entity(self): self.org_id, min_timestamp, max_timestamp, filters) self.assertEqual(code, 200) self.assertEqual(response['filter_values']['pool'], []) + + def test_available_filters_no_cloud_account(self): + _, org = self.client.organization_create( + {'name': "organization1"}) + max_timestamp = int(datetime.max.replace( + tzinfo=timezone.utc).timestamp()) - 1 + code, response = self.client.available_filters_get( + org['id'], 0, max_timestamp, {}) + self.assertEqual(code, 200) + self.assertEqual(response['filter_values'], {}) From 4110cb9afcca414df98bfcbfcdf6b83f53c0196d Mon Sep 17 00:00:00 2001 From: ek-hystax <33006768+ek-hystax@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:43:02 +0400 Subject: [PATCH 31/65] OS-8059. [Dependabot] Infinite loop in nanoid --- jira_ui/ui/package-lock.json | 40 +++++++++++----------- jira_ui/ui/package.json | 4 +-- ngui/ui/package.json | 6 ++-- ngui/ui/pnpm-lock.yaml | 64 ++++++++++++++++++------------------ 4 files changed, 57 insertions(+), 57 deletions(-) diff --git a/jira_ui/ui/package-lock.json b/jira_ui/ui/package-lock.json index 1a233eb3..5199b788 100644 --- a/jira_ui/ui/package-lock.json +++ b/jira_ui/ui/package-lock.json @@ -27,8 +27,8 @@ "react-markdown": "^9.0.1", "react-router-dom": "^6.19.0", "remark-gfm": "^4.0.0", - "vite": "^5.4.6", - "vite-tsconfig-paths": "^5.0.1" + "vite": "^5.4.11", + "vite-tsconfig-paths": "^5.1.4" }, "devDependencies": { "eslint": "^8.57.0", @@ -7993,9 +7993,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -9899,9 +9899,9 @@ } }, "node_modules/vite": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", - "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -9957,9 +9957,9 @@ } }, "node_modules/vite-tsconfig-paths": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.0.1.tgz", - "integrity": "sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", @@ -15556,9 +15556,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==" }, "natural-compare": { "version": "1.4.0", @@ -16862,9 +16862,9 @@ } }, "vite": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", - "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", "requires": { "esbuild": "^0.21.3", "fsevents": "~2.3.3", @@ -16873,9 +16873,9 @@ } }, "vite-tsconfig-paths": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.0.1.tgz", - "integrity": "sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", "requires": { "debug": "^4.1.1", "globrex": "^0.1.2", diff --git a/jira_ui/ui/package.json b/jira_ui/ui/package.json index 33a6b201..2e15ffde 100644 --- a/jira_ui/ui/package.json +++ b/jira_ui/ui/package.json @@ -23,8 +23,8 @@ "react-markdown": "^9.0.1", "react-router-dom": "^6.19.0", "remark-gfm": "^4.0.0", - "vite": "^5.4.6", - "vite-tsconfig-paths": "^5.0.1" + "vite": "^5.4.11", + "vite-tsconfig-paths": "^5.1.4" }, "scripts": { "start": "vite", diff --git a/ngui/ui/package.json b/ngui/ui/package.json index 72718458..c17060d9 100644 --- a/ngui/ui/package.json +++ b/ngui/ui/package.json @@ -28,7 +28,7 @@ "@types/react": "^18.2.45", "@types/react-dom": "^18.2.18", "@uiw/react-textarea-code-editor": "^2.1.1", - "@vitejs/plugin-react-swc": "^3.7.0", + "@vitejs/plugin-react-swc": "^3.7.2", "ajv": "^8.12.0", "analytics": "^0.8.1", "axios": "^1.7.4", @@ -74,8 +74,8 @@ "typescript": "^5.3.3", "unist-util-visit": "^5.0.0", "uuid": "^9.0.0", - "vite": "^5.4.6", - "vite-tsconfig-paths": "^5.0.1" + "vite": "^5.4.11", + "vite-tsconfig-paths": "^5.1.4" }, "scripts": { "start": "vite", diff --git a/ngui/ui/pnpm-lock.yaml b/ngui/ui/pnpm-lock.yaml index b471b76a..2043df4b 100644 --- a/ngui/ui/pnpm-lock.yaml +++ b/ngui/ui/pnpm-lock.yaml @@ -81,8 +81,8 @@ dependencies: specifier: ^2.1.1 version: 2.1.1(@babel/runtime@7.24.7)(react-dom@18.2.0)(react@18.2.0) '@vitejs/plugin-react-swc': - specifier: ^3.7.0 - version: 3.7.0(vite@5.4.6) + specifier: ^3.7.2 + version: 3.7.2(vite@5.4.11) ajv: specifier: ^8.12.0 version: 8.12.0 @@ -219,11 +219,11 @@ dependencies: specifier: ^9.0.0 version: 9.0.0 vite: - specifier: ^5.4.6 - version: 5.4.6(@types/node@20.10.5) + specifier: ^5.4.11 + version: 5.4.11(@types/node@20.10.5) vite-tsconfig-paths: - specifier: ^5.0.1 - version: 5.0.1(typescript@5.3.3)(vite@5.4.6) + specifier: ^5.1.4 + version: 5.1.4(typescript@5.3.3)(vite@5.4.11) devDependencies: '@storybook/addon-actions': @@ -246,7 +246,7 @@ devDependencies: version: 7.6.20(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) '@storybook/react-vite': specifier: ^7.6.20 - version: 7.6.20(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)(vite@5.4.6) + version: 7.6.20(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)(vite@5.4.11) '@storybook/theming': specifier: ^7.6.5 version: 7.6.5(react-dom@18.2.0)(react@18.2.0) @@ -2831,7 +2831,7 @@ packages: chalk: 4.1.2 dev: true - /@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.3.3)(vite@5.4.6): + /@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.3.3)(vite@5.4.11): resolution: {integrity: sha512-2D6y7fNvFmsLmRt6UCOFJPvFoPMJGT0Uh1Wg0RaigUp7kdQPs6yYn8Dmx6GZkOH/NW0yMTwRz/p0SRMMRo50vA==} peerDependencies: typescript: '>= 4.3.x' @@ -2845,7 +2845,7 @@ packages: magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.3.3) typescript: 5.3.3 - vite: 5.4.6(@types/node@20.10.5) + vite: 5.4.11(@types/node@20.10.5) dev: true /@jridgewell/gen-mapping@0.3.3: @@ -4650,7 +4650,7 @@ packages: - supports-color dev: true - /@storybook/builder-vite@7.6.20(typescript@5.3.3)(vite@5.4.6): + /@storybook/builder-vite@7.6.20(typescript@5.3.3)(vite@5.4.11): resolution: {integrity: sha512-q3vf8heE7EaVYTWlm768ewaJ9lh6v/KfoPPeHxXxzSstg4ByP9kg4E1mrfAo/l6broE9E9zo3/Q4gsM/G/rw8Q==} peerDependencies: '@preact/preset-vite': '*' @@ -4682,7 +4682,7 @@ packages: magic-string: 0.30.5 rollup: 3.29.5 typescript: 5.3.3 - vite: 5.4.6(@types/node@20.10.5) + vite: 5.4.11(@types/node@20.10.5) transitivePeerDependencies: - encoding - supports-color @@ -5087,7 +5087,7 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/react-vite@7.6.20(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)(vite@5.4.6): + /@storybook/react-vite@7.6.20(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)(vite@5.4.11): resolution: {integrity: sha512-uKuBFyGPZxpfR8vpDU/2OE9v7iTaxwL7ldd7k1swYd1rTSAPacTnEHSMl1R5AjUhkdI7gRmGN9q7qiVfK2XJCA==} engines: {node: '>=16'} peerDependencies: @@ -5095,16 +5095,16 @@ packages: react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 vite: ^3.0.0 || ^4.0.0 || ^5.0.0 dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.3.3)(vite@5.4.6) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.3.3)(vite@5.4.11) '@rollup/pluginutils': 5.1.0 - '@storybook/builder-vite': 7.6.20(typescript@5.3.3)(vite@5.4.6) + '@storybook/builder-vite': 7.6.20(typescript@5.3.3)(vite@5.4.11) '@storybook/react': 7.6.20(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) - '@vitejs/plugin-react': 3.1.0(vite@5.4.6) + '@vitejs/plugin-react': 3.1.0(vite@5.4.11) magic-string: 0.30.5 react: 18.2.0 react-docgen: 7.0.1 react-dom: 18.2.0(react@18.2.0) - vite: 5.4.6(@types/node@20.10.5) + vite: 5.4.11(@types/node@20.10.5) transitivePeerDependencies: - '@preact/preset-vite' - encoding @@ -6049,18 +6049,18 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vitejs/plugin-react-swc@3.7.0(vite@5.4.6): - resolution: {integrity: sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA==} + /@vitejs/plugin-react-swc@3.7.2(vite@5.4.11): + resolution: {integrity: sha512-y0byko2b2tSVVf5Gpng1eEhX1OvPC7x8yns1Fx8jDzlJp4LS6CMkCPfLw47cjyoMrshQDoQw4qcgjsU9VvlCew==} peerDependencies: - vite: ^4 || ^5 + vite: ^4 || ^5 || ^6 dependencies: '@swc/core': 1.7.26 - vite: 5.4.6(@types/node@20.10.5) + vite: 5.4.11(@types/node@20.10.5) transitivePeerDependencies: - '@swc/helpers' dev: false - /@vitejs/plugin-react@3.1.0(vite@5.4.6): + /@vitejs/plugin-react@3.1.0(vite@5.4.11): resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -6071,7 +6071,7 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.6) magic-string: 0.27.0 react-refresh: 0.14.0 - vite: 5.4.6(@types/node@20.10.5) + vite: 5.4.11(@types/node@20.10.5) transitivePeerDependencies: - supports-color dev: true @@ -11471,8 +11471,8 @@ packages: resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==} dev: false - /nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + /nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -12115,7 +12115,7 @@ packages: resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} engines: {node: ^10 || ^12 || >=14} dependencies: - nanoid: 3.3.7 + nanoid: 3.3.8 picocolors: 1.1.0 source-map-js: 1.2.1 @@ -14586,7 +14586,7 @@ packages: mlly: 1.4.2 pathe: 1.1.2 picocolors: 1.1.0 - vite: 5.4.6(@types/node@20.10.5) + vite: 5.4.11(@types/node@20.10.5) transitivePeerDependencies: - '@types/node' - less @@ -14599,8 +14599,8 @@ packages: - terser dev: true - /vite-tsconfig-paths@5.0.1(typescript@5.3.3)(vite@5.4.6): - resolution: {integrity: sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==} + /vite-tsconfig-paths@5.1.4(typescript@5.3.3)(vite@5.4.11): + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} peerDependencies: vite: '*' peerDependenciesMeta: @@ -14610,14 +14610,14 @@ packages: debug: 4.3.5 globrex: 0.1.2 tsconfck: 3.0.3(typescript@5.3.3) - vite: 5.4.6(@types/node@20.10.5) + vite: 5.4.11(@types/node@20.10.5) transitivePeerDependencies: - supports-color - typescript dev: false - /vite@5.4.6(@types/node@20.10.5): - resolution: {integrity: sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==} + /vite@5.4.11(@types/node@20.10.5): + resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -14707,7 +14707,7 @@ packages: strip-literal: 1.3.0 tinybench: 2.5.1 tinypool: 0.7.0 - vite: 5.4.6(@types/node@20.10.5) + vite: 5.4.11(@types/node@20.10.5) vite-node: 0.34.6(@types/node@20.10.5) why-is-node-running: 2.2.2 transitivePeerDependencies: From 7b324d5e0fbd4c291f321be10576dddfbfcd0e4f Mon Sep 17 00:00:00 2001 From: ek-hystax <33006768+ek-hystax@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:43:46 +0400 Subject: [PATCH 32/65] OS-8056. Relocate the "Add" Power Schedule button to the table action bar --- .../PowerSchedules/PowerSchedules.tsx | 44 ++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/ngui/ui/src/components/PowerSchedules/PowerSchedules.tsx b/ngui/ui/src/components/PowerSchedules/PowerSchedules.tsx index 51c7dd65..08b03907 100644 --- a/ngui/ui/src/components/PowerSchedules/PowerSchedules.tsx +++ b/ngui/ui/src/components/PowerSchedules/PowerSchedules.tsx @@ -47,20 +47,7 @@ const PowerSchedules = ({ const actionBarDefinition = { title: { messageId: "powerSchedulesTitle" - }, - items: [ - { - key: "btn-add", - dataTestId: "btn_add", - icon: , - messageId: "add", - color: "success", - variant: "contained", - type: "button", - requiredActions: ["EDIT_PARTNER"], - action: () => navigate(CREATE_POWER_SCHEDULE) - } - ] + } }; const tableData = useMemo(() => powerSchedules, [powerSchedules]); @@ -169,7 +156,34 @@ const PowerSchedules = ({ <> - {isGetPowerSchedulesLoading ? :
} + {isGetPowerSchedulesLoading ? ( + + ) : ( +
, + messageId: "add", + color: "success", + variant: "contained", + type: "button", + requiredActions: ["EDIT_PARTNER"], + action: () => navigate(CREATE_POWER_SCHEDULE) + } + ] + } + }} + pageSize={50} + /> + )} ); From 8c1e23e10f101a3463665efdc2e9c9b88c73b3f8 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Mon, 16 Dec 2024 05:36:47 +0000 Subject: [PATCH 33/65] OS-4103. Return limit_hits param if hit_days=0 in list org constraints --- .../controllers/organization_constraint.py | 3 +-- .../unittests/test_organization_constraints.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/rest_api/rest_api_server/controllers/organization_constraint.py b/rest_api/rest_api_server/controllers/organization_constraint.py index 8d9ec2e2..58a55bb9 100644 --- a/rest_api/rest_api_server/controllers/organization_constraint.py +++ b/rest_api/rest_api_server/controllers/organization_constraint.py @@ -518,8 +518,7 @@ def list(self, **kwargs): extended_filters = self._extend_filters( organization_id, c.loaded_filters) c.filters = json.dumps(extended_filters) - if hit_days: - c.limit_hits = constraint_hits_map.get(c.id, []) + c.limit_hits = constraint_hits_map.get(c.id, []) return result def delete_constraint_by_id(self, constraint_id): diff --git a/rest_api/rest_api_server/tests/unittests/test_organization_constraints.py b/rest_api/rest_api_server/tests/unittests/test_organization_constraints.py index 86aef9df..add9df1b 100644 --- a/rest_api/rest_api_server/tests/unittests/test_organization_constraints.py +++ b/rest_api/rest_api_server/tests/unittests/test_organization_constraints.py @@ -795,6 +795,20 @@ def test_list_constraints_with_hit_days(self): self.assertNotIn( 'constraint', resp['organization_constraints'][0].keys()) + def test_list_constraints_with_zero_hit_days(self): + constr = self.create_org_constraint(self.org_id, self.pool_id) + self.create_org_limit_hit(self.org_id, self.pool_id, + constraint_id=constr['id']) + code, resp = self.client.organization_constraint_list(self.org_id, + hit_days=0) + self.assertEqual(code, 200) + self.assertEqual(len(resp['organization_constraints']), 3) + resp_constraint = [x for x in resp['organization_constraints'] + if x['id'] == constr['id']][0] + self.assertEqual(len(resp_constraint['limit_hits']), 0) + self.assertNotIn( + 'constraint', resp['organization_constraints'][0].keys()) + def test_limit_hit_with_invalid_hit_days(self): code, resp = self.client.organization_constraint_list(self.org_id, hit_days='str') From a01d42ab9cf7659f8b3e80b1e4dd916814dd604d Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Mon, 16 Dec 2024 05:44:09 +0000 Subject: [PATCH 34/65] OS-5155. Added OE0455 to swagger --- rest_api/rest_api_server/handlers/v2/cloud_account.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_api/rest_api_server/handlers/v2/cloud_account.py b/rest_api/rest_api_server/handlers/v2/cloud_account.py index f57eb314..b77e380d 100644 --- a/rest_api/rest_api_server/handlers/v2/cloud_account.py +++ b/rest_api/rest_api_server/handlers/v2/cloud_account.py @@ -94,6 +94,7 @@ async def post(self, **url_params): - OE0226: Argument should be True or False - OE0371: Unable to configure billing report - OE0437: Can’t connect the cloud subscription + - OE0455: Cloud connection error - OE0456: Duplicate path parameters in the request body - OE0513: Cloud validation is timed out. Please retry later 401: @@ -571,6 +572,7 @@ async def patch(self, id, **kwargs): - OE0226: Argument should be True or False - OE0371: Unable to configure billing report - OE0437: Can’t connect the cloud subscription + - OE0455: Cloud connection error - OE0449: Parameter of cloud account can\'t be changed - OE0559: Parameter date should be between a month and a year ago - OE0560: Changing import dates is not supported for cloud account type From 873eeb9edcdbb410f44afb23777241bc0b45228a Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Mon, 16 Dec 2024 05:45:31 +0000 Subject: [PATCH 35/65] OS-8026. Fixed swagger in patch user --- auth/auth_server/handlers/v2/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/auth_server/handlers/v2/users.py b/auth/auth_server/handlers/v2/users.py index 898df533..304f4b12 100644 --- a/auth/auth_server/handlers/v2/users.py +++ b/auth/auth_server/handlers/v2/users.py @@ -101,7 +101,7 @@ async def patch(self, user_id, **kwargs): Required permission: EDIT_USER_INFO or ACTIVATE_USER or RESET_USER_PASSWORD parameters: - - name: id + - name: user_id in: path description: ID of user to modify required: true From 73762fc7ac6cf90624a0a41119a173f1b7e016a1 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Mon, 16 Dec 2024 05:46:31 +0000 Subject: [PATCH 36/65] OS-5101. Fixed showing deleted resources in env cloud account --- .../rest_api_server/controllers/expense.py | 1 + .../tests/unittests/test_cloud_accounts.py | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/rest_api/rest_api_server/controllers/expense.py b/rest_api/rest_api_server/controllers/expense.py index 6cb5edab..c013dbbe 100644 --- a/rest_api/rest_api_server/controllers/expense.py +++ b/rest_api/rest_api_server/controllers/expense.py @@ -121,6 +121,7 @@ def get_cloud_expenses_with_resource_info(self, cloud_acc_list, start_date, hour=0, minute=0, second=0, microsecond=0)}}, {'first_seen': {'$lt': int(end_date.timestamp())}}, {'last_seen': {'$gte': int(start_date.timestamp())}}, + {'deleted_at': 0} ] } }, diff --git a/rest_api/rest_api_server/tests/unittests/test_cloud_accounts.py b/rest_api/rest_api_server/tests/unittests/test_cloud_accounts.py index f81f6323..64982bd3 100644 --- a/rest_api/rest_api_server/tests/unittests/test_cloud_accounts.py +++ b/rest_api/rest_api_server/tests/unittests/test_cloud_accounts.py @@ -687,6 +687,50 @@ def test_get_details(self): self.assertEqual(details['resources'], 2) self.assertDictEqual(cloud_discovery_info, res_discovery_info) + def test_get_details_deleted_res(self): + self.valid_aws_cloud_acc['name'] = 'cloud_1' + code, cloud_acc1 = self.create_cloud_account( + self.org_id, self.valid_aws_cloud_acc) + self.assertEqual(code, 201) + + day_in_month = datetime.datetime(2020, 1, 14) + + _, resource = self.cloud_resource_create( + cloud_acc1['id'], { + 'cloud_resource_id': 'cloud_resource_id', + 'resource_type': 'resource_type', + 'first_seen': int(day_in_month.timestamp()), + 'last_seen': int(day_in_month.timestamp()) + }) + self.expenses.append({ + 'resource_id': resource['id'], + 'cost': 100, + 'date': day_in_month, + 'cloud_account_id': cloud_acc1['id'], + 'sign': 1, + }) + + code, res = self.client.discovery_info_list( + cloud_acc1['id']) + self.assertEqual(code, 200) + res_discovery_info = {di['id']: di for di in res['discovery_info']} + + self.resources_collection.update_one({'_id': resource['id']}, + {'$set': {'deleted_at': 1}}) + + with freeze_time(datetime.datetime(2020, 1, 15)): + code, cloud_acc = self.client.cloud_account_get( + cloud_acc1['id'], details=True) + details = cloud_acc['details'] + cloud_discovery_info = { + di['id']: di for di in details['discovery_infos'] + } + self.assertEqual(details['cost'], 0) + self.assertEqual(details['forecast'], 0) + self.assertEqual(details['last_month_cost'], 0) + self.assertEqual(details['resources'], 0) + self.assertDictEqual(cloud_discovery_info, res_discovery_info) + def test_patch_enable_import(self): ca_params = self.valid_aws_cloud_acc code, cloud_acc = self.create_cloud_account( From 52f7e8a56c696d74b24e0640e9cac01cd61aa4f0 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Mon, 16 Dec 2024 05:47:43 +0000 Subject: [PATCH 37/65] OS-8055. Updated description for token param in swagger --- rest_api/rest_api_server/handlers/v2/layouts.py | 4 ++-- rest_api/rest_api_server/handlers/v2/profiling/artifacts.py | 3 +-- rest_api/rest_api_server/handlers/v2/profiling/executors.py | 3 +-- rest_api/rest_api_server/handlers/v2/profiling/runs.py | 4 ++-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/rest_api/rest_api_server/handlers/v2/layouts.py b/rest_api/rest_api_server/handlers/v2/layouts.py index d7751e5f..6b35b46b 100644 --- a/rest_api/rest_api_server/handlers/v2/layouts.py +++ b/rest_api/rest_api_server/handlers/v2/layouts.py @@ -48,7 +48,7 @@ async def get(self, organization_id): - name: token in: query description: | - Unique token related to organization profiling token (only with + MD5 hash of organization profiling token (only with layout_type=ml_run_charts_dashboard) required: false type: string @@ -247,7 +247,7 @@ async def get(self, organization_id, layout_id): - name: token in: query description: | - Unique token related to organization profiling token + MD5 hash of organization profiling token required: false type: string responses: diff --git a/rest_api/rest_api_server/handlers/v2/profiling/artifacts.py b/rest_api/rest_api_server/handlers/v2/profiling/artifacts.py index 6cf82e70..139e20a5 100644 --- a/rest_api/rest_api_server/handlers/v2/profiling/artifacts.py +++ b/rest_api/rest_api_server/handlers/v2/profiling/artifacts.py @@ -233,8 +233,7 @@ async def get(self, organization_id, **url_params): - name: token in: query description: | - Unique token related to organization profiling token - (only with run_id) + MD5 hash of organization profiling token (only with run_id) required: false type: string responses: diff --git a/rest_api/rest_api_server/handlers/v2/profiling/executors.py b/rest_api/rest_api_server/handlers/v2/profiling/executors.py index 76391033..9dbe6138 100644 --- a/rest_api/rest_api_server/handlers/v2/profiling/executors.py +++ b/rest_api/rest_api_server/handlers/v2/profiling/executors.py @@ -51,8 +51,7 @@ async def get(self, organization_id, **url_params): - name: token in: query description: | - Unique token related to organization profiling token (only - with run_id) + MD5 hash of organization profiling token (only with run_id) required: false type: string responses: diff --git a/rest_api/rest_api_server/handlers/v2/profiling/runs.py b/rest_api/rest_api_server/handlers/v2/profiling/runs.py index 45181bb5..60f05f94 100644 --- a/rest_api/rest_api_server/handlers/v2/profiling/runs.py +++ b/rest_api/rest_api_server/handlers/v2/profiling/runs.py @@ -295,7 +295,7 @@ async def get(self, organization_id, id, **url_params): type: string - name: token in: query - description: Unique token related to organization profiling token + description: MD5 hash of organization profiling token required: false type: string responses: @@ -455,7 +455,7 @@ async def get(self, organization_id, id, **url_params): type: string - name: token in: query - description: Unique token related to organization profiling token + description: MD5 hash of organization profiling token required: false type: string responses: From 3cc7d78226cf0c52e2afb61a91a6620dd01b7969 Mon Sep 17 00:00:00 2001 From: ek-hystax <33006768+ek-hystax@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:33:44 +0400 Subject: [PATCH 38/65] OS-8063. Remove undefined exports --- ngui/ui/src/graphql/api/restapi/queries/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ngui/ui/src/graphql/api/restapi/queries/index.ts b/ngui/ui/src/graphql/api/restapi/queries/index.ts index 8140da05..ee27c5be 100644 --- a/ngui/ui/src/graphql/api/restapi/queries/index.ts +++ b/ngui/ui/src/graphql/api/restapi/queries/index.ts @@ -1,3 +1,3 @@ -import { GET_DATA_SOURCE, GET_LAYOUTS, GET_ML_EXECUTORS, GET_ML_RUN, GET_ML_RUN_BREAKDOWN } from "./restapi.queries"; +import { GET_DATA_SOURCE } from "./restapi.queries"; -export { GET_DATA_SOURCE, GET_ML_RUN, GET_ML_EXECUTORS, GET_ML_RUN_BREAKDOWN, GET_LAYOUTS }; +export { GET_DATA_SOURCE }; From 0222966efb997e10191b22add56e9d897687a8a9 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Tue, 17 Dec 2024 06:09:03 +0000 Subject: [PATCH 39/65] OS-5101. Fixed showing deleted env resources in pool api --- .../rest_api_server/controllers/expense.py | 4 +- .../tests/unittests/test_budget_expenses.py | 37 ++++++++---- .../tests/unittests/test_mytasks_api.py | 16 ++++-- .../tests/unittests/test_pools.py | 56 ++++++++++++++++++- 4 files changed, 92 insertions(+), 21 deletions(-) diff --git a/rest_api/rest_api_server/controllers/expense.py b/rest_api/rest_api_server/controllers/expense.py index c013dbbe..41b10bfd 100644 --- a/rest_api/rest_api_server/controllers/expense.py +++ b/rest_api/rest_api_server/controllers/expense.py @@ -64,6 +64,7 @@ def _get_expenses_clickhouse( '_last_seen_date': {'$gte': start_date.replace( hour=0, minute=0, second=0, microsecond=0)}, 'last_seen': {'$gte': int(start_date.timestamp())}, + 'deleted_at': 0 }, resource_fields) external_resource_table = [{ @@ -75,7 +76,8 @@ def _get_expenses_clickhouse( expenses_results = self.execute_clickhouse( query=""" SELECT - date, group_field, cloud_account_id, SUM(cost * sign) AS total_cost + date, group_field, cloud_account_id, + SUM(cost * sign) AS total_cost FROM expenses JOIN resources ON expenses.resource_id = resources._id AND expenses.cloud_account_id = resources.cloud_account_id diff --git a/rest_api/rest_api_server/tests/unittests/test_budget_expenses.py b/rest_api/rest_api_server/tests/unittests/test_budget_expenses.py index 4eb2b6e8..0056ca8a 100644 --- a/rest_api/rest_api_server/tests/unittests/test_budget_expenses.py +++ b/rest_api/rest_api_server/tests/unittests/test_budget_expenses.py @@ -72,30 +72,42 @@ def setUp(self, version='v2'): day_in_current2 = datetime(2020, 1, 18) # org pool - self.add_expense(day_in_last, 3, self.cloud_acc1['id'], self.org['pool_id']) - self.add_expense(day_in_current, 1, self.cloud_acc1['id'], self.org['pool_id']) + self.add_expense(day_in_last, 3, self.cloud_acc1['id'], + self.org['pool_id']) + self.add_expense(day_in_current, 1, self.cloud_acc1['id'], + self.org['pool_id']) # child1 - self.add_expense(day_in_last, 7, self.cloud_acc1['id'], self.child1['id']) - self.add_expense(day_in_current, 10, self.cloud_acc1['id'], self.child1['id']) + self.add_expense(day_in_last, 7, self.cloud_acc1['id'], + self.child1['id']) + self.add_expense(day_in_current, 10, self.cloud_acc1['id'], + self.child1['id']) # child1_1 - self.add_expense(day_in_current, 3, self.cloud_acc1['id'], self.child1_1['id']) + self.add_expense(day_in_current, 3, self.cloud_acc1['id'], + self.child1_1['id']) # child1_2 - self.add_expense(day_in_last, 4, self.cloud_acc1['id'], self.child1_2['id']) + self.add_expense(day_in_last, 4, self.cloud_acc1['id'], + self.child1_2['id']) # child2_1 in cloud 2 - self.add_expense(day_in_current, 5, self.cloud_acc2['id'], self.child2_1['id']) + self.add_expense(day_in_current, 5, self.cloud_acc2['id'], + self.child2_1['id']) # org2 - self.add_expense(day_in_last, 3000, self.cloud_acc_org2['id'], self.org2['pool_id']) - self.add_expense(day_in_last2, 4000, self.cloud_acc_org2['id'], self.org2['pool_id']) - self.add_expense(day_in_current, 4999, self.cloud_acc_org2['id'], self.org2['pool_id']) - self.add_expense(day_in_current2, 4111, self.cloud_acc_org2['id'], self.org2['pool_id']) + self.add_expense(day_in_last, 3000, self.cloud_acc_org2['id'], + self.org2['pool_id']) + self.add_expense(day_in_last2, 4000, self.cloud_acc_org2['id'], + self.org2['pool_id']) + self.add_expense(day_in_current, 4999, self.cloud_acc_org2['id'], + self.org2['pool_id']) + self.add_expense(day_in_current2, 4111, self.cloud_acc_org2['id'], + self.org2['pool_id']) self.today_ts = int(datetime(2020, 1, 20, 17, 34, 00).timestamp()) - self.last_month_ts = int(datetime(2019, 12, 31, 23, 59, 59).timestamp()) + self.last_month_ts = int(datetime( + 2019, 12, 31, 23, 59, 59).timestamp()) self.this_month_ts = int(datetime(2020, 1, 31, 23, 59, 59).timestamp()) self.p_assign = patch( 'rest_api.rest_api_server.controllers.pool.PoolController.' @@ -116,6 +128,7 @@ def add_expense(self, day, cost, cloud, pool, resource_id=None): '_first_seen_date': timestamp_to_day_start(timestamp), 'last_seen': timestamp, '_last_seen_date': timestamp_to_day_start(timestamp), + 'deleted_at': 0 } self.resources_collection.insert_one(resource) resource_id = resource['_id'] diff --git a/rest_api/rest_api_server/tests/unittests/test_mytasks_api.py b/rest_api/rest_api_server/tests/unittests/test_mytasks_api.py index 6ca7b8ce..849f9c52 100644 --- a/rest_api/rest_api_server/tests/unittests/test_mytasks_api.py +++ b/rest_api/rest_api_server/tests/unittests/test_mytasks_api.py @@ -79,10 +79,12 @@ def setUp(self, version='v2'): self.assertEqual(code, 201) self._mock_auth_user(self.user_id) - patch('rest_api.rest_api_server.handlers.v2.my_tasks.MyTasksAsyncHandler.' - 'check_cluster_secret', return_value=False).start() - patch('rest_api.rest_api_server.controllers.assignment.AssignmentController.' - '_authorize_action_for_pool', return_value=True).start() + patch('rest_api.rest_api_server.handlers.v2.my_tasks.' + 'MyTasksAsyncHandler.check_cluster_secret', + return_value=False).start() + patch('rest_api.rest_api_server.controllers.assignment.' + 'AssignmentController._authorize_action_for_pool', + return_value=True).start() def test_get_my_tasks_no_types(self): code, tasks = self.client.my_tasks_get(self.org_id) @@ -157,7 +159,8 @@ def add_outgoing_assignment_request(self, resource_id=None): self.assertEqual(code, 201) return request - def add_expense_records(self, pool_id=None, cost=1, date=None, owner_id=None): + def add_expense_records(self, pool_id=None, cost=1, date=None, + owner_id=None): if not date: date = utcnow() date = date.replace(hour=0, minute=0, second=0, microsecond=0) @@ -176,7 +179,8 @@ def add_expense_records(self, pool_id=None, cost=1, date=None, owner_id=None): 'first_seen': int(date.timestamp()), '_first_seen_date': date, 'last_seen': int(date.timestamp()), - '_last_seen_date': date + '_last_seen_date': date, + 'deleted_at': 0 } self.resources_collection.insert_one(resource) self.expenses.append({ diff --git a/rest_api/rest_api_server/tests/unittests/test_pools.py b/rest_api/rest_api_server/tests/unittests/test_pools.py index 30de0188..031aba40 100644 --- a/rest_api/rest_api_server/tests/unittests/test_pools.py +++ b/rest_api/rest_api_server/tests/unittests/test_pools.py @@ -4,7 +4,9 @@ from unittest.mock import patch, ANY from rest_api.rest_api_server.models.db_factory import DBFactory, DBType from rest_api.rest_api_server.models.db_base import BaseDB -from rest_api.rest_api_server.models.models import Checklist, OrganizationLimitHit +from rest_api.rest_api_server.models.models import ( + Checklist, OrganizationLimitHit +) from rest_api.rest_api_server.utils import timestamp_to_day_start from freezegun import freeze_time @@ -580,7 +582,8 @@ def test_get_organization_children(self): 'first_seen': int(day_in_month.timestamp()), 'last_seen': int(day_in_month.timestamp()), '_first_seen_date': day_in_month, - '_last_seen_date': day_in_month + '_last_seen_date': day_in_month, + 'deleted_at': 0 } self.resources_collection.insert_one(resource) self.expenses.append({ @@ -634,6 +637,55 @@ def test_get_organization_children(self): pool_id, children=True) self.assertEqual(len(pool['children']), len(children)) + def test_get_deleted_resource(self): + _, org = self.client.organization_create({'name': 'org name'}) + auth_user_1 = self.gen_id() + _, employee = self.client.employee_create( + org['id'], {'name': 'employee', + 'auth_user_id': auth_user_1}) + _, org_pool = self.client.pool_update(org['pool_id'], + {'limit': 600}) + + code, cloud_acc = self.create_cloud_account( + org['id'], { + 'name': 'my cloud_acc', + 'type': 'aws_cnr', + 'config': { + 'access_key_id': 'key', + 'secret_access_key': 'secret', + } + }, auth_user_id=auth_user_1) + code, response = self.client.rules_list(org['id']) + created_cloud_rule = response['rules'][0] + _, created_cloud_pool = self.client.pool_get( + created_cloud_rule['pool_id']) + self.set_allowed_pair(auth_user_1, created_cloud_pool['id']) + day_in_month = datetime(2020, 1, 10) + resource = { + '_id': str(uuid.uuid4()), + 'cloud_account_id': cloud_acc['id'], + 'cloud_resource_id': str(uuid.uuid4()), + 'pool_id': org_pool['id'], + 'employee_id': self.employee_1_1['id'], + 'name': 'name', + 'resource_type': 'Instance', + 'first_seen': int(day_in_month.timestamp()), + 'last_seen': int(day_in_month.timestamp()), + '_first_seen_date': day_in_month, + '_last_seen_date': day_in_month, + 'deleted_at': 1 + } + self.resources_collection.insert_one(resource) + self.expenses.append({ + 'resource_id': resource['_id'], + 'cost': 11, + 'date': day_in_month, + 'cloud_account_id': cloud_acc['id'], + 'sign': 1, + }) + code, pool = self.client.pool_get(org_pool['id'], children=True) + self.assertEqual(pool.get('cost', 0), 0) + def test_get_with_details_and_removed_children(self): get_expenses = patch( 'rest_api.rest_api_server.controllers.pool.PoolController.' From ad8ab1e148f89ff2f51b9746f36d2f14e1bb7e83 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Tue, 17 Dec 2024 06:09:56 +0000 Subject: [PATCH 40/65] OS-4548. Fixed missing organization_id field in saving_spike task --- .../bumiworker/modules/service/saving_spike_notification.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bumiworker/bumiworker/modules/service/saving_spike_notification.py b/bumiworker/bumiworker/modules/service/saving_spike_notification.py index 9fe4a33e..5e82770a 100644 --- a/bumiworker/bumiworker/modules/service/saving_spike_notification.py +++ b/bumiworker/bumiworker/modules/service/saving_spike_notification.py @@ -64,6 +64,7 @@ def _get(self): modules_data.sort(key=lambda x: x['saving'], reverse=True) task = { "object_id": self.organization_id, + "organization_id": self.organization_id, "object_type": "organization", "action": "saving_spike", "meta": {"previous_total": previous_total, From cddcc0a1e3246c684faf80a645cf45061acc1db6 Mon Sep 17 00:00:00 2001 From: ek-hystax <33006768+ek-hystax@users.noreply.github.com> Date: Tue, 17 Dec 2024 10:40:51 +0400 Subject: [PATCH 41/65] OS-4151. Re-request resources after applying force assignment rules --- ngui/ui/src/api/restapi/actionCreators.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ngui/ui/src/api/restapi/actionCreators.ts b/ngui/ui/src/api/restapi/actionCreators.ts index cf7633af..f01c81d4 100644 --- a/ngui/ui/src/api/restapi/actionCreators.ts +++ b/ngui/ui/src/api/restapi/actionCreators.ts @@ -1460,6 +1460,7 @@ export const applyAssignmentRules = (organizationId, params) => url: `${API_URL}/organizations/${organizationId}/rules_apply`, method: "POST", label: APPLY_ASSIGNMENT_RULES, + affectedRequests: [GET_CLEAN_EXPENSES], params: { pool_id: params.poolId, include_children: params.includeChildren From d9dc9293ea0ba5b37067a9baab064a51738701bb Mon Sep 17 00:00:00 2001 From: ek-hystax <33006768+ek-hystax@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:09:41 +0400 Subject: [PATCH 42/65] OS-3669. Update booking labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Add the "Until" label for current bookings to indicate the end time. 2. Replace the infinite symbol (∞) with the "Infinite" label for better readability. 3. Remove the "Duration" field for infinite upcoming bookings, as it is redundant. --- .../AvailableIn/AvailableIn.tsx | 35 ++++++++++++++++ .../CurrentBooking/AvailableIn/index.ts | 3 ++ .../CurrentBooking/CurrentBooking.tsx | 11 ++--- .../BookingTimeMeasure/BookingTimeMeasure.tsx | 29 ------------- .../BookingTimeMeasure/index.ts | 3 -- .../UpcomingBooking/Duration/Duration.tsx | 35 ++++++++++++++++ .../UpcomingBooking/Duration/index.ts | 3 ++ .../UpcomingBooking/UpcomingBooking.tsx | 41 ++++++++++++------- .../src/components/UpcomingBooking/index.ts | 3 +- ngui/ui/src/translations/en-US/app.json | 1 + ngui/ui/src/utils/constants.ts | 2 - 11 files changed, 111 insertions(+), 55 deletions(-) create mode 100644 ngui/ui/src/components/CurrentBooking/AvailableIn/AvailableIn.tsx create mode 100644 ngui/ui/src/components/CurrentBooking/AvailableIn/index.ts delete mode 100644 ngui/ui/src/components/UpcomingBooking/BookingTimeMeasure/BookingTimeMeasure.tsx delete mode 100644 ngui/ui/src/components/UpcomingBooking/BookingTimeMeasure/index.ts create mode 100644 ngui/ui/src/components/UpcomingBooking/Duration/Duration.tsx create mode 100644 ngui/ui/src/components/UpcomingBooking/Duration/index.ts diff --git a/ngui/ui/src/components/CurrentBooking/AvailableIn/AvailableIn.tsx b/ngui/ui/src/components/CurrentBooking/AvailableIn/AvailableIn.tsx new file mode 100644 index 00000000..8ed9c078 --- /dev/null +++ b/ngui/ui/src/components/CurrentBooking/AvailableIn/AvailableIn.tsx @@ -0,0 +1,35 @@ +import KeyValueLabel from "components/KeyValueLabel/KeyValueLabel"; +import { useFormatIntervalDuration } from "hooks/useFormatIntervalDuration"; +import { INTERVAL_DURATION_VALUE_TYPES } from "utils/datetime"; + +type AvailableInProps = { + remained: { + weeks: number; + days: number; + hours: number; + minutes: number; + seconds: number; + milliseconds: number; + }; +}; + +const AvailableIn = ({ remained }: AvailableInProps) => { + const formatInterval = useFormatIntervalDuration(); + + return ( + + ); +}; + +export default AvailableIn; diff --git a/ngui/ui/src/components/CurrentBooking/AvailableIn/index.ts b/ngui/ui/src/components/CurrentBooking/AvailableIn/index.ts new file mode 100644 index 00000000..ca485c37 --- /dev/null +++ b/ngui/ui/src/components/CurrentBooking/AvailableIn/index.ts @@ -0,0 +1,3 @@ +import AvailableIn from "./AvailableIn"; + +export default AvailableIn; diff --git a/ngui/ui/src/components/CurrentBooking/CurrentBooking.tsx b/ngui/ui/src/components/CurrentBooking/CurrentBooking.tsx index 8c70b741..3f1dd05a 100644 --- a/ngui/ui/src/components/CurrentBooking/CurrentBooking.tsx +++ b/ngui/ui/src/components/CurrentBooking/CurrentBooking.tsx @@ -1,16 +1,17 @@ +import { FormattedMessage } from "react-intl"; import JiraIssuesAttachments from "components/JiraIssuesAttachments"; import KeyValueLabel from "components/KeyValueLabel/KeyValueLabel"; -import { BookingTimeMeasure, getBookingTimeMeasuresDefinition } from "components/UpcomingBooking"; -import { INFINITY_SIGN } from "utils/constants"; +import { getBookingTimeMeasuresDefinition } from "components/UpcomingBooking"; +import AvailableIn from "./AvailableIn"; -// TODO: generalize Current and Upcoming bookings const CurrentBooking = ({ employeeName, acquiredSince, releasedAt, jiraIssues = [] }) => { - const { remained } = getBookingTimeMeasuresDefinition({ releasedAt, acquiredSince }); + const { remained, bookedUntil } = getBookingTimeMeasuresDefinition({ releasedAt, acquiredSince }); return ( <> - {remained !== INFINITY_SIGN && } + : bookedUntil} /> + {remained !== Infinity && } {jiraIssues.length > 0 && } ); diff --git a/ngui/ui/src/components/UpcomingBooking/BookingTimeMeasure/BookingTimeMeasure.tsx b/ngui/ui/src/components/UpcomingBooking/BookingTimeMeasure/BookingTimeMeasure.tsx deleted file mode 100644 index 25a16c1b..00000000 --- a/ngui/ui/src/components/UpcomingBooking/BookingTimeMeasure/BookingTimeMeasure.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import KeyValueLabel from "components/KeyValueLabel/KeyValueLabel"; -import { useFormatIntervalDuration } from "hooks/useFormatIntervalDuration"; -import { INFINITY_SIGN } from "utils/constants"; -import { INTERVAL_DURATION_VALUE_TYPES } from "utils/datetime"; - -const BookingTimeMeasure = ({ messageId, measure }) => { - const formatInterval = useFormatIntervalDuration(); - - return ( - - ); -}; - -export default BookingTimeMeasure; diff --git a/ngui/ui/src/components/UpcomingBooking/BookingTimeMeasure/index.ts b/ngui/ui/src/components/UpcomingBooking/BookingTimeMeasure/index.ts deleted file mode 100644 index e4faeda8..00000000 --- a/ngui/ui/src/components/UpcomingBooking/BookingTimeMeasure/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import BookingTimeMeasure from "./BookingTimeMeasure"; - -export default BookingTimeMeasure; diff --git a/ngui/ui/src/components/UpcomingBooking/Duration/Duration.tsx b/ngui/ui/src/components/UpcomingBooking/Duration/Duration.tsx new file mode 100644 index 00000000..9d2435a3 --- /dev/null +++ b/ngui/ui/src/components/UpcomingBooking/Duration/Duration.tsx @@ -0,0 +1,35 @@ +import KeyValueLabel from "components/KeyValueLabel/KeyValueLabel"; +import { useFormatIntervalDuration } from "hooks/useFormatIntervalDuration"; +import { INTERVAL_DURATION_VALUE_TYPES } from "utils/datetime"; + +type DurationProps = { + duration: { + weeks: number; + days: number; + hours: number; + minutes: number; + seconds: number; + milliseconds: number; + }; +}; + +const Duration = ({ duration }: DurationProps) => { + const formatInterval = useFormatIntervalDuration(); + + return ( + + ); +}; + +export default Duration; diff --git a/ngui/ui/src/components/UpcomingBooking/Duration/index.ts b/ngui/ui/src/components/UpcomingBooking/Duration/index.ts new file mode 100644 index 00000000..3a28ffcc --- /dev/null +++ b/ngui/ui/src/components/UpcomingBooking/Duration/index.ts @@ -0,0 +1,3 @@ +import Duration from "./Duration"; + +export default Duration; diff --git a/ngui/ui/src/components/UpcomingBooking/UpcomingBooking.tsx b/ngui/ui/src/components/UpcomingBooking/UpcomingBooking.tsx index 952c9a05..3289c546 100644 --- a/ngui/ui/src/components/UpcomingBooking/UpcomingBooking.tsx +++ b/ngui/ui/src/components/UpcomingBooking/UpcomingBooking.tsx @@ -1,17 +1,24 @@ +import { FormattedMessage } from "react-intl"; import KeyValueLabel from "components/KeyValueLabel/KeyValueLabel"; -import { INFINITY_SIGN } from "utils/constants"; import { EN_FULL_FORMAT, format, secondsToMilliseconds, intervalToDuration } from "utils/datetime"; -import BookingTimeMeasure from "./BookingTimeMeasure"; +import Duration from "./Duration"; -const getInfiniteBookingTimeMeasuresDefinition = (acquiredSince) => ({ - duration: INFINITY_SIGN, - remained: INFINITY_SIGN, - bookedUntil: INFINITY_SIGN, - // TODO: generalize getBookedSince in InfiniteBookingTimeMeasures and FiniteBookingTimeMeasures - bookedSince: format(secondsToMilliseconds(acquiredSince), EN_FULL_FORMAT) -}); +type UpcomingBookingProps = { + employeeName: string; + acquiredSince: number; + releasedAt: number; +}; + +const getInfiniteBookingTimeMeasuresDefinition = (acquiredSince: number) => + ({ + duration: Infinity, + remained: Infinity, + bookedUntil: Infinity, + // TODO: generalize getBookedSince in InfiniteBookingTimeMeasures and FiniteBookingTimeMeasures + bookedSince: format(secondsToMilliseconds(acquiredSince), EN_FULL_FORMAT) + }) as const; -const getFiniteBookingTimeMeasuresDefinition = (acquiredSince, releasedAt) => { +const getFiniteBookingTimeMeasuresDefinition = (acquiredSince: number, releasedAt: number) => { const acquiredSinceInMilliseconds = secondsToMilliseconds(acquiredSince); const releasedAtInMilliseconds = secondsToMilliseconds(releasedAt); @@ -29,7 +36,13 @@ const getFiniteBookingTimeMeasuresDefinition = (acquiredSince, releasedAt) => { }; }; -export const getBookingTimeMeasuresDefinition = ({ releasedAt, acquiredSince }) => { +export const getBookingTimeMeasuresDefinition = ({ + releasedAt, + acquiredSince +}: { + releasedAt: number; + acquiredSince: number; +}) => { const timeMeasuresDefinition = releasedAt === 0 ? getInfiniteBookingTimeMeasuresDefinition(acquiredSince) @@ -37,15 +50,15 @@ export const getBookingTimeMeasuresDefinition = ({ releasedAt, acquiredSince }) return timeMeasuresDefinition; }; -const UpcomingBooking = ({ employeeName, acquiredSince, releasedAt }) => { +const UpcomingBooking = ({ employeeName, acquiredSince, releasedAt }: UpcomingBookingProps) => { const { bookedSince, bookedUntil, duration } = getBookingTimeMeasuresDefinition({ releasedAt, acquiredSince }); return ( <> - - + : bookedUntil} /> + {bookedUntil !== Infinity && } ); }; diff --git a/ngui/ui/src/components/UpcomingBooking/index.ts b/ngui/ui/src/components/UpcomingBooking/index.ts index b1dd46f2..1db1d6b1 100644 --- a/ngui/ui/src/components/UpcomingBooking/index.ts +++ b/ngui/ui/src/components/UpcomingBooking/index.ts @@ -1,5 +1,4 @@ -import BookingTimeMeasure from "./BookingTimeMeasure"; import UpcomingBooking, { getBookingTimeMeasuresDefinition } from "./UpcomingBooking"; -export { BookingTimeMeasure, getBookingTimeMeasuresDefinition }; +export { getBookingTimeMeasuresDefinition }; export default UpcomingBooking; diff --git a/ngui/ui/src/translations/en-US/app.json b/ngui/ui/src/translations/en-US/app.json index 22b2b47f..60e5ce9d 100644 --- a/ngui/ui/src/translations/en-US/app.json +++ b/ngui/ui/src/translations/en-US/app.json @@ -903,6 +903,7 @@ "incorrectDateFormat": "Incorrect date format", "independentCompute": "Independent compute", "independentComputeTooltip": "Region is recommended for running new workloads that are mostly independent from the existing ones", + "infinite": "Infinite", "infinity": "Infinity", "info": "Info", "infrastructure": "Infrastructure", diff --git a/ngui/ui/src/utils/constants.ts b/ngui/ui/src/utils/constants.ts index 1f27179b..0851c784 100644 --- a/ngui/ui/src/utils/constants.ts +++ b/ngui/ui/src/utils/constants.ts @@ -515,8 +515,6 @@ export const ALERT_SEVERITY = Object.freeze({ WARNING: "warning" }); -export const INFINITY_SIGN = "∞"; - export const ENVIRONMENT_SOFTWARE_FIELD = "Software "; export const ENVIRONMENT_JIRA_TICKETS_FIELD = "Jira tickets "; export const ENVIRONMENT_TOUR_IDS_BY_DYNAMIC_FIELDS = Object.freeze({ From 802783a034dc9cb1bbeed8a1d7fe864354d17399 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Thu, 19 Dec 2024 05:08:34 +0000 Subject: [PATCH 43/65] OS-8060. Add public_ip to user_template.yml --- optscale-deploy/overlay/user_template.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/optscale-deploy/overlay/user_template.yml b/optscale-deploy/overlay/user_template.yml index bd0edb21..c9edabf6 100644 --- a/optscale-deploy/overlay/user_template.yml +++ b/optscale-deploy/overlay/user_template.yml @@ -108,3 +108,8 @@ grafana: env: htpasswd_user: userforgrafana htpasswd_pass: passwordforgrafana + +# Public ip of OptScale VM used in external integrations like emails, +# google calendar etc. Defaults to internal ip, change it if internal ip +# is not accessible in your network +public_ip: From 91c2e96b4d0ebe8cbbb9d4d420e012e51f95d0f4 Mon Sep 17 00:00:00 2001 From: nk-hystax <128669932+nk-hystax@users.noreply.github.com> Date: Thu, 19 Dec 2024 05:09:20 +0000 Subject: [PATCH 44/65] OS-2570. Improved validation for env_properties_collector api --- .../v2/environment_resources_properties.py | 37 +++++++++++++++---- .../test_environment_resources_properties.py | 16 ++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/rest_api/rest_api_server/handlers/v2/environment_resources_properties.py b/rest_api/rest_api_server/handlers/v2/environment_resources_properties.py index 1acb682d..ee3e184f 100644 --- a/rest_api/rest_api_server/handlers/v2/environment_resources_properties.py +++ b/rest_api/rest_api_server/handlers/v2/environment_resources_properties.py @@ -7,7 +7,8 @@ from rest_api.rest_api_server.handlers.v1.base import BaseAuthHandler from rest_api.rest_api_server.handlers.v2.base import BaseHandler from rest_api.rest_api_server.utils import (run_task, ModelEncoder, - check_int_attribute, object_to_xlsx) + check_int_attribute, object_to_xlsx, + check_string_attribute) from tools.optscale_exceptions.http_exc import OptHTTPError from tools.optscale_exceptions.common_exc import (NotFoundException, WrongArgumentsException) @@ -227,6 +228,19 @@ class EnvironmentResourcePropertiesCollectorAsyncItemHandler(BaseHandler): def _get_controller_class(self): return CloudResourceAsyncController + @staticmethod + def _validate_parameters(params): + try: + if not isinstance(params, dict): + raise WrongArgumentsException(Err.OE0344, ['Properties']) + if not params: + return + for k, v in params.items(): + check_string_attribute('Property', k) + check_string_attribute(k, v, allow_empty=True) + except WrongArgumentsException as ex: + raise OptHTTPError.from_opt_exception(400, ex) + async def post(self, id): """ --- @@ -235,30 +249,36 @@ async def post(self, id): of an environment resource Required permission: none tags: [cloud_resources] - summary: Registers information about the properties of an environment resource + summary: | + Registers information about the properties of an environment + resource parameters: - name: id in: path - description: Cloud resource ID + description: resource ID required: true type: string - in: body name: body description: new values of resource properties required: true - type: object - example: - version: 1.23.4 - status: success - some_property: some_value + schema: + type: object + example: + version: 1.23.4 + status: success + some_property: some_value responses: 204: description: No content 400: description: | Wrong arguments: + - OE0214: Param should be a string + - OE0215: Param should contain 1-255 characters - OE0233: Incorrect body received - OE0344: Properties should be a dictionary + - OE0416: Param should not contain only whitespaces - OE0480: Resource is not shareable 404: description: | @@ -266,5 +286,6 @@ async def post(self, id): - OE0002: Resource not found """ props = self._request_body() + self._validate_parameters(props) await run_task(self.controller.edit, id, env_properties=props) self.set_status(204) diff --git a/rest_api/rest_api_server/tests/unittests/test_environment_resources_properties.py b/rest_api/rest_api_server/tests/unittests/test_environment_resources_properties.py index 0f49de33..417194d9 100644 --- a/rest_api/rest_api_server/tests/unittests/test_environment_resources_properties.py +++ b/rest_api/rest_api_server/tests/unittests/test_environment_resources_properties.py @@ -123,6 +123,22 @@ def test_send_properties(self): result_props = resp.get('env_properties', {}) self.assertNotIn('field1', result_props.keys()) + def test_send_invalid_params(self): + code, resp = self.client.env_properties_send( + self.env_resource_id, {'test': ['test']}) + self.assertEqual(code, 400) + self.assertEqual(resp['error']['error_code'], 'OE0214') + + code, resp = self.client.env_properties_send( + self.env_resource_id, {'': 'test'}) + self.assertEqual(code, 400) + self.assertEqual(resp['error']['error_code'], 'OE0215') + + code, resp = self.client.env_properties_send( + self.env_resource_id, {' ': 'test'}) + self.assertEqual(code, 400) + self.assertEqual(resp['error']['error_code'], 'OE0416') + def test_send_properties_slack_message(self): p_send_msg = patch('rest_api.rest_api_server.controllers.base.' 'BaseController.publish_activities_task').start() From beb534b837d57e932f2c47b85ac8e33d765c2e07 Mon Sep 17 00:00:00 2001 From: ek-hystax <33006768+ek-hystax@users.noreply.github.com> Date: Thu, 19 Dec 2024 09:28:02 +0400 Subject: [PATCH 45/65] OS-8062. Fix forms validation * AddInstancesToScheduleForm/FormElements/InstancesField * CreateS3DuplicateFinderCheckForm/FormElements/BucketsField --- .../FormElements/InstancesField.tsx | 4 +-- .../FormElements/BucketsField.tsx | 30 +++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/ngui/ui/src/components/forms/AddInstancesToScheduleForm/FormElements/InstancesField.tsx b/ngui/ui/src/components/forms/AddInstancesToScheduleForm/FormElements/InstancesField.tsx index c0805f5b..e365ccf4 100644 --- a/ngui/ui/src/components/forms/AddInstancesToScheduleForm/FormElements/InstancesField.tsx +++ b/ngui/ui/src/components/forms/AddInstancesToScheduleForm/FormElements/InstancesField.tsx @@ -1,7 +1,7 @@ import { useMemo } from "react"; import { FormControl, FormHelperText } from "@mui/material"; import { Controller, useFormContext } from "react-hook-form"; -import { FormattedMessage, useIntl } from "react-intl"; +import { useIntl } from "react-intl"; import FormContentDescription from "components/FormContentDescription"; import Table from "components/Table"; import TableLoader from "components/TableLoader"; @@ -107,7 +107,7 @@ const InstancesField = ({ instances, instancesCountLimit, isLoading = false }) = rules={{ validate: { atLeastOneSelected: (value) => - isEmptyObject(value) ? : true + isEmptyObject(value) ? intl.formatMessage({ id: "atLeastOneInstanceMustBeSelected" }) : true } }} render={({ field: { value, onChange } }) => { diff --git a/ngui/ui/src/components/forms/CreateS3DuplicateFinderCheckForm/FormElements/BucketsField.tsx b/ngui/ui/src/components/forms/CreateS3DuplicateFinderCheckForm/FormElements/BucketsField.tsx index 0da7620c..82f0fac7 100644 --- a/ngui/ui/src/components/forms/CreateS3DuplicateFinderCheckForm/FormElements/BucketsField.tsx +++ b/ngui/ui/src/components/forms/CreateS3DuplicateFinderCheckForm/FormElements/BucketsField.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo } from "react"; import { FormControl, FormHelperText } from "@mui/material"; import Typography from "@mui/material/Typography"; import { Controller, useFormContext } from "react-hook-form"; -import { FormattedMessage } from "react-intl"; +import { FormattedMessage, useIntl } from "react-intl"; import CloudResourceId from "components/CloudResourceId"; import Table from "components/Table"; import TableLoader from "components/TableLoader"; @@ -77,6 +77,8 @@ const TableField = ({ buckets, value, dataSources, onChange, errors }) => { }; const BucketsField = ({ buckets, dataSources, isLoading }) => { + const intl = useIntl(); + const { formState: { errors }, watch, @@ -101,19 +103,23 @@ const BucketsField = ({ buckets, dataSources, isLoading }) => { rules={{ validate: { atLeastOneSelected: (value) => - isEmptyObject(value) ? : true, + isEmptyObject(value) + ? intl.formatMessage({ + id: "atLeastOneBucketMustBeSelected" + }) + : true, maxBuckets: (value) => { const bucketsCount = Object.keys(value).length; - return bucketsCount > MAX_SELECTED_BUCKETS ? ( - - ) : ( - true - ); + return bucketsCount > MAX_SELECTED_BUCKETS + ? intl.formatMessage( + { + id: "maxNBucketsCanBeSelected" + }, + { + value: MAX_SELECTED_BUCKETS + } + ) + : true; } } }} From cbbfec7f96ec150d5a00f26bf36fe909f28bf74d Mon Sep 17 00:00:00 2001 From: ek-hystax <33006768+ek-hystax@users.noreply.github.com> Date: Thu, 19 Dec 2024 09:29:13 +0400 Subject: [PATCH 46/65] OS-8064. Fix unique keys warning on resource details page --- ngui/ui/src/components/ResourceDetails/ResourceDetails.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngui/ui/src/components/ResourceDetails/ResourceDetails.tsx b/ngui/ui/src/components/ResourceDetails/ResourceDetails.tsx index 1e72f57b..03deada8 100644 --- a/ngui/ui/src/components/ResourceDetails/ResourceDetails.tsx +++ b/ngui/ui/src/components/ResourceDetails/ResourceDetails.tsx @@ -16,7 +16,7 @@ import { MetadataNodes } from "utils/metadata"; import { isEmpty } from "utils/objects"; import CollapsableTableCell from "../CollapsableTableCell"; -const renderKeyValueLabels = (options) => options.map((opt) => ); +const renderKeyValueLabels = (options) => options.map((opt) => ); const getIdLabelDefinition = ({ cloudResourceIdentifier, isActive }) => ({ value: ( From 55226abbacbc6bcfc658ccc733c41da41010cf49 Mon Sep 17 00:00:00 2001 From: ek-hystax <33006768+ek-hystax@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:00:12 +0400 Subject: [PATCH 47/65] feature/app_initialization --- auth/auth_server/controllers/signin.py | 7 + auth/auth_server/handlers/v2/signin.py | 1 - ngui/server/.env.sample | 7 +- ngui/server/api/auth/client.ts | 77 ++++ ngui/server/api/restapi/client.ts | 207 +++++++++- ngui/server/api/slacker/client.ts | 2 +- ngui/server/codegen.ts | 4 + .../graphql/resolvers/auth.generated.ts | 205 +++++++++ ngui/server/graphql/resolvers/auth.ts | 33 ++ .../graphql/resolvers/restapi.generated.ts | 242 ++++++++++- ngui/server/graphql/resolvers/restapi.ts | 85 ++++ ngui/server/graphql/schemas/auth.graphql | 36 ++ ngui/server/graphql/schemas/restapi.graphql | 92 ++++- ngui/server/server.ts | 9 +- ngui/ui/src/api/auth/actionCreators.ts | 23 +- ngui/ui/src/api/auth/actionTypes.ts | 1 - ngui/ui/src/api/auth/handlers.ts | 11 - ngui/ui/src/api/auth/index.ts | 2 - ngui/ui/src/api/auth/reducer.ts | 8 +- ngui/ui/src/api/index.ts | 30 -- ngui/ui/src/api/restapi/actionCreators.ts | 171 +------- ngui/ui/src/api/restapi/actionTypes.ts | 28 -- ngui/ui/src/api/restapi/handlers.ts | 31 -- ngui/ui/src/api/restapi/index.ts | 28 -- ngui/ui/src/api/restapi/reducer.ts | 72 ---- .../AcceptInvitations.test.tsx | 14 - .../AcceptInvitations/AcceptInvitations.tsx | 67 --- .../src/components/AcceptInvitations/index.ts | 3 - .../ActivityListener/ActivityListener.ts | 8 +- .../ApolloProvider/ApolloProvider.tsx | 11 +- ngui/ui/src/components/App/App.tsx | 10 +- .../CloudAccountDetails.tsx | 11 +- .../CloudAccountsOverviewMocked.tsx | 12 +- .../CloudAccountsTable/CloudAccountsTable.tsx | 4 +- .../ui/src/components/Dashboard/Dashboard.tsx | 9 +- .../ChildrenList/ChildrenList.tsx | 13 +- .../Properties/AzureProperties.tsx | 30 +- .../EmailVerification/EmailVerification.tsx | 38 +- .../GenerateLiveDemo/GenerateLiveDemo.tsx | 8 +- .../GoogleAuthButton/GoogleAuthButton.tsx | 20 +- ngui/ui/src/components/MainMenu/MainMenu.tsx | 6 +- .../MicrosoftSignInButton.tsx | 16 +- .../RecommendationListItem.test.tsx | 14 - .../RecommendationListItem.tsx | 14 - .../RecommendationListItem copy/index.ts | 3 - .../RecommendationListItem.test.tsx | 14 - .../RecommendationListItem.tsx | 14 - .../RecommendationListItem/index.ts | 3 - ...commendationListItemResourceLabel.test.tsx | 14 - .../RecommendationListItemResourceLabel.tsx | 30 -- .../index.ts | 3 - ...commendationListItemResourceLabel.test.tsx | 14 - .../RecommendationListItemResourceLabel.tsx | 30 -- .../index.ts | 3 - .../Layer/index.ts | 3 - .../Layer/useRenderWeekendsHighlightLayer.ts | 64 --- ngui/ui/src/components/Mode/Mode.tsx | 13 +- .../components/ModeWrapper/ModeWrapper.tsx | 6 +- .../OrganizationConstraintsTable.tsx | 11 +- .../OrganizationCurrency.tsx | 11 +- .../OrganizationLabel/OrganizationLabel.tsx | 43 +- .../OrganizationSelector.tsx | 6 +- .../OrganizationsOverviewTable.tsx | 16 +- .../PasswordRecovery/PasswordRecovery.tsx | 51 ++- .../PendingInvitationsAlert.tsx | 6 +- .../ui/src/components/PoolLabel/PoolLabel.tsx | 29 +- .../SelectedCloudAccounts.tsx | 9 +- .../RecommendationListItemResourceLabel.tsx | 7 +- .../ResourceLocationCell.tsx | 7 +- .../ResourcesPerspectives.tsx | 2 +- .../TopAlertWrapper/TopAlertWrapper.tsx | 36 +- .../TopResourcesExpensesCard.tsx | 2 +- .../WrongInvitationEmailAlert.styles.ts} | 0 .../WrongInvitationEmailAlert.tsx | 2 +- .../FormElements/DataSourcesField.tsx | 7 +- .../LoginForm/FormElements/FormButtons.tsx | 3 +- .../components/forms/LoginForm/LoginForm.tsx | 4 +- .../src/components/forms/LoginForm/types.ts | 1 + .../RegistrationForm/RegistrationForm.tsx | 3 +- .../forms/RegistrationForm/types.ts | 1 + .../AcceptInvitationsContainer.tsx | 43 -- .../AcceptInvitationsContainer/index.ts | 3 - .../AuthorizationContainer.tsx | 182 +++++--- .../BookEnvironmentFormContainer.tsx | 7 +- .../ConfirmEmailVerificationCodeContainer.tsx | 29 +- .../ConfirmVerificationCodeContainer.tsx | 2 +- .../ConnectCloudAccountContainer.tsx | 49 ++- .../CoreDataContainer/CoreDataContainer.tsx | 136 ++++++ .../src/containers/CoreDataContainer/index.ts | 3 + .../CreateAssignmentRuleFormContainer.tsx | 10 +- .../CreateNewPasswordContainer.tsx | 25 +- .../CreateOrganizationContainer.tsx | 22 +- ...ateResourceAssignmentRuleFormContainer.tsx | 17 +- .../CreateResourcePerspectiveContainer.tsx | 43 +- .../DeleteEmployeeContainer.tsx | 7 +- .../DeleteOrganizationContainer.tsx | 29 +- .../DeleteResourcePerspectiveContainer.tsx | 37 +- .../DisconnectCloudAccountContainer.tsx | 43 +- .../EditAssignmentRuleFormContainer.tsx | 16 +- .../EditOrganizationCurrencyFormContainer.tsx | 30 +- .../EditOrganizationFormContainer.tsx | 28 +- .../GenerateLiveDemoContainer.tsx | 31 +- .../GetCloudAccountsContainer.tsx | 9 +- .../InitializeContainer/AcceptInvitations.tsx | 88 ++++ .../InitializeContainer.tsx | 112 +++++ .../InitializeContainer/SetupOrganization.tsx | 33 ++ .../containers/InitializeContainer/index.ts | 3 + .../redux/actionCreators.ts | 6 + .../InitializeContainer/redux/actionTypes.ts | 1 + .../InitializeContainer/redux/index.ts | 6 + .../InitializeContainer/redux/reducer.ts | 14 + .../IntegrationJiraContainer.tsx | 6 +- .../IntegrationsSlackContainer.tsx | 7 +- .../InvitationActionsContainer.tsx | 35 +- .../InvitationsContainer.tsx | 32 +- .../InvitedContainer/InvitedContainer.tsx | 15 +- .../MainLayoutContainer.tsx | 122 ------ .../containers/MainLayoutContainer/index.ts | 3 - .../MlRunsetTemplateCreateFormContainer.tsx | 7 +- .../MlRunsetTemplateEditContainer.tsx | 7 +- .../ModeContainer/ModeContainer.tsx | 40 +- .../OrganizationSelectorContainer.tsx | 55 +-- .../OrganizationThemeSettingsContainer.tsx | 34 +- .../ProfileMenuContainer.test.tsx | 4 +- .../ProfileMenuContainer.tsx | 7 +- .../RenameDataSourceContainer.tsx | 14 +- .../ResourcesContainer/ResourcesContainer.tsx | 2 +- .../SshSettingsContainer.tsx | 5 +- .../UpdateDataSourceCredentialsContainer.tsx | 14 +- ...UserEmailNotificationSettingsContainer.tsx | 7 +- .../graphql/api/auth/queries/auth.queries.ts | 49 +++ ngui/ui/src/graphql/api/auth/queries/index.ts | 3 + .../src/graphql/api/restapi/queries/index.ts | 28 +- .../api/restapi/queries/restapi.queries.ts | 391 ++++++++++++++---- ngui/ui/src/hooks/coreData/index.ts | 21 + .../src/hooks/coreData/useAllDataSources.ts | 16 + .../src/hooks/coreData/useCurrentEmployee.ts | 16 + .../src/hooks/coreData/useGetOptscaleMode.ts | 18 + ngui/ui/src/hooks/coreData/useInvitations.ts | 16 + .../coreData/useOrganizationAllowedActions.ts | 20 + .../hooks/coreData/useOrganizationFeatures.ts | 16 + .../useOrganizationPerspectives.ts | 25 +- .../coreData/useOrganizationThemeSettings.ts | 16 + .../ui/src/hooks/coreData/useOrganizations.ts | 10 + ngui/ui/src/hooks/useAllowedActions.ts | 23 +- ngui/ui/src/hooks/useAuthorization.ts | 62 --- ngui/ui/src/hooks/useAwsDataSources.ts | 7 +- ngui/ui/src/hooks/useConstraints.ts | 4 +- .../hooks/useCustomOrganizationWeekends.ts | 2 +- ngui/ui/src/hooks/useFetchAndDownload.ts | 7 +- ngui/ui/src/hooks/useGetToken.ts | 16 + ngui/ui/src/hooks/useIsFeatureEnabled.ts | 2 +- .../src/hooks/useIsNebiusConnectionEnabled.ts | 2 +- ngui/ui/src/hooks/useIsOptScaleModeEnabled.ts | 8 +- ngui/ui/src/hooks/useNewAuthorization.ts | 237 ----------- ngui/ui/src/hooks/useOptScaleMode.ts | 13 - ngui/ui/src/hooks/useOrganizationFeatures.ts | 11 - ...useOrganizationIdQueryParameterListener.ts | 26 -- ngui/ui/src/hooks/useOrganizationInfo.ts | 50 +-- .../src/hooks/useOrganizationThemeSettings.ts | 11 - .../hooks/useResourceConstraintPermissions.ts | 7 +- ngui/ui/src/hooks/useResourceFilters.ts | 5 +- .../useShouldRenderConnectCloudAccountMock.ts | 14 +- ngui/ui/src/hooks/useSignOut.ts | 7 +- ngui/ui/src/hooks/useThemeSettingsOptions.ts | 2 +- ngui/ui/src/hooks/useUpdateScope.ts | 20 + ngui/ui/src/layouts/BaseLayout/BaseLayout.tsx | 11 +- ngui/ui/src/middleware/api.ts | 4 +- .../AcceptInvitations/AcceptInvitations.tsx | 5 - ngui/ui/src/pages/AcceptInvitations/index.ts | 3 - ngui/ui/src/pages/Initialize/Initialize.tsx | 5 + ngui/ui/src/pages/Initialize/index.ts | 3 + .../pages/Recommendations/Recommendations.tsx | 13 +- ngui/ui/src/pages/Resources/Resources.tsx | 2 +- .../src/pages/RiSpCoverage/RiSpCoverage.tsx | 9 +- ngui/ui/src/reducers.ts | 3 + ngui/ui/src/services/DataSourcesService.ts | 34 +- ngui/ui/src/services/InvitationsService.ts | 32 -- .../services/OrganizationOptionsService.ts | 70 ---- ngui/ui/src/services/OrganizationsService.ts | 47 --- ngui/ui/src/services/ResetPasswordServices.ts | 90 ++-- ngui/ui/src/services/VerifyEmailService.ts | 27 +- .../Components/CloudExpensesChart.stories.tsx | 4 +- .../Components/ClusterTypesTable.stories.tsx | 20 +- .../src/stories/Pages/Dashboard.stories.tsx | 74 +--- .../tests/utils/mockStore/MockState.test.ts | 101 +---- ngui/ui/src/translations/en-US/app.json | 1 + ngui/ui/src/urls.ts | 5 +- ngui/ui/src/utils/MockState.ts | 42 +- ngui/ui/src/utils/columns/resource.tsx | 7 +- ngui/ui/src/utils/columns/userLocation.tsx | 15 +- ngui/ui/src/utils/constants.ts | 5 + .../dataSources/summarizeTenantChildren.ts | 6 +- ngui/ui/src/utils/network.ts | 13 - .../utils/routes/acceptInvitationsRoute.ts | 12 - ngui/ui/src/utils/routes/index.ts | 4 +- ngui/ui/src/utils/routes/initializeRoute.ts | 12 + .../controllers/cloud_account.py | 2 +- .../handlers/v2/cloud_account.py | 2 +- .../tests/unittests/test_cloud_accounts.py | 4 +- 200 files changed, 2975 insertions(+), 2564 deletions(-) create mode 100644 ngui/server/api/auth/client.ts create mode 100644 ngui/server/graphql/resolvers/auth.generated.ts create mode 100644 ngui/server/graphql/resolvers/auth.ts create mode 100644 ngui/server/graphql/schemas/auth.graphql delete mode 100644 ngui/ui/src/components/AcceptInvitations/AcceptInvitations.test.tsx delete mode 100644 ngui/ui/src/components/AcceptInvitations/AcceptInvitations.tsx delete mode 100644 ngui/ui/src/components/AcceptInvitations/index.ts delete mode 100644 ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem copy/RecommendationListItem.test.tsx delete mode 100644 ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem copy/RecommendationListItem.tsx delete mode 100644 ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem copy/index.ts delete mode 100644 ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem/RecommendationListItem.test.tsx delete mode 100644 ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem/RecommendationListItem.tsx delete mode 100644 ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem/index.ts delete mode 100644 ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel copy/RecommendationListItemResourceLabel.test.tsx delete mode 100644 ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel copy/RecommendationListItemResourceLabel.tsx delete mode 100644 ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel copy/index.ts delete mode 100644 ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel/RecommendationListItemResourceLabel.test.tsx delete mode 100644 ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel/RecommendationListItemResourceLabel.tsx delete mode 100644 ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel/index.ts delete mode 100644 ngui/ui/src/components/MlRunHistoryChart/ResourceCountBreakdown/ResourceCountBreakdownLineChart/Layer/index.ts delete mode 100644 ngui/ui/src/components/MlRunHistoryChart/ResourceCountBreakdown/ResourceCountBreakdownLineChart/Layer/useRenderWeekendsHighlightLayer.ts rename ngui/ui/src/components/{AcceptInvitations/AcceptInvitations.styles.ts => WrongInvitationEmailAlert/WrongInvitationEmailAlert.styles.ts} (100%) delete mode 100644 ngui/ui/src/containers/AcceptInvitationsContainer/AcceptInvitationsContainer.tsx delete mode 100644 ngui/ui/src/containers/AcceptInvitationsContainer/index.ts create mode 100644 ngui/ui/src/containers/CoreDataContainer/CoreDataContainer.tsx create mode 100644 ngui/ui/src/containers/CoreDataContainer/index.ts create mode 100644 ngui/ui/src/containers/InitializeContainer/AcceptInvitations.tsx create mode 100644 ngui/ui/src/containers/InitializeContainer/InitializeContainer.tsx create mode 100644 ngui/ui/src/containers/InitializeContainer/SetupOrganization.tsx create mode 100644 ngui/ui/src/containers/InitializeContainer/index.ts create mode 100644 ngui/ui/src/containers/InitializeContainer/redux/actionCreators.ts create mode 100644 ngui/ui/src/containers/InitializeContainer/redux/actionTypes.ts create mode 100644 ngui/ui/src/containers/InitializeContainer/redux/index.ts create mode 100644 ngui/ui/src/containers/InitializeContainer/redux/reducer.ts delete mode 100644 ngui/ui/src/containers/MainLayoutContainer/MainLayoutContainer.tsx delete mode 100644 ngui/ui/src/containers/MainLayoutContainer/index.ts create mode 100644 ngui/ui/src/graphql/api/auth/queries/auth.queries.ts create mode 100644 ngui/ui/src/graphql/api/auth/queries/index.ts create mode 100644 ngui/ui/src/hooks/coreData/index.ts create mode 100644 ngui/ui/src/hooks/coreData/useAllDataSources.ts create mode 100644 ngui/ui/src/hooks/coreData/useCurrentEmployee.ts create mode 100644 ngui/ui/src/hooks/coreData/useGetOptscaleMode.ts create mode 100644 ngui/ui/src/hooks/coreData/useInvitations.ts create mode 100644 ngui/ui/src/hooks/coreData/useOrganizationAllowedActions.ts create mode 100644 ngui/ui/src/hooks/coreData/useOrganizationFeatures.ts rename ngui/ui/src/hooks/{ => coreData}/useOrganizationPerspectives.ts (58%) create mode 100644 ngui/ui/src/hooks/coreData/useOrganizationThemeSettings.ts create mode 100644 ngui/ui/src/hooks/coreData/useOrganizations.ts delete mode 100644 ngui/ui/src/hooks/useAuthorization.ts create mode 100644 ngui/ui/src/hooks/useGetToken.ts delete mode 100644 ngui/ui/src/hooks/useNewAuthorization.ts delete mode 100644 ngui/ui/src/hooks/useOptScaleMode.ts delete mode 100644 ngui/ui/src/hooks/useOrganizationFeatures.ts delete mode 100644 ngui/ui/src/hooks/useOrganizationIdQueryParameterListener.ts delete mode 100644 ngui/ui/src/hooks/useOrganizationThemeSettings.ts create mode 100644 ngui/ui/src/hooks/useUpdateScope.ts delete mode 100644 ngui/ui/src/pages/AcceptInvitations/AcceptInvitations.tsx delete mode 100644 ngui/ui/src/pages/AcceptInvitations/index.ts create mode 100644 ngui/ui/src/pages/Initialize/Initialize.tsx create mode 100644 ngui/ui/src/pages/Initialize/index.ts delete mode 100644 ngui/ui/src/services/InvitationsService.ts delete mode 100644 ngui/ui/src/services/OrganizationsService.ts delete mode 100644 ngui/ui/src/utils/routes/acceptInvitationsRoute.ts create mode 100644 ngui/ui/src/utils/routes/initializeRoute.ts diff --git a/auth/auth_server/controllers/signin.py b/auth/auth_server/controllers/signin.py index 9627f247..82e0fc43 100644 --- a/auth/auth_server/controllers/signin.py +++ b/auth/auth_server/controllers/signin.py @@ -62,6 +62,7 @@ def exchange_token(self, code, redirect_uri): "code": code, 'redirect_uri': redirect_uri, } + LOG.error(f"request_body: {request_body}") request = google_requests.Request() response = request( url=self.DEFAULT_TOKEN_URI, @@ -69,11 +70,14 @@ def exchange_token(self, code, redirect_uri): headers={"Content-Type": "application/x-www-form-urlencoded"}, body=urlencode(request_body).encode("utf-8"), ) + LOG.error(f"response: {response}") + LOG.error(f"response.data: {response.data}") response_body = ( response.data.decode("utf-8") if hasattr(response.data, "decode") else response.data ) + LOG.error(f"response_body: {response_body}") if response.status != 200: raise ValueError(response_body) response_data = json.loads(response_body) @@ -81,10 +85,13 @@ def exchange_token(self, code, redirect_uri): def verify(self, code, **kwargs): try: + LOG.error(f"code: {code}") redirect_uri = kwargs.pop('redirect_uri', None) token = self.exchange_token(code, redirect_uri) + LOG.info(f"token: {token}") token_info = id_token.verify_oauth2_token( token, google_requests.Request(), self.client_id()) + LOG.warning(f"token_info: {token_info}") if not token_info.get('email_verified', False): raise ForbiddenException(Err.OA0012, []) email = token_info['email'] diff --git a/auth/auth_server/handlers/v2/signin.py b/auth/auth_server/handlers/v2/signin.py index 2b4bf943..bd8fb051 100644 --- a/auth/auth_server/handlers/v2/signin.py +++ b/auth/auth_server/handlers/v2/signin.py @@ -66,7 +66,6 @@ async def post(self, **url_params): data = self._request_body() data.update(url_params) data.update({'ip': self.get_ip_addr()}) - data.update({'redirect_uri': self.request.headers.get('Origin')}) await self._validate_params(**data) res = await run_task(self.controller.signin, **data) self.set_status(201) diff --git a/ngui/server/.env.sample b/ngui/server/.env.sample index 4673cc40..aef38854 100644 --- a/ngui/server/.env.sample +++ b/ngui/server/.env.sample @@ -10,8 +10,11 @@ KEEPER_ENDPOINT= # Slacker endpoint. Used for endpoints that are migrated to Apollo. SLACKER_ENDPOINT= -# Rest endpoint. Used for endpoints that are migrated to Apollo. -REST_ENDPOINT= +# Restapi endpoint. Used for endpoints that are migrated to Apollo. +RESTAPI_ENDPOINT= + +# Auth endpoint. Used for endpoints that are migrated to Apollo. +AUTH_ENDPOINT= # Helps distinguish environments with the same values for NODE_ENV/import.meta.env/etc., because they are built similarly/identically (e.g., with k8s). # Can be any desired value, e.g., 'staging', 'production', etc. diff --git a/ngui/server/api/auth/client.ts b/ngui/server/api/auth/client.ts new file mode 100644 index 00000000..75c52a67 --- /dev/null +++ b/ngui/server/api/auth/client.ts @@ -0,0 +1,77 @@ +import BaseClient from "../baseClient.js"; +import { + MutationTokenArgs, + MutationUpdateUserArgs, + OrganizationAllowedActionsRequestParams, +} from "../../graphql/resolvers/auth.generated.js"; + +class AuthClient extends BaseClient { + override baseURL = `${process.env.AUTH_ENDPOINT || this.endpoint}/auth/v2/`; + + async getOrganizationAllowedActions( + requestParams: OrganizationAllowedActionsRequestParams + ) { + const path = `allowed_actions?organization=${requestParams.organization}`; + const actions = await this.get(path); + + return actions.allowed_actions; + } + + async createToken({ email, password, code }: MutationTokenArgs) { + const result = await this.post("tokens", { + body: { email, password, verification_code: code }, + }); + + return { + token: result.token, + user_email: result.user_email, + user_id: result.user_id, + }; + } + + async createUser(email, password, name) { + const result = await this.post("users", { + body: { email, password, display_name: name }, + }); + + return { + token: result.token, + user_email: result.email, + user_id: result.id, + }; + } + + async updateUser( + userId: MutationUpdateUserArgs["id"], + params: MutationUpdateUserArgs["params"] + ) { + const result = await this.patch(`users/${userId}`, { + body: { display_name: params.name, password: params.password }, + }); + + return { + token: result.token, + user_email: result.email, + user_id: result.id, + }; + } + + async signIn(provider, token, tenantId, redirectUri) { + const result = await this.post("signin", { + body: { + provider, + token, + tenant_id: tenantId, + redirect_uri: redirectUri, + }, + }); + + return { + token: result.token, + user_email: result.user_email, + user_id: result.user_id, + }; + } +} + +export default AuthClient; diff --git a/ngui/server/api/restapi/client.ts b/ngui/server/api/restapi/client.ts index d6e98ca0..90695923 100644 --- a/ngui/server/api/restapi/client.ts +++ b/ngui/server/api/restapi/client.ts @@ -2,15 +2,44 @@ import BaseClient from "../baseClient.js"; import { DataSourceRequestParams, MutationUpdateEmployeeEmailsArgs, - UpdateDataSourceInput, MutationUpdateEmployeeEmailArgs, + MutationCreateDataSourceArgs, + MutationUpdateDataSourceArgs, + MutationUpdateOrganizationArgs, + MutationCreateOrganizationArgs, + MutationDeleteOrganizationArgs, + MutationUpdateOptscaleModeArgs, + MutationUpdateOrganizationPerspectivesArgs, + QueryOrganizationPerspectivesArgs, + QueryOrganizationFeaturesArgs, } from "../../graphql/resolvers/restapi.generated.js"; -class RestClient extends BaseClient { +class RestApiClient extends BaseClient { override baseURL = `${ - process.env.REST_ENDPOINT || this.endpoint + process.env.RESTAPI_ENDPOINT || this.endpoint }/restapi/v2/`; + async getOrganizations() { + const organizations = await this.get("organizations"); + + return organizations.organizations; + } + + async getCurrentEmployee(organizationId: string) { + const path = `organizations/${organizationId}/employees?current_only=true`; + const currentEmployee = await this.get(path); + + return currentEmployee.employees[0]; + } + + async getDataSources(organizationId: string) { + const path = `organizations/${organizationId}/cloud_accounts?details=true`; + + const dataSources = await this.get(path); + + return dataSources.cloud_accounts; + } + async getDataSource( dataSourceId: string, requestParams: DataSourceRequestParams @@ -22,7 +51,37 @@ class RestClient extends BaseClient { return dataSource; } - async updateDataSource(dataSourceId, params: UpdateDataSourceInput) { + async createDataSource( + organizationId: MutationCreateDataSourceArgs["organizationId"], + params: MutationCreateDataSourceArgs["params"] + ) { + const path = `organizations/${organizationId}/cloud_accounts`; + + const dataSource = await this.post(path, { + body: { + name: params.name, + type: params.type, + config: { + ...params.awsRootConfig, + ...params.awsLinkedConfig, + ...params.azureSubscriptionConfig, + ...params.azureTenantConfig, + ...params.gcpConfig, + ...params.alibabaConfig, + ...params.nebiusConfig, + ...params.databricksConfig, + ...params.k8sConfig, + }, + }, + }); + + return dataSource; + } + + async updateDataSource( + dataSourceId: MutationUpdateDataSourceArgs["dataSourceId"], + params: MutationUpdateDataSourceArgs["params"] + ) { const path = `cloud_accounts/${dataSourceId}`; const dataSource = await this.patch(path, { @@ -90,6 +149,144 @@ class RestClient extends BaseClient { return email; } + + async deleteDataSource(dataSourceId) { + const path = `cloud_accounts/${dataSourceId}`; + + return await this.delete(path); + } + + async getInvitations() { + const invitations = await this.get("invites"); + + return invitations.invites; + } + + async updateInvitation(invitationId: string, action: string) { + const path = `invites/${invitationId}`; + + return await this.patch(path, { + body: JSON.stringify({ + action, + }), + }); + } + + async getOrganizationFeatures( + organizationId: QueryOrganizationFeaturesArgs["organizationId"] + ) { + const path = `organizations/${organizationId}/options/features`; + const features = await this.get(path); + + const parsedFeatures = JSON.parse(features.value); + + return parsedFeatures; + } + + async getOptscaleMode(organizationId: string) { + const path = `organizations/${organizationId}/options/optscale_mode`; + const mode = await this.get(path); + + const parsedMode = JSON.parse(mode.value); + + return parsedMode.value; + } + + async updateOptscaleMode( + organizationId: MutationUpdateOptscaleModeArgs["organizationId"], + value: MutationUpdateOptscaleModeArgs["value"] + ) { + const path = `organizations/${organizationId}/options/optscale_mode`; + const mode = await this.patch(path, { + body: { + value: JSON.stringify({ + value, + }), + }, + }); + + const parsedMode = JSON.parse(mode.value); + + return parsedMode.value; + } + + async getOrganizationThemeSettings(organizationId: string) { + const path = `organizations/${organizationId}/options/theme_settings`; + const settings = await this.get(path); + + const parsedSettings = JSON.parse(settings.value); + + return parsedSettings; + } + + async updateOrganizationThemeSettings(organizationId, value) { + const themeSettings = await this.patch( + `organizations/${organizationId}/options/theme_settings`, + { + body: { + value: JSON.stringify(value), + }, + } + ); + + const parsedThemeSettings = JSON.parse(themeSettings.value); + + return parsedThemeSettings; + } + + async getOrganizationPerspectives( + organizationId: QueryOrganizationPerspectivesArgs["organizationId"] + ) { + const path = `organizations/${organizationId}/options/perspectives`; + const perspectives = await this.get(path); + + const parsedPerspectives = JSON.parse(perspectives.value); + + return parsedPerspectives; + } + + async updateOrganizationPerspectives( + organizationId: MutationUpdateOrganizationPerspectivesArgs["organizationId"], + value: MutationUpdateOrganizationPerspectivesArgs["value"] + ) { + const perspectives = await this.patch( + `organizations/${organizationId}/options/perspectives`, + { + body: { + value: JSON.stringify(value), + }, + } + ); + + const parsedPerspectives = JSON.parse(perspectives.value); + + return parsedPerspectives; + } + + async createOrganization( + organizationName: MutationCreateOrganizationArgs["organizationName"] + ) { + return await this.post("organizations", { + body: { + name: organizationName, + }, + }); + } + + async updateOrganization( + organizationId: MutationUpdateOrganizationArgs["organizationId"], + params: MutationUpdateOrganizationArgs["params"] + ) { + return await this.patch(`organizations/${organizationId}`, { + body: params, + }); + } + + async deleteOrganization( + organizationId: MutationDeleteOrganizationArgs["organizationId"] + ) { + return await this.delete(`organizations/${organizationId}`); + } } -export default RestClient; +export default RestApiClient; diff --git a/ngui/server/api/slacker/client.ts b/ngui/server/api/slacker/client.ts index 96d53852..ca560a41 100644 --- a/ngui/server/api/slacker/client.ts +++ b/ngui/server/api/slacker/client.ts @@ -13,7 +13,7 @@ class SlackerClient extends BaseClient { async connectSlackUser(secret) { return this.post("connect_slack_user", { - body: JSON.stringify({ secret }), + body: { secret }, }); } } diff --git a/ngui/server/codegen.ts b/ngui/server/codegen.ts index 31f2a909..38b8f81b 100644 --- a/ngui/server/codegen.ts +++ b/ngui/server/codegen.ts @@ -23,6 +23,10 @@ const config: CodegenConfig = { }, }, }, + "./graphql/resolvers/auth.generated.ts": { + schema: "./graphql/schemas/auth.graphql", + plugins: commonPlugins, + }, }, }; diff --git a/ngui/server/graphql/resolvers/auth.generated.ts b/ngui/server/graphql/resolvers/auth.generated.ts new file mode 100644 index 00000000..61f3f2ce --- /dev/null +++ b/ngui/server/graphql/resolvers/auth.generated.ts @@ -0,0 +1,205 @@ +import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql'; +export type Maybe = T | null; +export type InputMaybe = Maybe; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; +export type RequireFields = Omit & { [P in K]-?: NonNullable }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string; } + String: { input: string; output: string; } + Boolean: { input: boolean; output: boolean; } + Int: { input: number; output: number; } + Float: { input: number; output: number; } + OrganizationAllowedActionsScalar: { input: any; output: any; } +}; + +export type Mutation = { + __typename?: 'Mutation'; + signIn?: Maybe; + token?: Maybe; + updateUser?: Maybe; + user?: Maybe; +}; + + +export type MutationSignInArgs = { + provider: Scalars['String']['input']; + redirectUri?: InputMaybe; + tenantId?: InputMaybe; + token: Scalars['String']['input']; +}; + + +export type MutationTokenArgs = { + code?: InputMaybe; + email: Scalars['String']['input']; + password?: InputMaybe; +}; + + +export type MutationUpdateUserArgs = { + id: Scalars['ID']['input']; + params: UpdateUserParams; +}; + + +export type MutationUserArgs = { + email: Scalars['String']['input']; + name: Scalars['String']['input']; + password: Scalars['String']['input']; +}; + +export type OrganizationAllowedActionsRequestParams = { + organization: Scalars['String']['input']; +}; + +export type Query = { + __typename?: 'Query'; + organizationAllowedActions?: Maybe; +}; + + +export type QueryOrganizationAllowedActionsArgs = { + requestParams?: InputMaybe; +}; + +export type Token = { + __typename?: 'Token'; + token?: Maybe; + user_email: Scalars['String']['output']; + user_id: Scalars['ID']['output']; +}; + +export type UpdateUserParams = { + name?: InputMaybe; + password?: InputMaybe; +}; + + + +export type ResolverTypeWrapper = Promise | T; + + +export type ResolverWithResolve = { + resolve: ResolverFn; +}; +export type Resolver = ResolverFn | ResolverWithResolve; + +export type ResolverFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => Promise | TResult; + +export type SubscriptionSubscribeFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => AsyncIterable | Promise>; + +export type SubscriptionResolveFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => TResult | Promise; + +export interface SubscriptionSubscriberObject { + subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs>; + resolve?: SubscriptionResolveFn; +} + +export interface SubscriptionResolverObject { + subscribe: SubscriptionSubscribeFn; + resolve: SubscriptionResolveFn; +} + +export type SubscriptionObject = + | SubscriptionSubscriberObject + | SubscriptionResolverObject; + +export type SubscriptionResolver = + | ((...args: any[]) => SubscriptionObject) + | SubscriptionObject; + +export type TypeResolveFn = ( + parent: TParent, + context: TContext, + info: GraphQLResolveInfo +) => Maybe | Promise>; + +export type IsTypeOfResolverFn = (obj: T, context: TContext, info: GraphQLResolveInfo) => boolean | Promise; + +export type NextResolverFn = () => Promise; + +export type DirectiveResolverFn = ( + next: NextResolverFn, + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => TResult | Promise; + + + +/** Mapping between all available schema types and the resolvers types */ +export type ResolversTypes = { + Boolean: ResolverTypeWrapper; + ID: ResolverTypeWrapper; + Mutation: ResolverTypeWrapper<{}>; + OrganizationAllowedActionsRequestParams: OrganizationAllowedActionsRequestParams; + OrganizationAllowedActionsScalar: ResolverTypeWrapper; + Query: ResolverTypeWrapper<{}>; + String: ResolverTypeWrapper; + Token: ResolverTypeWrapper; + UpdateUserParams: UpdateUserParams; +}; + +/** Mapping between all available schema types and the resolvers parents */ +export type ResolversParentTypes = { + Boolean: Scalars['Boolean']['output']; + ID: Scalars['ID']['output']; + Mutation: {}; + OrganizationAllowedActionsRequestParams: OrganizationAllowedActionsRequestParams; + OrganizationAllowedActionsScalar: Scalars['OrganizationAllowedActionsScalar']['output']; + Query: {}; + String: Scalars['String']['output']; + Token: Token; + UpdateUserParams: UpdateUserParams; +}; + +export type MutationResolvers = { + signIn?: Resolver, ParentType, ContextType, RequireFields>; + token?: Resolver, ParentType, ContextType, RequireFields>; + updateUser?: Resolver, ParentType, ContextType, RequireFields>; + user?: Resolver, ParentType, ContextType, RequireFields>; +}; + +export interface OrganizationAllowedActionsScalarScalarConfig extends GraphQLScalarTypeConfig { + name: 'OrganizationAllowedActionsScalar'; +} + +export type QueryResolvers = { + organizationAllowedActions?: Resolver, ParentType, ContextType, Partial>; +}; + +export type TokenResolvers = { + token?: Resolver, ParentType, ContextType>; + user_email?: Resolver; + user_id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type Resolvers = { + Mutation?: MutationResolvers; + OrganizationAllowedActionsScalar?: GraphQLScalarType; + Query?: QueryResolvers; + Token?: TokenResolvers; +}; + diff --git a/ngui/server/graphql/resolvers/auth.ts b/ngui/server/graphql/resolvers/auth.ts new file mode 100644 index 00000000..dd289df7 --- /dev/null +++ b/ngui/server/graphql/resolvers/auth.ts @@ -0,0 +1,33 @@ +import { Resolvers } from "./auth.generated.js"; + +const resolvers: Resolvers = { + Query: { + organizationAllowedActions: async ( + _, + { requestParams }, + { dataSources } + ) => { + return dataSources.auth.getOrganizationAllowedActions(requestParams); + }, + }, + Mutation: { + token: async (_, { email, password, code }, { dataSources }) => { + return dataSources.auth.createToken({ email, password, code }); + }, + user: async (_, { email, password, name }, { dataSources }) => { + return dataSources.auth.createUser(email, password, name); + }, + updateUser: async (_, { id, params }, { dataSources }) => { + return dataSources.auth.updateUser(id, params); + }, + signIn: async ( + _, + { provider, token, tenantId, redirectUri }, + { dataSources } + ) => { + return dataSources.auth.signIn(provider, token, tenantId, redirectUri); + }, + }, +}; + +export default resolvers; diff --git a/ngui/server/graphql/resolvers/restapi.generated.ts b/ngui/server/graphql/resolvers/restapi.generated.ts index 501a9291..ee8e569f 100644 --- a/ngui/server/graphql/resolvers/restapi.generated.ts +++ b/ngui/server/graphql/resolvers/restapi.generated.ts @@ -152,12 +152,26 @@ export type AzureTenantDataSource = DataSourceInterface & { type: DataSourceType; }; +export type CreateDataSourceInput = { + alibabaConfig?: InputMaybe; + awsLinkedConfig?: InputMaybe; + awsRootConfig?: InputMaybe; + azureSubscriptionConfig?: InputMaybe; + azureTenantConfig?: InputMaybe; + databricksConfig?: InputMaybe; + gcpConfig?: InputMaybe; + k8sConfig?: InputMaybe; + name?: InputMaybe; + nebiusConfig?: InputMaybe; + type?: InputMaybe; +}; + export type DataSourceDetails = { __typename?: 'DataSourceDetails'; cost: Scalars['Float']['output']; discovery_infos?: Maybe>>; forecast: Scalars['Float']['output']; - last_month_cost: Scalars['Float']['output']; + last_month_cost?: Maybe; resources: Scalars['Int']['output']; }; @@ -235,6 +249,13 @@ export type DatabricksDataSource = DataSourceInterface & { type: DataSourceType; }; +export type Employee = { + __typename?: 'Employee'; + id: Scalars['String']['output']; + jira_connected: Scalars['Boolean']['output']; + slack_connected: Scalars['Boolean']['output']; +}; + export type EmployeeEmail = { __typename?: 'EmployeeEmail'; available_by_role: Scalars['Boolean']['output']; @@ -299,6 +320,23 @@ export type GcpDataSource = DataSourceInterface & { type: DataSourceType; }; +export type Invitation = { + __typename?: 'Invitation'; + id: Scalars['String']['output']; + invite_assignments?: Maybe>; + organization: Scalars['String']['output']; + owner_email: Scalars['String']['output']; + owner_name: Scalars['String']['output']; +}; + +export type InvitationAssignment = { + __typename?: 'InvitationAssignment'; + id: Scalars['String']['output']; + purpose: Scalars['String']['output']; + scope_id: Scalars['String']['output']; + scope_type: Scalars['String']['output']; +}; + export type K8CostModelConfig = { __typename?: 'K8CostModelConfig'; cpu_hourly_cost: Scalars['Float']['output']; @@ -335,9 +373,39 @@ export type K8sDataSource = DataSourceInterface & { export type Mutation = { __typename?: 'Mutation'; + createDataSource?: Maybe; + createOrganization?: Maybe; + deleteDataSource?: Maybe; + deleteOrganization?: Maybe; updateDataSource?: Maybe; updateEmployeeEmail?: Maybe; updateEmployeeEmails?: Maybe>>; + updateInvitation?: Maybe; + updateOptscaleMode?: Maybe; + updateOrganization?: Maybe; + updateOrganizationPerspectives?: Maybe; + updateOrganizationThemeSettings?: Maybe; +}; + + +export type MutationCreateDataSourceArgs = { + organizationId: Scalars['ID']['input']; + params: CreateDataSourceInput; +}; + + +export type MutationCreateOrganizationArgs = { + organizationName: Scalars['String']['input']; +}; + + +export type MutationDeleteDataSourceArgs = { + dataSourceId: Scalars['ID']['input']; +}; + + +export type MutationDeleteOrganizationArgs = { + organizationId: Scalars['ID']['input']; }; @@ -358,6 +426,36 @@ export type MutationUpdateEmployeeEmailsArgs = { params: UpdateEmployeeEmailsInput; }; + +export type MutationUpdateInvitationArgs = { + action: Scalars['String']['input']; + invitationId: Scalars['String']['input']; +}; + + +export type MutationUpdateOptscaleModeArgs = { + organizationId: Scalars['ID']['input']; + value?: InputMaybe; +}; + + +export type MutationUpdateOrganizationArgs = { + organizationId: Scalars['ID']['input']; + params: UpdateOrganizationInput; +}; + + +export type MutationUpdateOrganizationPerspectivesArgs = { + organizationId: Scalars['ID']['input']; + value: Scalars['JSONObject']['input']; +}; + + +export type MutationUpdateOrganizationThemeSettingsArgs = { + organizationId: Scalars['ID']['input']; + value: Scalars['JSONObject']['input']; +}; + export type NebiusConfig = { __typename?: 'NebiusConfig'; access_key_id?: Maybe; @@ -396,10 +494,43 @@ export type NebiusDataSource = DataSourceInterface & { type: DataSourceType; }; +export type OptscaleMode = { + __typename?: 'OptscaleMode'; + finops?: Maybe; + mlops?: Maybe; +}; + +export type OptscaleModeParams = { + finops?: InputMaybe; + mlops?: InputMaybe; +}; + +export type Organization = { + __typename?: 'Organization'; + currency: Scalars['String']['output']; + id: Scalars['String']['output']; + is_demo: Scalars['Boolean']['output']; + name: Scalars['String']['output']; + pool_id: Scalars['String']['output']; +}; + export type Query = { __typename?: 'Query'; + currentEmployee?: Maybe; dataSource?: Maybe; + dataSources?: Maybe>>; employeeEmails?: Maybe>>; + invitations?: Maybe>>; + optscaleMode?: Maybe; + organizationFeatures?: Maybe; + organizationPerspectives?: Maybe; + organizationThemeSettings?: Maybe; + organizations?: Maybe>>; +}; + + +export type QueryCurrentEmployeeArgs = { + organizationId: Scalars['ID']['input']; }; @@ -409,10 +540,35 @@ export type QueryDataSourceArgs = { }; +export type QueryDataSourcesArgs = { + organizationId: Scalars['ID']['input']; +}; + + export type QueryEmployeeEmailsArgs = { employeeId: Scalars['ID']['input']; }; + +export type QueryOptscaleModeArgs = { + organizationId: Scalars['ID']['input']; +}; + + +export type QueryOrganizationFeaturesArgs = { + organizationId: Scalars['ID']['input']; +}; + + +export type QueryOrganizationPerspectivesArgs = { + organizationId: Scalars['ID']['input']; +}; + + +export type QueryOrganizationThemeSettingsArgs = { + organizationId: Scalars['ID']['input']; +}; + export type UpdateDataSourceInput = { alibabaConfig?: InputMaybe; awsLinkedConfig?: InputMaybe; @@ -443,6 +599,11 @@ export type UpdateEmployeeEmailsInput = { enable?: InputMaybe>; }; +export type UpdateOrganizationInput = { + currency?: InputMaybe; + name?: InputMaybe; +}; + export type ResolverTypeWrapper = Promise | T; @@ -532,6 +693,7 @@ export type ResolversTypes = { AzureTenantConfigInput: AzureTenantConfigInput; AzureTenantDataSource: ResolverTypeWrapper; Boolean: ResolverTypeWrapper; + CreateDataSourceInput: CreateDataSourceInput; DataSourceDetails: ResolverTypeWrapper; DataSourceDiscoveryInfos: ResolverTypeWrapper; DataSourceInterface: ResolverTypeWrapper['DataSourceInterface']>; @@ -540,6 +702,7 @@ export type ResolversTypes = { DatabricksConfig: ResolverTypeWrapper; DatabricksConfigInput: DatabricksConfigInput; DatabricksDataSource: ResolverTypeWrapper; + Employee: ResolverTypeWrapper; EmployeeEmail: ResolverTypeWrapper; EnvironmentDataSource: ResolverTypeWrapper; Float: ResolverTypeWrapper; @@ -550,6 +713,8 @@ export type ResolversTypes = { GcpDataSource: ResolverTypeWrapper; ID: ResolverTypeWrapper; Int: ResolverTypeWrapper; + Invitation: ResolverTypeWrapper; + InvitationAssignment: ResolverTypeWrapper; JSONObject: ResolverTypeWrapper; K8CostModelConfig: ResolverTypeWrapper; K8sConfig: ResolverTypeWrapper; @@ -559,12 +724,16 @@ export type ResolversTypes = { NebiusConfig: ResolverTypeWrapper; NebiusConfigInput: NebiusConfigInput; NebiusDataSource: ResolverTypeWrapper; + OptscaleMode: ResolverTypeWrapper; + OptscaleModeParams: OptscaleModeParams; + Organization: ResolverTypeWrapper; Query: ResolverTypeWrapper<{}>; String: ResolverTypeWrapper; UpdateDataSourceInput: UpdateDataSourceInput; UpdateEmployeeEmailInput: UpdateEmployeeEmailInput; UpdateEmployeeEmailsAction: UpdateEmployeeEmailsAction; UpdateEmployeeEmailsInput: UpdateEmployeeEmailsInput; + UpdateOrganizationInput: UpdateOrganizationInput; }; /** Mapping between all available schema types and the resolvers parents */ @@ -583,6 +752,7 @@ export type ResolversParentTypes = { AzureTenantConfigInput: AzureTenantConfigInput; AzureTenantDataSource: AzureTenantDataSource; Boolean: Scalars['Boolean']['output']; + CreateDataSourceInput: CreateDataSourceInput; DataSourceDetails: DataSourceDetails; DataSourceDiscoveryInfos: DataSourceDiscoveryInfos; DataSourceInterface: ResolversInterfaceTypes['DataSourceInterface']; @@ -590,6 +760,7 @@ export type ResolversParentTypes = { DatabricksConfig: DatabricksConfig; DatabricksConfigInput: DatabricksConfigInput; DatabricksDataSource: DatabricksDataSource; + Employee: Employee; EmployeeEmail: EmployeeEmail; EnvironmentDataSource: EnvironmentDataSource; Float: Scalars['Float']['output']; @@ -600,6 +771,8 @@ export type ResolversParentTypes = { GcpDataSource: GcpDataSource; ID: Scalars['ID']['output']; Int: Scalars['Int']['output']; + Invitation: Invitation; + InvitationAssignment: InvitationAssignment; JSONObject: Scalars['JSONObject']['output']; K8CostModelConfig: K8CostModelConfig; K8sConfig: K8sConfig; @@ -609,11 +782,15 @@ export type ResolversParentTypes = { NebiusConfig: NebiusConfig; NebiusConfigInput: NebiusConfigInput; NebiusDataSource: NebiusDataSource; + OptscaleMode: OptscaleMode; + OptscaleModeParams: OptscaleModeParams; + Organization: Organization; Query: {}; String: Scalars['String']['output']; UpdateDataSourceInput: UpdateDataSourceInput; UpdateEmployeeEmailInput: UpdateEmployeeEmailInput; UpdateEmployeeEmailsInput: UpdateEmployeeEmailsInput; + UpdateOrganizationInput: UpdateOrganizationInput; }; export type AlibabaConfigResolvers = { @@ -720,7 +897,7 @@ export type DataSourceDetailsResolvers; discovery_infos?: Resolver>>, ParentType, ContextType>; forecast?: Resolver; - last_month_cost?: Resolver; + last_month_cost?: Resolver, ParentType, ContextType>; resources?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -778,6 +955,13 @@ export type DatabricksDataSourceResolvers; }; +export type EmployeeResolvers = { + id?: Resolver; + jira_connected?: Resolver; + slack_connected?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type EmployeeEmailResolvers = { available_by_role?: Resolver; email_template?: Resolver; @@ -832,6 +1016,23 @@ export type GcpDataSourceResolvers; }; +export type InvitationResolvers = { + id?: Resolver; + invite_assignments?: Resolver>, ParentType, ContextType>; + organization?: Resolver; + owner_email?: Resolver; + owner_name?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type InvitationAssignmentResolvers = { + id?: Resolver; + purpose?: Resolver; + scope_id?: Resolver; + scope_type?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export interface JsonObjectScalarConfig extends GraphQLScalarTypeConfig { name: 'JSONObject'; } @@ -866,9 +1067,18 @@ export type K8sDataSourceResolvers = { + createDataSource?: Resolver, ParentType, ContextType, RequireFields>; + createOrganization?: Resolver, ParentType, ContextType, RequireFields>; + deleteDataSource?: Resolver, ParentType, ContextType, RequireFields>; + deleteOrganization?: Resolver, ParentType, ContextType, RequireFields>; updateDataSource?: Resolver, ParentType, ContextType, RequireFields>; updateEmployeeEmail?: Resolver, ParentType, ContextType, RequireFields>; updateEmployeeEmails?: Resolver>>, ParentType, ContextType, RequireFields>; + updateInvitation?: Resolver, ParentType, ContextType, RequireFields>; + updateOptscaleMode?: Resolver, ParentType, ContextType, RequireFields>; + updateOrganization?: Resolver, ParentType, ContextType, RequireFields>; + updateOrganizationPerspectives?: Resolver, ParentType, ContextType, RequireFields>; + updateOrganizationThemeSettings?: Resolver, ParentType, ContextType, RequireFields>; }; export type NebiusConfigResolvers = { @@ -898,9 +1108,32 @@ export type NebiusDataSourceResolvers; }; +export type OptscaleModeResolvers = { + finops?: Resolver, ParentType, ContextType>; + mlops?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type OrganizationResolvers = { + currency?: Resolver; + id?: Resolver; + is_demo?: Resolver; + name?: Resolver; + pool_id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type QueryResolvers = { + currentEmployee?: Resolver, ParentType, ContextType, RequireFields>; dataSource?: Resolver, ParentType, ContextType, RequireFields>; + dataSources?: Resolver>>, ParentType, ContextType, RequireFields>; employeeEmails?: Resolver>>, ParentType, ContextType, RequireFields>; + invitations?: Resolver>>, ParentType, ContextType>; + optscaleMode?: Resolver, ParentType, ContextType, RequireFields>; + organizationFeatures?: Resolver, ParentType, ContextType, RequireFields>; + organizationPerspectives?: Resolver, ParentType, ContextType, RequireFields>; + organizationThemeSettings?: Resolver, ParentType, ContextType, RequireFields>; + organizations?: Resolver>>, ParentType, ContextType>; }; export type Resolvers = { @@ -917,11 +1150,14 @@ export type Resolvers = { DataSourceInterface?: DataSourceInterfaceResolvers; DatabricksConfig?: DatabricksConfigResolvers; DatabricksDataSource?: DatabricksDataSourceResolvers; + Employee?: EmployeeResolvers; EmployeeEmail?: EmployeeEmailResolvers; EnvironmentDataSource?: EnvironmentDataSourceResolvers; GcpBillingDataConfig?: GcpBillingDataConfigResolvers; GcpConfig?: GcpConfigResolvers; GcpDataSource?: GcpDataSourceResolvers; + Invitation?: InvitationResolvers; + InvitationAssignment?: InvitationAssignmentResolvers; JSONObject?: GraphQLScalarType; K8CostModelConfig?: K8CostModelConfigResolvers; K8sConfig?: K8sConfigResolvers; @@ -929,6 +1165,8 @@ export type Resolvers = { Mutation?: MutationResolvers; NebiusConfig?: NebiusConfigResolvers; NebiusDataSource?: NebiusDataSourceResolvers; + OptscaleMode?: OptscaleModeResolvers; + Organization?: OrganizationResolvers; Query?: QueryResolvers; }; diff --git a/ngui/server/graphql/resolvers/restapi.ts b/ngui/server/graphql/resolvers/restapi.ts index 599e3534..19855538 100644 --- a/ngui/server/graphql/resolvers/restapi.ts +++ b/ngui/server/graphql/resolvers/restapi.ts @@ -47,8 +47,47 @@ const resolvers: Resolvers = { employeeEmails: async (_, { employeeId }, { dataSources }) => { return dataSources.restapi.getEmployeeEmails(employeeId); }, + organizations: async (_, __, { dataSources }) => { + return dataSources.restapi.getOrganizations(); + }, + currentEmployee: async (_, { organizationId }, { dataSources }) => { + return dataSources.restapi.getCurrentEmployee(organizationId); + }, + dataSources: async (_, { organizationId }, { dataSources }) => { + return dataSources.restapi.getDataSources(organizationId); + }, + invitations: async (_, __, { dataSources }) => { + return dataSources.restapi.getInvitations(); + }, + organizationFeatures: async (_, { organizationId }, { dataSources }) => { + return dataSources.restapi.getOrganizationFeatures(organizationId); + }, + optscaleMode: async (_, { organizationId }, { dataSources }) => { + return dataSources.restapi.getOptscaleMode(organizationId); + }, + organizationThemeSettings: async ( + _, + { organizationId }, + { dataSources } + ) => { + return dataSources.restapi.getOrganizationThemeSettings(organizationId); + }, + organizationPerspectives: async ( + _, + { organizationId }, + { dataSources } + ) => { + return dataSources.restapi.getOrganizationPerspectives(organizationId); + }, }, Mutation: { + createDataSource: async ( + _, + { organizationId, params }, + { dataSources } + ) => { + return dataSources.restapi.createDataSource(organizationId, params); + }, updateDataSource: async (_, { dataSourceId, params }, { dataSources }) => { return dataSources.restapi.updateDataSource(dataSourceId, params); }, @@ -62,6 +101,52 @@ const resolvers: Resolvers = { updateEmployeeEmail: async (_, { employeeId, params }, { dataSources }) => { return dataSources.restapi.updateEmployeeEmail(employeeId, params); }, + deleteDataSource: async (_, { dataSourceId }, { dataSources }) => { + return dataSources.restapi.deleteDataSource(dataSourceId); + }, + createOrganization: async (_, { organizationName }, { dataSources }) => { + return dataSources.restapi.createOrganization(organizationName); + }, + updateOrganization: async ( + _, + { organizationId, params }, + { dataSources } + ) => { + return dataSources.restapi.updateOrganization(organizationId, params); + }, + deleteOrganization: async (_, { organizationId }, { dataSources }) => { + return dataSources.restapi.deleteOrganization(organizationId); + }, + updateInvitation: async (_, { invitationId, action }, { dataSources }) => { + return dataSources.restapi.updateInvitation(invitationId, action); + }, + updateOptscaleMode: async ( + _, + { organizationId, value }, + { dataSources } + ) => { + return dataSources.restapi.updateOptscaleMode(organizationId, value); + }, + updateOrganizationThemeSettings: async ( + _, + { organizationId, value }, + { dataSources } + ) => { + return dataSources.restapi.updateOrganizationThemeSettings( + organizationId, + value + ); + }, + updateOrganizationPerspectives: async ( + _, + { organizationId, value }, + { dataSources } + ) => { + return dataSources.restapi.updateOrganizationPerspectives( + organizationId, + value + ); + }, }, }; diff --git a/ngui/server/graphql/schemas/auth.graphql b/ngui/server/graphql/schemas/auth.graphql new file mode 100644 index 00000000..1aa8dcfc --- /dev/null +++ b/ngui/server/graphql/schemas/auth.graphql @@ -0,0 +1,36 @@ +input OrganizationAllowedActionsRequestParams { + organization: String! +} + +# TODO: Split Token and User and use them separately for token and user related mutations? +type Token { + user_email: String! + user_id: ID! + token: String +} + +# TODO: Represents an object with dynamic fields (IDs) and an array of strings +scalar OrganizationAllowedActionsScalar + +type Query { + organizationAllowedActions( + requestParams: OrganizationAllowedActionsRequestParams + ): OrganizationAllowedActionsScalar +} + +input UpdateUserParams { + password: String + name: String +} + +type Mutation { + token(email: String!, password: String, code: String): Token + user(email: String!, password: String!, name: String!): Token + updateUser(id: ID!, params: UpdateUserParams!): Token + signIn( + provider: String! + token: String! + tenantId: String + redirectUri: String + ): Token +} diff --git a/ngui/server/graphql/schemas/restapi.graphql b/ngui/server/graphql/schemas/restapi.graphql index 2ce9a481..c5f73f93 100644 --- a/ngui/server/graphql/schemas/restapi.graphql +++ b/ngui/server/graphql/schemas/restapi.graphql @@ -30,7 +30,7 @@ type DataSourceDetails { cost: Float! discovery_infos: [DataSourceDiscoveryInfos] forecast: Float! - last_month_cost: Float! + last_month_cost: Float resources: Int! } @@ -336,6 +336,20 @@ input DatabricksConfigInput { client_secret: String! } +input CreateDataSourceInput { + name: String + type: String + awsRootConfig: AwsRootConfigInput + awsLinkedConfig: AwsLinkedConfigInput + azureSubscriptionConfig: AzureSubscriptionConfigInput + azureTenantConfig: AzureTenantConfigInput + gcpConfig: GcpConfigInput + alibabaConfig: AlibabaConfigInput + nebiusConfig: NebiusConfigInput + databricksConfig: DatabricksConfigInput + k8sConfig: K8sConfigInput +} + input UpdateDataSourceInput { name: String lastImportAt: Int @@ -374,15 +388,71 @@ input UpdateEmployeeEmailInput { action: UpdateEmployeeEmailsAction! } +type Organization { + id: String! + name: String! + is_demo: Boolean! + currency: String! + pool_id: String! +} + +type Employee { + id: String! + jira_connected: Boolean! + slack_connected: Boolean! +} + +type InvitationAssignment { + id: String! + scope_id: String! + scope_type: String! + purpose: String! +} + +type Invitation { + id: String! + owner_name: String! + owner_email: String! + organization: String! + invite_assignments: [InvitationAssignment!] +} + +type OptscaleMode { + finops: Boolean + mlops: Boolean +} + +input OptscaleModeParams { + finops: Boolean + mlops: Boolean +} + +input UpdateOrganizationInput { + name: String + currency: String +} + type Query { + organizations: [Organization] + currentEmployee(organizationId: ID!): Employee + dataSources(organizationId: ID!): [DataSourceInterface] dataSource( dataSourceId: ID! requestParams: DataSourceRequestParams ): DataSourceInterface employeeEmails(employeeId: ID!): [EmployeeEmail] + invitations: [Invitation] + organizationFeatures(organizationId: ID!): JSONObject + optscaleMode(organizationId: ID!): OptscaleMode + organizationThemeSettings(organizationId: ID!): JSONObject + organizationPerspectives(organizationId: ID!): JSONObject } type Mutation { + createDataSource( + organizationId: ID! + params: CreateDataSourceInput! + ): DataSourceInterface updateDataSource( dataSourceId: ID! params: UpdateDataSourceInput! @@ -395,4 +465,24 @@ type Mutation { employeeId: ID! params: UpdateEmployeeEmailInput! ): EmployeeEmail + deleteDataSource(dataSourceId: ID!): String + createOrganization(organizationName: String!): Organization + updateOrganization( + organizationId: ID! + params: UpdateOrganizationInput! + ): Organization + deleteOrganization(organizationId: ID!): String + updateInvitation(invitationId: String!, action: String!): String + updateOptscaleMode( + organizationId: ID! + value: OptscaleModeParams + ): OptscaleMode + updateOrganizationThemeSettings( + organizationId: ID! + value: JSONObject! + ): JSONObject + updateOrganizationPerspectives( + organizationId: ID! + value: JSONObject! + ): JSONObject } diff --git a/ngui/server/server.ts b/ngui/server/server.ts index bec821db..d61595e9 100644 --- a/ngui/server/server.ts +++ b/ngui/server/server.ts @@ -11,11 +11,13 @@ import checkEnvironment from "./checkEnvironment.js"; import KeeperClient from "./api/keeper/client.js"; import keeperResolvers from "./graphql/resolvers/keeper.js"; import slackerResolvers from "./graphql/resolvers/slacker.js"; -import restResolvers from "./graphql/resolvers/restapi.js"; +import authResolvers from "./graphql/resolvers/auth.js"; +import restapiResolvers from "./graphql/resolvers/restapi.js"; import SlackerClient from "./api/slacker/client.js"; import { mergeTypeDefs, mergeResolvers } from "@graphql-tools/merge"; import { loadFilesSync } from "@graphql-tools/load-files"; import RestApiClient from "./api/restapi/client.js"; +import AuthClient from "./api/auth/client.js"; checkEnvironment(["UI_BUILD_PATH", "PROXY_URL"]); @@ -28,6 +30,7 @@ interface ContextValue { keeper: KeeperClient; slacker: SlackerClient; restapi: RestApiClient; + auth: AuthClient; }; } @@ -41,7 +44,8 @@ const typeDefs = mergeTypeDefs(typesArray); const resolvers = mergeResolvers([ keeperResolvers, slackerResolvers, - restResolvers, + restapiResolvers, + authResolvers, ]); // Same ApolloServer initialization as before, plus the drain plugin @@ -75,6 +79,7 @@ app.use( keeper: new KeeperClient({ cache }, token, "http://keeper"), slacker: new SlackerClient({ cache }, token, "http://slacker"), restapi: new RestApiClient({ cache }, token, "http://restapi"), + auth: new AuthClient({ cache }, token, "http://auth"), }, }; }, diff --git a/ngui/ui/src/api/auth/actionCreators.ts b/ngui/ui/src/api/auth/actionCreators.ts index 0113af40..7d389a4b 100644 --- a/ngui/ui/src/api/auth/actionCreators.ts +++ b/ngui/ui/src/api/auth/actionCreators.ts @@ -3,7 +3,6 @@ import { MINUTE } from "api/constants"; import { apiAction, getApiUrl, hashParams } from "api/utils"; import { CREATE_USER, - GET_ORGANIZATION_ALLOWED_ACTIONS, GET_TOKEN, GET_USER, SET_USER, @@ -12,19 +11,18 @@ import { GET_RESOURCE_ALLOWED_ACTIONS, SET_ALLOWED_ACTIONS, SIGN_IN, - UPDATE_USER + UPDATE_USER, + SET_TOKEN } from "./actionTypes"; -import { onSuccessSignIn, onSuccessGetToken } from "./handlers"; +import { onSuccessSignIn } from "./handlers"; export const API_URL = getApiUrl("auth"); -export const getToken = ({ email, password, code, isTokenTemporary }) => +export const getToken = ({ email, password, code }) => apiAction({ url: `${API_URL}/tokens`, - onSuccess: onSuccessGetToken({ - isTokenTemporary - }), + onSuccess: handleSuccess(SET_TOKEN), label: GET_TOKEN, params: { email, password, verification_code: code } }); @@ -62,17 +60,6 @@ export const getUser = (userId) => ttl: 30 * MINUTE }); -export const getOrganizationAllowedActions = (params) => - apiAction({ - url: `${API_URL}/allowed_actions`, - method: "GET", - onSuccess: handleSuccess(SET_ALLOWED_ACTIONS), - label: GET_ORGANIZATION_ALLOWED_ACTIONS, - hash: hashParams(params), - params: { organization: params }, - ttl: 30 * MINUTE - }); - export const getResourceAllowedActions = (params) => apiAction({ url: `${API_URL}/allowed_actions`, diff --git a/ngui/ui/src/api/auth/actionTypes.ts b/ngui/ui/src/api/auth/actionTypes.ts index f56ca204..3fa15fed 100644 --- a/ngui/ui/src/api/auth/actionTypes.ts +++ b/ngui/ui/src/api/auth/actionTypes.ts @@ -7,7 +7,6 @@ export const SET_TOKEN = "SET_TOKEN"; export const GET_USER = "GET_USER"; export const SET_USER = "SET_USER"; -export const GET_ORGANIZATION_ALLOWED_ACTIONS = "GET_ORGANIZATION_ALLOWED_ACTIONS"; export const GET_POOL_ALLOWED_ACTIONS = "GET_POOL_ALLOWED_ACTIONS"; export const GET_RESOURCE_ALLOWED_ACTIONS = "GET_RESOURCE_ALLOWED_ACTIONS"; export const SET_ALLOWED_ACTIONS = "SET_ALLOWED_ACTIONS"; diff --git a/ngui/ui/src/api/auth/handlers.ts b/ngui/ui/src/api/auth/handlers.ts index 8ef32141..82a283c9 100644 --- a/ngui/ui/src/api/auth/handlers.ts +++ b/ngui/ui/src/api/auth/handlers.ts @@ -1,16 +1,5 @@ import { GET_TOKEN, SET_TOKEN } from "./actionTypes"; -export const onSuccessGetToken = - ({ isTokenTemporary }) => - (data) => ({ - type: SET_TOKEN, - payload: { - ...data, - isTokenTemporary - }, - label: GET_TOKEN - }); - export const onSuccessSignIn = (data) => ({ type: SET_TOKEN, payload: data, diff --git a/ngui/ui/src/api/auth/index.ts b/ngui/ui/src/api/auth/index.ts index 31365acd..b6a237d1 100644 --- a/ngui/ui/src/api/auth/index.ts +++ b/ngui/ui/src/api/auth/index.ts @@ -1,6 +1,5 @@ import { createUser, - getOrganizationAllowedActions, getPoolAllowedActions, getToken, getUser, @@ -15,7 +14,6 @@ export { createUser, getToken, getUser, - getOrganizationAllowedActions, getPoolAllowedActions, resetPassword, getResourceAllowedActions, diff --git a/ngui/ui/src/api/auth/reducer.ts b/ngui/ui/src/api/auth/reducer.ts index cb1d494e..a536fd92 100644 --- a/ngui/ui/src/api/auth/reducer.ts +++ b/ngui/ui/src/api/auth/reducer.ts @@ -6,7 +6,7 @@ export const AUTH = "auth"; const reducer = (state = {}, action) => { switch (action.type) { case SET_TOKEN: { - const { token, user_id: userId, user_email: userEmail, isTokenTemporary } = action.payload; + const { token, user_id: userId, user_email: userEmail } = action.payload; const caveats = macaroon.processCaveats(macaroon.deserialize(token).getCaveats()); @@ -15,11 +15,7 @@ const reducer = (state = {}, action) => { [action.label]: { userId, userEmail, - /** - * The use of a temporary token is a security measure to ensure that users update their passwords before gaining full access to the application. - * This prevents users from accessing other parts of the application until their password has been successfully changed. - */ - [isTokenTemporary ? "temporaryToken" : "token"]: token, + token, ...caveats } }; diff --git a/ngui/ui/src/api/index.ts b/ngui/ui/src/api/index.ts index 09e4a5f4..93772fab 100644 --- a/ngui/ui/src/api/index.ts +++ b/ngui/ui/src/api/index.ts @@ -3,7 +3,6 @@ import { createUser, getToken, getUser, - getOrganizationAllowedActions, getPoolAllowedActions, resetPassword, getResourceAllowedActions, @@ -15,21 +14,16 @@ import { AUTH } from "./auth/reducer"; import { updateUserAssignment, getJiraOrganizationStatus } from "./jira_bus"; import { JIRA_BUS } from "./jira_bus/reducer"; import { - getOrganizationFeatures, getOrganizationOptions, getOrganizationOption, updateOrganizationOption, createOrganizationOption, deleteOrganizationOption, getOrganizationConstraints, - createDataSource, getPool, createAssignmentRule, - disconnectDataSource, updateDataSource, createPool, - createOrganization, - getOrganizations, getOrganizationsOverview, getPoolExpenses, getCloudsExpenses, @@ -38,7 +32,6 @@ import { uploadCodeReport, submitForAudit, getInvitation, - updateInvitation, createInvitations, updatePool, deletePool, @@ -48,7 +41,6 @@ import { getAuthorizedEmployees, getEmployees, getOrganizationExpenses, - getCurrentEmployee, getRawExpenses, getCleanExpenses, getExpensesSummary, @@ -72,7 +64,6 @@ import { getResourceLimitHits, getOptimizationsOverview, updateOptimizations, - getDataSources, getLiveDemo, createLiveDemo, getTtlAnalysis, @@ -118,8 +109,6 @@ import { deleteCalendarSynchronization, updateEnvironmentProperty, updateOrganization, - deleteOrganization, - getInvitations, deleteEmployee, updatePoolPolicyActivity, createDailyExpenseLimitResourceConstraint, @@ -141,9 +130,6 @@ import { getArchivedOptimizationsCount, getArchivedOptimizationsBreakdown, getArchivedOptimizationDetails, - getOrganizationThemeSettings, - getOrganizationPerspectives, - updateOrganizationPerspectives, updateEnvironmentSshRequirement, getMlTasks, getMlLeaderboardTemplate, @@ -193,7 +179,6 @@ import { createOrganizationGemini, getGemini, getS3DuplicatesOrganizationSettings, - updateOrganizationThemeSettings, getPowerSchedules, createPowerSchedule, getPowerSchedule, @@ -242,25 +227,19 @@ export { resetTtl, getToken, getUser, - getOrganizationAllowedActions, getPoolAllowedActions, resetPassword, - getOrganizationFeatures, getOrganizationOptions, getOrganizationOption, updateOrganizationOption, createOrganizationOption, deleteOrganizationOption, getOrganizationConstraints, - createDataSource, getPool, createAssignmentRule, - disconnectDataSource, updateDataSource, createUser, createPool, - createOrganization, - getOrganizations, getOrganizationsOverview, getPoolExpenses, getCloudsExpenses, @@ -269,7 +248,6 @@ export { uploadCodeReport, submitForAudit, getInvitation, - updateInvitation, createInvitations, updatePool, deletePool, @@ -279,7 +257,6 @@ export { getAuthorizedEmployees, getEmployees, getOrganizationExpenses, - getCurrentEmployee, getRawExpenses, getCleanExpenses, getExpensesSummary, @@ -303,7 +280,6 @@ export { getResourceLimitHits, getOptimizationsOverview, updateOptimizations, - getDataSources, getLiveDemo, createLiveDemo, getTtlAnalysis, @@ -349,8 +325,6 @@ export { deleteCalendarSynchronization, updateEnvironmentProperty, updateOrganization, - deleteOrganization, - getInvitations, signIn, deleteEmployee, updatePoolPolicyActivity, @@ -375,9 +349,6 @@ export { getArchivedOptimizationsCount, getArchivedOptimizationsBreakdown, getArchivedOptimizationDetails, - getOrganizationThemeSettings, - getOrganizationPerspectives, - updateOrganizationPerspectives, updateEnvironmentSshRequirement, getMlTasks, getMlLeaderboardTemplate, @@ -434,7 +405,6 @@ export { attachInstancesToSchedule, removeInstancesFromSchedule, updateMlLeaderboardTemplate, - updateOrganizationThemeSettings, createSurvey, getMlTaskRunsBulk, getMlLeaderboards, diff --git a/ngui/ui/src/api/restapi/actionCreators.ts b/ngui/ui/src/api/restapi/actionCreators.ts index f01c81d4..ffb6c1a2 100644 --- a/ngui/ui/src/api/restapi/actionCreators.ts +++ b/ngui/ui/src/api/restapi/actionCreators.ts @@ -3,8 +3,6 @@ import { MINUTE, HALF_HOUR, HOUR, ERROR_HANDLER_TYPE_LOCAL, SUCCESS_HANDLER_TYPE import { apiAction, getApiUrl, hashParams } from "api/utils"; import { DAILY_EXPENSE_LIMIT, TOTAL_EXPENSE_LIMIT, TTL } from "utils/constraints"; import { - GET_ORGANIZATION_FEATURES, - SET_ORGANIZATION_FEATURES, GET_ORGANIZATION_OPTIONS, SET_ORGANIZATION_OPTIONS, GET_ORGANIZATION_OPTION, @@ -14,15 +12,10 @@ import { UPDATE_ORGANIZATION_OPTION, CREATE_ORGANIZATION_OPTION, SET_ORGANIZATION_OPTION, - CREATE_DATA_SOURCE, - GET_POOL, - DELETE_DATA_SOURCE, UPDATE_DATA_SOURCE, SET_POOL, UPDATE_POOL, DELETE_POOL, - GET_ORGANIZATIONS, - SET_ORGANIZATIONS, GET_ORGANIZATIONS_OVERVIEW, SET_ORGANIZATIONS_OVERVIEW, CREATE_POOL, @@ -38,7 +31,6 @@ import { SUBMIT_FOR_AUDIT, GET_INVITATION, SET_INVITATION, - UPDATE_INVITATION, CREATE_INVITATIONS, GET_SPLIT_RESOURCES, SET_SPLIT_RESOURCES, @@ -51,8 +43,6 @@ import { DELETE_EMPLOYEE, SET_AUTHORIZED_EMPLOYEES, SET_EMPLOYEES, - GET_CURRENT_EMPLOYEE, - CREATE_ORGANIZATION, GET_ORGANIZATION_EXPENSES, SET_ORGANIZATION_EXPENSES, GET_RAW_EXPENSES, @@ -148,9 +138,6 @@ import { DELETE_CALENDAR_SYNCHRONIZATION, UPDATE_ENVIRONMENT_PROPERTY, UPDATE_ORGANIZATION, - DELETE_ORGANIZATION, - SET_INVITATIONS, - GET_INVITATIONS, CREATE_DAILY_EXPENSE_LIMIT_RESOURCE_CONSTRAINT, UPDATE_DAILY_EXPENSE_LIMIT_RESOURCE_CONSTRAINT, SET_RESOURCE_COUNT_BREAKDOWN, @@ -187,12 +174,6 @@ import { SET_ARCHIVED_OPTIMIZATION_DETAILS, SET_K8S_RIGHTSIZING, GET_K8S_RIGHTSIZING, - UPDATE_ORGANIZATION_THEME_SETTINGS, - SET_ORGANIZATION_THEME_SETTINGS, - GET_ORGANIZATION_THEME_SETTINGS, - SET_ORGANIZATION_PERSPECTIVES, - GET_ORGANIZATION_PERSPECTIVES, - UPDATE_ORGANIZATION_PERSPECTIVES, UPDATE_ENVIRONMENT_SSH_REQUIREMENT, GET_ML_TASKS, SET_ML_TASKS, @@ -254,8 +235,6 @@ import { GET_ML_RUNSETS_RUNS, SET_ML_RUNSET_EXECUTORS, GET_ML_RUNSET_EXECUTORS, - SET_DATA_SOURCES, - GET_DATA_SOURCES, STOP_ML_RUNSET, SET_ORGANIZATION_BI_EXPORTS, GET_ORGANIZATION_BI_EXPORT, @@ -328,13 +307,12 @@ import { SET_ML_TASK_TAGS, GET_ML_TASK_TAGS, RESTORE_PASSWORD, - VERIFY_EMAIL + VERIFY_EMAIL, + GET_POOL } from "./actionTypes"; import { onUpdateOrganizationOption, - onSuccessUpdateInvitation, onSuccessDeletePool, - onSuccessGetCurrentEmployee, onSuccessCreatePoolPolicy, onSuccessCreateResourceConstraint, onSuccessDeleteResourceConstraint, @@ -353,9 +331,6 @@ import { onSuccessUpdateGlobalPoolPolicyLimit, onSuccessUpdateGlobalPoolPolicyActivity, onSuccessUpdateGlobalResourceConstraintLimit, - onUpdateOrganizationThemeSettings, - onUpdateOrganizationPerspectives, - onSuccessCreateOrganization, onSuccessUpdateEnvironmentSshRequirement, onUpdateMlTask, onSuccessGetOptimizationsOverview, @@ -373,47 +348,6 @@ import { export const API_URL = getApiUrl("restapi"); -export const getOrganizationFeatures = (organizationId) => - apiAction({ - url: `${API_URL}/organizations/${organizationId}/options/features`, - method: "GET", - ttl: HOUR, - onSuccess: handleSuccess(SET_ORGANIZATION_FEATURES), - hash: hashParams(organizationId), - label: GET_ORGANIZATION_FEATURES - }); - -export const getOrganizationThemeSettings = (organizationId) => - apiAction({ - url: `${API_URL}/organizations/${organizationId}/options/theme_settings`, - method: "GET", - ttl: HOUR, - onSuccess: handleSuccess(SET_ORGANIZATION_THEME_SETTINGS), - hash: hashParams(organizationId), - label: GET_ORGANIZATION_THEME_SETTINGS - }); - -export const getOrganizationPerspectives = (organizationId) => - apiAction({ - url: `${API_URL}/organizations/${organizationId}/options/perspectives`, - method: "GET", - ttl: HOUR, - onSuccess: handleSuccess(SET_ORGANIZATION_PERSPECTIVES), - hash: hashParams(organizationId), - label: GET_ORGANIZATION_PERSPECTIVES - }); - -export const updateOrganizationPerspectives = (organizationId, value) => - apiAction({ - url: `${API_URL}/organizations/${organizationId}/options/perspectives`, - method: "PATCH", - onSuccess: onUpdateOrganizationPerspectives, - label: UPDATE_ORGANIZATION_PERSPECTIVES, - params: { - value: JSON.stringify(value) - } - }); - export const getOrganizationOptions = (organizationId, withValues = false) => apiAction({ url: `${API_URL}/organizations/${organizationId}/options`, @@ -458,17 +392,6 @@ export const updateOrganizationOption = (organizationId, name, value) => } }); -export const updateOrganizationThemeSettings = (organizationId, value) => - apiAction({ - url: `${API_URL}/organizations/${organizationId}/options/theme_settings`, - method: "PATCH", - onSuccess: onUpdateOrganizationThemeSettings, - label: UPDATE_ORGANIZATION_THEME_SETTINGS, - params: { - value: JSON.stringify(value) - } - }); - // Creating an option via PATCH is correct export const createOrganizationOption = (organizationId, name, value) => apiAction({ @@ -529,36 +452,6 @@ export const updateOrganizationConstraint = (id, params) => params }); -export const getDataSources = (organizationId) => - apiAction({ - url: `${API_URL}/organizations/${organizationId}/cloud_accounts`, - method: "GET", - onSuccess: handleSuccess(SET_DATA_SOURCES), - label: GET_DATA_SOURCES, - hash: hashParams(organizationId), - ttl: 2 * MINUTE, - params: { - details: true - } - }); - -export const createDataSource = (organizationId, params) => - apiAction({ - url: `${API_URL}/organizations/${organizationId}/cloud_accounts`, - method: "POST", - affectedRequests: [GET_DATA_SOURCES, GET_AVAILABLE_FILTERS], - label: CREATE_DATA_SOURCE, - params - }); - -export const disconnectDataSource = (id) => - apiAction({ - url: `${API_URL}/cloud_accounts/${id}`, - method: "DELETE", - affectedRequests: [GET_DATA_SOURCES, GET_AVAILABLE_FILTERS], - label: DELETE_DATA_SOURCE - }); - export const uploadCloudReport = (cloudAccountId, file) => apiAction({ url: `${API_URL}/cloud_accounts/${cloudAccountId}/report_upload`, @@ -613,15 +506,6 @@ export const getPool = (poolId, children = false, details = false) => } }); -export const createOrganization = (name) => - apiAction({ - url: `${API_URL}/organizations`, - method: "POST", - onSuccess: onSuccessCreateOrganization, - label: CREATE_ORGANIZATION, - params: { name } - }); - export const createPool = (organizationId, params) => apiAction({ url: `${API_URL}/organizations/${organizationId}/pools`, @@ -672,29 +556,11 @@ export const deletePool = (id) => label: DELETE_POOL }); -export const getOrganizations = () => - apiAction({ - url: `${API_URL}/organizations`, - method: "GET", - onSuccess: handleSuccess(SET_ORGANIZATIONS), - ttl: HALF_HOUR, - label: GET_ORGANIZATIONS - }); - -export const deleteOrganization = (organizationId) => - apiAction({ - url: `${API_URL}/organizations/${organizationId}`, - method: "DELETE", - label: DELETE_ORGANIZATION, - affectedRequests: [GET_ORGANIZATIONS] - }); - export const updateOrganization = (organizationId, params) => apiAction({ url: `${API_URL}/organizations/${organizationId}`, method: "PATCH", label: UPDATE_ORGANIZATION, - affectedRequests: [GET_ORGANIZATIONS], params }); @@ -774,26 +640,6 @@ export const getInvitation = (inviteId) => label: GET_INVITATION }); -export const getInvitations = () => - apiAction({ - url: `${API_URL}/invites`, - method: "GET", - onSuccess: handleSuccess(SET_INVITATIONS), - label: GET_INVITATIONS, - ttl: HALF_HOUR - }); - -export const updateInvitation = (inviteId, action) => - apiAction({ - url: `${API_URL}/invites/${inviteId}`, - method: "PATCH", - onSuccess: onSuccessUpdateInvitation, - entityId: inviteId, - label: UPDATE_INVITATION, - affectedRequests: [GET_ORGANIZATIONS, GET_INVITATIONS], - params: { action } - }); - export const splitResources = (organizationId, ids) => apiAction({ url: `${API_URL}/organizations/${organizationId}/split_resources/assign`, @@ -871,19 +717,6 @@ export const deleteEmployee = (employeeId, { newOwnerId }) => } }); -export const getCurrentEmployee = (organizationId) => - apiAction({ - url: `${API_URL}/organizations/${organizationId}/employees`, - method: "GET", - onSuccess: onSuccessGetCurrentEmployee, - label: GET_CURRENT_EMPLOYEE, - ttl: HALF_HOUR, - hash: hashParams(organizationId), - params: { - current_only: true - } - }); - export const getOrganizationExpenses = (organizationId) => apiAction({ url: `${API_URL}/organizations/${organizationId}/pool_expenses`, diff --git a/ngui/ui/src/api/restapi/actionTypes.ts b/ngui/ui/src/api/restapi/actionTypes.ts index 8f70dd06..1a1fd8aa 100644 --- a/ngui/ui/src/api/restapi/actionTypes.ts +++ b/ngui/ui/src/api/restapi/actionTypes.ts @@ -1,14 +1,3 @@ -export const GET_ORGANIZATION_FEATURES = "GET_ORGANIZATION_FEATURES"; -export const SET_ORGANIZATION_FEATURES = "SET_ORGANIZATION_FEATURES"; -export const UPDATE_ORGANIZATION_PERSPECTIVES = "UPDATE_ORGANIZATION_PERSPECTIVES"; - -export const SET_ORGANIZATION_PERSPECTIVES = "SET_ORGANIZATION_PERSPECTIVES"; -export const GET_ORGANIZATION_PERSPECTIVES = "GET_ORGANIZATION_PERSPECTIVES"; - -export const GET_ORGANIZATION_THEME_SETTINGS = "GET_ORGANIZATION_THEME_SETTINGS"; -export const SET_ORGANIZATION_THEME_SETTINGS = "SET_ORGANIZATION_THEME_SETTINGS"; -export const UPDATE_ORGANIZATION_THEME_SETTINGS = "UPDATE_ORGANIZATION_THEME_SETTINGS"; - export const GET_ORGANIZATION_OPTIONS = "GET_ORGANIZATION_OPTIONS"; export const SET_ORGANIZATION_OPTIONS = "SET_ORGANIZATION_OPTIONS"; @@ -18,11 +7,6 @@ export const UPDATE_ORGANIZATION_OPTION = "UPDATE_ORGANIZATION_OPTION"; export const CREATE_ORGANIZATION_OPTION = "CREATE_ORGANIZATION_OPTION"; export const SET_ORGANIZATION_OPTION = "SET_ORGANIZATION_OPTION"; -export const GET_DATA_SOURCES = "GET_DATA_SOURCES"; -export const SET_DATA_SOURCES = "SET_DATA_SOURCES"; - -export const CREATE_DATA_SOURCE = "CREATE_DATA_SOURCE"; -export const DELETE_DATA_SOURCE = "DELETE_DATA_SOURCE"; export const UPDATE_DATA_SOURCE = "UPDATE_DATA_SOURCE"; export const GET_POOL = "GET_POOL"; @@ -30,11 +14,6 @@ export const SET_POOL = "SET_POOL"; export const CREATE_POOL = "CREATE_POOL"; -export const CREATE_ORGANIZATION = "CREATE_ORGANIZATION"; - -export const GET_ORGANIZATIONS = "GET_ORGANIZATIONS"; -export const SET_ORGANIZATIONS = "SET_ORGANIZATIONS"; - export const GET_ORGANIZATION_CONSTRAINTS = "GET_ORGANIZATION_CONSTRAINTS"; export const SET_ORGANIZATION_CONSTRAINTS = "SET_ORGANIZATION_CONSTRAINTS"; export const CREATE_ORGANIZATION_CONSTRAINT = "CREATE_ORGANIZATION_CONSTRAINT"; @@ -44,7 +23,6 @@ export const DELETE_ORGANIZATION_CONSTRAINT = "DELETE_ORGANIZATION_CONSTRAINT"; export const UPDATE_ORGANIZATION_CONSTRAINT = "UPDATE_ORGANIZATION_CONSTRAINT"; export const UPDATE_ORGANIZATION = "UPDATE_ORGANIZATION"; -export const DELETE_ORGANIZATION = "DELETE_ORGANIZATION"; export const GET_ORGANIZATIONS_OVERVIEW = "GET_ORGANIZATIONS_OVERVIEW"; export const SET_ORGANIZATIONS_OVERVIEW = "SET_ORGANIZATIONS_OVERVIEW"; @@ -84,9 +62,6 @@ export const SET_INVITATION = "SET_INVITATION"; export const UPDATE_INVITATION = "UPDATE_INVITATION"; export const CREATE_INVITATIONS = "CREATE_INVITATIONS"; -export const GET_INVITATIONS = "GET_INVITATIONS"; -export const SET_INVITATIONS = "SET_INVITATIONS"; - export const GET_SPLIT_RESOURCES = "GET_SPLIT_RESOURCES"; export const SET_SPLIT_RESOURCES = "SET_SPLIT_RESOURCES"; @@ -103,9 +78,6 @@ export const GET_EMPLOYEES = "GET_EMPLOYEES"; export const SET_EMPLOYEES = "SET_EMPLOYEES"; export const DELETE_EMPLOYEE = "DELETE_EMPLOYEE"; -export const SET_CURRENT_EMPLOYEE = "SET_CURRENT_EMPLOYEE"; -export const GET_CURRENT_EMPLOYEE = "GET_CURRENT_EMPLOYEE"; - export const ASSIGNMENT_REQUEST_UPDATE = "ASSIGNMENT_REQUEST_UPDATE"; export const GET_ORGANIZATION_EXPENSES = "GET_ORGANIZATION_EXPENSES"; diff --git a/ngui/ui/src/api/restapi/handlers.ts b/ngui/ui/src/api/restapi/handlers.ts index 7f6c5381..9d861349 100644 --- a/ngui/ui/src/api/restapi/handlers.ts +++ b/ngui/ui/src/api/restapi/handlers.ts @@ -5,8 +5,6 @@ import { SET_INVITATION, GET_POOL, DELETE_POOL, - GET_CURRENT_EMPLOYEE, - SET_CURRENT_EMPLOYEE, GET_ASSIGNMENT_RULES, SET_POOL_POLICY, GET_POOL_POLICIES, @@ -35,11 +33,6 @@ import { UPDATE_GLOBAL_POOL_POLICY, GET_GLOBAL_RESOURCE_CONSTRAINTS, UPDATE_GLOBAL_RESOURCE_CONSTRAINT, - UPDATE_ORGANIZATION_THEME_SETTINGS, - GET_ORGANIZATION_THEME_SETTINGS, - UPDATE_ORGANIZATION_PERSPECTIVES, - GET_ORGANIZATION_PERSPECTIVES, - CREATE_ORGANIZATION, UPDATE_ENVIRONMENT_SSH_REQUIREMENT, GET_ML_TASK, SET_ML_TASK, @@ -78,24 +71,12 @@ export const onSuccessUpdateInvitation = () => ({ label: GET_INVITATION }); -export const onSuccessCreateOrganization = (data) => ({ - type: CREATE_ORGANIZATION, - payload: data, - label: CREATE_ORGANIZATION -}); - export const onSuccessDeletePool = (id) => () => ({ type: DELETE_POOL, payload: { id }, label: GET_POOL }); -export const onSuccessGetCurrentEmployee = ({ employees = [] }) => ({ - type: SET_CURRENT_EMPLOYEE, - payload: employees[0], - label: GET_CURRENT_EMPLOYEE -}); - export const onSuccessCreatePoolPolicy = (data) => ({ type: SET_POOL_POLICY, payload: data, @@ -231,18 +212,6 @@ export const onSuccessUpdateAnomaly = (data) => ({ label: GET_ORGANIZATION_CONSTRAINT }); -export const onUpdateOrganizationThemeSettings = (data) => ({ - type: UPDATE_ORGANIZATION_THEME_SETTINGS, - payload: data, - label: GET_ORGANIZATION_THEME_SETTINGS -}); - -export const onUpdateOrganizationPerspectives = (data) => ({ - type: UPDATE_ORGANIZATION_PERSPECTIVES, - payload: data, - label: GET_ORGANIZATION_PERSPECTIVES -}); - export const onUpdateMlTask = (data) => ({ type: SET_ML_TASK, payload: data, diff --git a/ngui/ui/src/api/restapi/index.ts b/ngui/ui/src/api/restapi/index.ts index 56de3d85..a537b6c3 100644 --- a/ngui/ui/src/api/restapi/index.ts +++ b/ngui/ui/src/api/restapi/index.ts @@ -1,19 +1,14 @@ import { - getOrganizationFeatures, getOrganizationOptions, getOrganizationOption, getOrganizationConstraints, updateOrganizationOption, createOrganizationOption, deleteOrganizationOption, - createDataSource, - disconnectDataSource, updateDataSource, getPool, createAssignmentRule, createPool, - createOrganization, - getOrganizations, getOrganizationsOverview, getPoolExpenses, getCloudsExpenses, @@ -22,7 +17,6 @@ import { uploadCodeReport, submitForAudit, getInvitation, - updateInvitation, createInvitations, updatePool, deletePool, @@ -31,7 +25,6 @@ import { getPoolOwners, getAuthorizedEmployees, getEmployees, - getCurrentEmployee, getOrganizationExpenses, getRawExpenses, getCleanExpenses, @@ -56,7 +49,6 @@ import { getResourceLimitHits, getOptimizationsOverview, updateOptimizations, - getDataSources, getLiveDemo, createLiveDemo, getTtlAnalysis, @@ -103,8 +95,6 @@ import { deleteCalendarSynchronization, updateEnvironmentProperty, updateOrganization, - deleteOrganization, - getInvitations, deleteEmployee, updatePoolPolicyActivity, createDailyExpenseLimitResourceConstraint, @@ -127,9 +117,6 @@ import { getArchivedOptimizationsCount, getArchivedOptimizationsBreakdown, getArchivedOptimizationDetails, - getOrganizationThemeSettings, - getOrganizationPerspectives, - updateOrganizationPerspectives, updateEnvironmentSshRequirement, getMlTasks, getMlLeaderboardTemplate, @@ -178,7 +165,6 @@ import { createOrganizationGemini, getGemini, getS3DuplicatesOrganizationSettings, - updateOrganizationThemeSettings, createSurvey, getPowerSchedules, createPowerSchedule, @@ -220,21 +206,16 @@ import { } from "./actionCreators"; export { - getOrganizationFeatures, getOrganizationOptions, getOrganizationOption, getOrganizationConstraints, updateOrganizationOption, createOrganizationOption, deleteOrganizationOption, - createDataSource, getPool, createAssignmentRule, - disconnectDataSource, updateDataSource, createPool, - createOrganization, - getOrganizations, getOrganizationsOverview, getPoolExpenses, getCloudsExpenses, @@ -243,7 +224,6 @@ export { uploadCodeReport, submitForAudit, getInvitation, - updateInvitation, createInvitations, updatePool, deletePool, @@ -252,7 +232,6 @@ export { getPoolOwners, getAuthorizedEmployees, getEmployees, - getCurrentEmployee, getOrganizationExpenses, getRawExpenses, getCleanExpenses, @@ -277,7 +256,6 @@ export { getResourceLimitHits, getOptimizationsOverview, updateOptimizations, - getDataSources, getLiveDemo, createLiveDemo, getTtlAnalysis, @@ -324,8 +302,6 @@ export { deleteCalendarSynchronization, updateEnvironmentProperty, updateOrganization, - deleteOrganization, - getInvitations, deleteEmployee, updatePoolPolicyActivity, createDailyExpenseLimitResourceConstraint, @@ -348,9 +324,6 @@ export { getArchivedOptimizationsCount, getArchivedOptimizationsBreakdown, getArchivedOptimizationDetails, - getOrganizationThemeSettings, - getOrganizationPerspectives, - updateOrganizationPerspectives, updateEnvironmentSshRequirement, getMlTasks, getMlLeaderboardTemplate, @@ -400,7 +373,6 @@ export { createOrganizationGemini, getGemini, getS3DuplicatesOrganizationSettings, - updateOrganizationThemeSettings, createSurvey, getPowerSchedules, createPowerSchedule, diff --git a/ngui/ui/src/api/restapi/reducer.ts b/ngui/ui/src/api/restapi/reducer.ts index 36652540..0ec6f706 100644 --- a/ngui/ui/src/api/restapi/reducer.ts +++ b/ngui/ui/src/api/restapi/reducer.ts @@ -1,13 +1,10 @@ import { reformatBreakdown } from "utils/api"; import { removeObjects } from "utils/arrays"; import { - SET_ORGANIZATION_FEATURES, SET_ORGANIZATION_OPTIONS, SET_ORGANIZATION_OPTION, SET_ORGANIZATION_CONSTRAINTS, SET_POOL, - SET_DATA_SOURCES, - SET_ORGANIZATIONS, SET_ORGANIZATIONS_OVERVIEW, SET_POOL_EXPENSES_BREAKDOWN, SET_CLOUDS_EXPENSES, @@ -19,7 +16,6 @@ import { SET_AUTHORIZED_EMPLOYEES, SET_EMPLOYEES, SET_ORGANIZATION_EXPENSES, - SET_CURRENT_EMPLOYEE, SET_RAW_EXPENSES, SET_CLEAN_EXPENSES, SET_EXPENSES_SUMMARY, @@ -55,7 +51,6 @@ import { SET_OPTIMIZATION_OPTIONS, SET_ORGANIZATION_CALENDAR, UPDATE_ENVIRONMENT_PROPERTY, - SET_INVITATIONS, UPDATE_SSH_KEY, SET_RESOURCE_COUNT_BREAKDOWN, SET_TAGS_BREAKDOWN, @@ -74,11 +69,6 @@ import { SET_ARCHIVED_OPTIMIZATION_DETAILS, SET_K8S_RIGHTSIZING, DELETE_POOL, - UPDATE_ORGANIZATION_THEME_SETTINGS, - SET_ORGANIZATION_THEME_SETTINGS, - SET_ORGANIZATION_PERSPECTIVES, - UPDATE_ORGANIZATION_PERSPECTIVES, - CREATE_ORGANIZATION, UPDATE_ENVIRONMENT_SSH_REQUIREMENT, SET_ML_TASKS, SET_ML_LEADERBOARD_TEMPLATE, @@ -136,18 +126,6 @@ export const RESTAPI = "restapi"; const reducer = (state = {}, action) => { switch (action.type) { - case SET_ORGANIZATION_FEATURES: { - return { - ...state, - [action.label]: action.payload - }; - } - case SET_ORGANIZATION_THEME_SETTINGS: { - return { - ...state, - [action.label]: action.payload - }; - } case SET_ORGANIZATION_OPTIONS: { return { ...state, @@ -160,14 +138,6 @@ const reducer = (state = {}, action) => { [action.label]: action.payload.value }; } - case SET_DATA_SOURCES: { - return { - ...state, - [action.label]: { - cloudAccounts: action.payload.cloud_accounts - } - }; - } case UPDATE_POOL_EXPENSES_EXPORT: { return { ...state, @@ -229,26 +199,6 @@ const reducer = (state = {}, action) => { } }; } - case SET_CURRENT_EMPLOYEE: { - return { - ...state, - [action.label]: { - currentEmployee: action.payload - } - }; - } - case CREATE_ORGANIZATION: { - return { - ...state, - [action.label]: action.payload - }; - } - case SET_ORGANIZATIONS: { - return { - ...state, - [action.label]: action.payload - }; - } case SET_ORGANIZATIONS_OVERVIEW: { return { ...state, @@ -324,11 +274,6 @@ const reducer = (state = {}, action) => { invitation: action.payload } }; - case SET_INVITATIONS: - return { - ...state, - [action.label]: action.payload.invites - }; case SET_RAW_EXPENSES: { return { ...state, @@ -612,17 +557,6 @@ const reducer = (state = {}, action) => { ...state, [action.label]: action.payload }; - case SET_ORGANIZATION_PERSPECTIVES: - return { - ...state, - [action.label]: action.payload - }; - case UPDATE_ORGANIZATION_PERSPECTIVES: { - return { - ...state, - [action.label]: action.payload - }; - } case UPDATE_ENVIRONMENT_PROPERTY: { return { ...state, @@ -712,12 +646,6 @@ const reducer = (state = {}, action) => { [action.label]: action.payload }; } - case UPDATE_ORGANIZATION_THEME_SETTINGS: { - return { - ...state, - [action.label]: action.payload - }; - } case SET_ML_TASKS: { return { ...state, diff --git a/ngui/ui/src/components/AcceptInvitations/AcceptInvitations.test.tsx b/ngui/ui/src/components/AcceptInvitations/AcceptInvitations.test.tsx deleted file mode 100644 index 5020e805..00000000 --- a/ngui/ui/src/components/AcceptInvitations/AcceptInvitations.test.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoot } from "react-dom/client"; -import TestProvider from "tests/TestProvider"; -import AcceptInvitations from "./AcceptInvitations"; - -it("renders without crashing", () => { - const div = document.createElement("div"); - const root = createRoot(div); - root.render( - - {}} />{" "} - - ); - root.unmount(); -}); diff --git a/ngui/ui/src/components/AcceptInvitations/AcceptInvitations.tsx b/ngui/ui/src/components/AcceptInvitations/AcceptInvitations.tsx deleted file mode 100644 index fed00f8a..00000000 --- a/ngui/ui/src/components/AcceptInvitations/AcceptInvitations.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import NavigationIcon from "@mui/icons-material/Navigation"; -import { Box, Stack } from "@mui/system"; -import { useDispatch } from "react-redux"; -import { getInvitations } from "api"; -import { GET_TOKEN } from "api/auth/actionTypes"; -import ButtonLoader from "components/ButtonLoader"; -import Invitations from "components/Invitations"; -import Logo from "components/Logo"; -import { getLoginRedirectionPath } from "containers/AuthorizationContainer/AuthorizationContainer"; -import { useApiData } from "hooks/useApiData"; -import { SPACING_6 } from "utils/layouts"; -import useStyles from "./AcceptInvitations.styles"; - -const AcceptInvitations = ({ invitations = [], activateScope, isLoadingProps = {} }) => { - const dispatch = useDispatch(); - const { classes } = useStyles(); - - const { - apiData: { userEmail } - } = useApiData(GET_TOKEN); - - const onSuccessAccept = () => dispatch(getInvitations()); - - const onSuccessDecline = () => dispatch(getInvitations()); - - const { - isGetInvitationsLoading = false, - isGetOrganizationsLoading = false, - isCreateOrganizationLoading = false, - isUpdateInvitationLoading = false - } = isLoadingProps; - - return ( - - - - - - - - - - activateScope(userEmail, { - getOnSuccessRedirectionPath: ({ userEmail: scopeUserEmail }) => getLoginRedirectionPath(scopeUserEmail) - }) - } - isLoading={isGetOrganizationsLoading || isCreateOrganizationLoading || isUpdateInvitationLoading} - startIcon={} - customWrapperClass={classes.dashboardButton} - /> - - - ); -}; - -export default AcceptInvitations; diff --git a/ngui/ui/src/components/AcceptInvitations/index.ts b/ngui/ui/src/components/AcceptInvitations/index.ts deleted file mode 100644 index 79a6a7aa..00000000 --- a/ngui/ui/src/components/AcceptInvitations/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import AcceptInvitations from "./AcceptInvitations"; - -export default AcceptInvitations; diff --git a/ngui/ui/src/components/ActivityListener/ActivityListener.ts b/ngui/ui/src/components/ActivityListener/ActivityListener.ts index 0f63d5bb..d5cc57b6 100644 --- a/ngui/ui/src/components/ActivityListener/ActivityListener.ts +++ b/ngui/ui/src/components/ActivityListener/ActivityListener.ts @@ -1,14 +1,12 @@ import { useEffect } from "react"; import { useLocation } from "react-router-dom"; -import { GET_TOKEN } from "api/auth/actionTypes"; -import { useApiData } from "hooks/useApiData"; +import { useGetToken } from "hooks/useGetToken"; import { useOrganizationInfo } from "hooks/useOrganizationInfo"; import { dropUserIdentificationIfUniqueIdChanged, identify, initializeHotjar, trackPage } from "utils/analytics"; const ActivityListener = () => { - const { - apiData: { userId } - } = useApiData(GET_TOKEN); + const { userId } = useGetToken(); + const { isDemo, organizationId } = useOrganizationInfo(); // hotjar init diff --git a/ngui/ui/src/components/ApolloProvider/ApolloProvider.tsx b/ngui/ui/src/components/ApolloProvider/ApolloProvider.tsx index 9ff842ff..5fb15028 100644 --- a/ngui/ui/src/components/ApolloProvider/ApolloProvider.tsx +++ b/ngui/ui/src/components/ApolloProvider/ApolloProvider.tsx @@ -5,9 +5,8 @@ import { getMainDefinition } from "@apollo/client/utilities"; import { type GraphQLError } from "graphql"; import { createClient } from "graphql-ws"; import { v4 as uuidv4 } from "uuid"; -import { GET_TOKEN } from "api/auth/actionTypes"; import { GET_ERROR } from "graphql/api/common"; -import { useApiData } from "hooks/useApiData"; +import { useGetToken } from "hooks/useGetToken"; import { getEnvironmentVariable } from "utils/env"; const httpBase = getEnvironmentVariable("VITE_APOLLO_HTTP_BASE"); @@ -23,9 +22,7 @@ const writeErrorToCache = (cache: DefaultContext, graphQLError: GraphQLError) => }; const ApolloClientProvider = ({ children }) => { - const { - apiData: { token } - } = useApiData(GET_TOKEN); + const { token } = useGetToken(); const httpLink = new HttpLink({ uri: `${httpBase}/api`, @@ -67,8 +64,8 @@ const ApolloClientProvider = ({ children }) => { ); const client = new ApolloClient({ - link: from([errorLink, splitLink]), - cache: new InMemoryCache() + cache: new InMemoryCache(), + link: from([errorLink, splitLink]) }); return {children}; diff --git a/ngui/ui/src/components/App/App.tsx b/ngui/ui/src/components/App/App.tsx index 34db5a59..a8a39000 100644 --- a/ngui/ui/src/components/App/App.tsx +++ b/ngui/ui/src/components/App/App.tsx @@ -1,10 +1,8 @@ import { Routes, Route, Navigate } from "react-router-dom"; -import { GET_TOKEN } from "api/auth/actionTypes"; import ErrorBoundary from "components/ErrorBoundary"; import LayoutWrapper from "components/LayoutWrapper"; import RoutePathContextProvider from "contexts/RoutePathContext/RoutePathContextProvider"; -import { useApiData } from "hooks/useApiData"; -import { useOrganizationIdQueryParameterListener } from "hooks/useOrganizationIdQueryParameterListener"; +import { useGetToken } from "hooks/useGetToken"; import { LOGIN, USER_EMAIL_QUERY_PARAMETER_NAME } from "urls"; import mainMenu from "utils/menus"; import { formQueryString, getPathname, getQueryParams } from "utils/network"; @@ -38,11 +36,7 @@ const LoginNavigation = () => { }; const RouteRender = ({ isTokenRequired, component, layout, context }) => { - const { - apiData: { token } - } = useApiData(GET_TOKEN); - - useOrganizationIdQueryParameterListener(); + const { token } = useGetToken(); // TODO: create a Page component and wrap each page explicitly with Redirector if (!token && isTokenRequired) { diff --git a/ngui/ui/src/components/CloudAccountDetails/CloudAccountDetails.tsx b/ngui/ui/src/components/CloudAccountDetails/CloudAccountDetails.tsx index f4d9e6ce..afffdfe7 100644 --- a/ngui/ui/src/components/CloudAccountDetails/CloudAccountDetails.tsx +++ b/ngui/ui/src/components/CloudAccountDetails/CloudAccountDetails.tsx @@ -5,7 +5,6 @@ import { Link } from "@mui/material"; import Grid from "@mui/material/Grid"; import { FormattedMessage } from "react-intl"; import { Link as RouterLink } from "react-router-dom"; -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; import ActionBar from "components/ActionBar"; import AdvancedDataSourceDetails from "components/AdvancedDataSourceDetails"; import DataSourceDetails from "components/DataSourceDetails"; @@ -22,7 +21,7 @@ import TabsWrapper from "components/TabsWrapper"; import DataSourceNodesContainer from "containers/DataSourceNodesContainer"; import DataSourceSkusContainer from "containers/DataSourceSkusContainer"; import UploadCloudReportDataContainer from "containers/UploadCloudReportDataContainer"; -import { useApiData } from "hooks/useApiData"; +import { useAllDataSources } from "hooks/coreData"; import { useDataSources } from "hooks/useDataSources"; import { useIsFeatureEnabled } from "hooks/useIsFeatureEnabled"; import { useOpenSideModal } from "hooks/useOpenSideModal"; @@ -352,13 +351,11 @@ const CloudAccountDetails = ({ data = {}, isLoading = false }) => { config = {} } = data; - const { - apiData: { cloudAccounts = [] } - } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); - const childrenAccounts = cloudAccounts.filter(({ parent_id: accountParentId }) => accountParentId === id); + const childrenDataSources = dataSources.filter(({ parent_id: accountParentId }) => accountParentId === id); - const childrenDetails = summarizeChildrenDetails(childrenAccounts); + const childrenDetails = summarizeChildrenDetails(childrenDataSources); const { cost = 0, diff --git a/ngui/ui/src/components/CloudAccountsOverview/CloudAccountsOverviewMocked.tsx b/ngui/ui/src/components/CloudAccountsOverview/CloudAccountsOverviewMocked.tsx index 53e0b6bd..ee7dce0c 100644 --- a/ngui/ui/src/components/CloudAccountsOverview/CloudAccountsOverviewMocked.tsx +++ b/ngui/ui/src/components/CloudAccountsOverview/CloudAccountsOverviewMocked.tsx @@ -8,7 +8,7 @@ const CloudAccountsOverviewMocked = () => ( { config: {}, details: { - tracked: 16, + resources: 16, last_month_cost: 18560.75036486765, forecast: 20110.78, cost: 12785.47 @@ -20,7 +20,7 @@ const CloudAccountsOverviewMocked = () => ( { config: {}, details: { - tracked: 610, + resources: 610, last_month_cost: 40120.98, forecast: 35270.79, cost: 28385.59 @@ -32,7 +32,7 @@ const CloudAccountsOverviewMocked = () => ( { config: {}, details: { - tracked: 5, + resources: 5, last_month_cost: 11750, forecast: 10750.8, cost: 6102.09 @@ -44,7 +44,7 @@ const CloudAccountsOverviewMocked = () => ( { config: {}, details: { - tracked: 5, + resources: 5, last_month_cost: 6500.5523346274, forecast: 7850.19, cost: 4334.18 @@ -56,7 +56,7 @@ const CloudAccountsOverviewMocked = () => ( { config: {}, details: { - tracked: 12, + resources: 12, last_month_cost: 5900.5523346274, forecast: 5226.19, cost: 2512.18 @@ -68,7 +68,7 @@ const CloudAccountsOverviewMocked = () => ( { config: {}, details: { - tracked: 125, + resources: 125, last_month_cost: 0, forecast: 203.6, cost: 203.59941599999996 diff --git a/ngui/ui/src/components/CloudAccountsTable/CloudAccountsTable.tsx b/ngui/ui/src/components/CloudAccountsTable/CloudAccountsTable.tsx index 8f5ce4c6..f7bf1e61 100644 --- a/ngui/ui/src/components/CloudAccountsTable/CloudAccountsTable.tsx +++ b/ngui/ui/src/components/CloudAccountsTable/CloudAccountsTable.tsx @@ -105,8 +105,8 @@ const CloudAccountsTable = ({ cloudAccounts = [], isLoading = false }) => { }, { header: intl.formatMessage({ id: "resourcesChargedThisMonth" }), - id: "details.tracked", - accessorFn: (originalRow) => originalRow.details?.tracked, + id: "details.resources", + accessorFn: (originalRow) => originalRow.details?.resources, emptyValue: "0" }, { diff --git a/ngui/ui/src/components/Dashboard/Dashboard.tsx b/ngui/ui/src/components/Dashboard/Dashboard.tsx index 02c8524d..44e48085 100644 --- a/ngui/ui/src/components/Dashboard/Dashboard.tsx +++ b/ngui/ui/src/components/Dashboard/Dashboard.tsx @@ -1,6 +1,5 @@ import Link from "@mui/material/Link"; import { FormattedMessage } from "react-intl"; -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; import AlertDialog from "components/AlertDialog"; import DashboardGridLayout from "components/DashboardGridLayout"; import MailTo from "components/MailTo"; @@ -14,7 +13,7 @@ import RecentModelsCardContainer from "containers/RecentModelsCardContainer"; import RecentTasksCardContainer from "containers/RecentTasksCardContainer"; import RecommendationsCardContainer from "containers/RecommendationsCardContainer"; import TopResourcesExpensesCardContainer from "containers/TopResourcesExpensesCardContainer"; -import { useApiData } from "hooks/useApiData"; +import { useAllDataSources } from "hooks/coreData"; import { useIsUpMediaQuery } from "hooks/useMediaQueries"; import { EMAIL_SUPPORT, DOCS_HYSTAX_OPTSCALE, SHOW_POLICY_QUERY_PARAM } from "urls"; import { ENVIRONMENT } from "utils/constants"; @@ -26,11 +25,9 @@ const Dashboard = () => { const startTour = useStartTour(); - const { - apiData: { cloudAccounts = [] } - } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); - const thereAreOnlyEnvironmentDataSources = cloudAccounts.every(({ type }) => type === ENVIRONMENT); + const thereAreOnlyEnvironmentDataSources = dataSources.every(({ type }) => type === ENVIRONMENT); const { isFinished } = useProductTour(PRODUCT_TOUR); diff --git a/ngui/ui/src/components/DataSourceDetails/ChildrenList/ChildrenList.tsx b/ngui/ui/src/components/DataSourceDetails/ChildrenList/ChildrenList.tsx index 60e230df..831b68db 100644 --- a/ngui/ui/src/components/DataSourceDetails/ChildrenList/ChildrenList.tsx +++ b/ngui/ui/src/components/DataSourceDetails/ChildrenList/ChildrenList.tsx @@ -1,29 +1,26 @@ import Typography from "@mui/material/Typography"; import { FormattedMessage } from "react-intl"; -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; import CloudLabel from "components/CloudLabel"; import SubTitle from "components/SubTitle"; -import { useApiData } from "hooks/useApiData"; +import { useAllDataSources } from "hooks/coreData"; import { isEmpty } from "utils/arrays"; const ChildrenList = ({ parentId }) => { - const { - apiData: { cloudAccounts = [] } - } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); - const childrenAccounts = cloudAccounts.filter(({ parent_id: accountParentId }) => accountParentId === parentId); + const childDataSources = dataSources.filter(({ parent_id: accountParentId }) => accountParentId === parentId); return ( <> - {isEmpty(childrenAccounts) ? ( + {isEmpty(childDataSources) ? ( ) : ( - childrenAccounts.map(({ id, name, type }) => ) + childDataSources.map(({ id, name, type }) => ) )} ); diff --git a/ngui/ui/src/components/DataSourceDetails/Properties/AzureProperties.tsx b/ngui/ui/src/components/DataSourceDetails/Properties/AzureProperties.tsx index 3c68a0eb..afe3ea5c 100644 --- a/ngui/ui/src/components/DataSourceDetails/Properties/AzureProperties.tsx +++ b/ngui/ui/src/components/DataSourceDetails/Properties/AzureProperties.tsx @@ -1,27 +1,27 @@ -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; import CloudLabel from "components/CloudLabel"; import KeyValueLabel from "components/KeyValueLabel/KeyValueLabel"; -import { useApiData } from "hooks/useApiData"; +import { useAllDataSources } from "hooks/coreData"; import { AZURE_CNR } from "utils/constants"; -const AzureProperties = ({ config, parentId }) => { - const { client_id: clientId, tenant, expense_import_scheme: expenseImportScheme, subscription_id: subscriptionId } = config; +const ParentDataSource = ({ parentDataSourceId }) => { + const dataSources = useAllDataSources(); + const { name, type } = dataSources.find((dataSource) => dataSource.id === parentDataSourceId) ?? {}; - const { - apiData: { cloudAccounts = [] } - } = useApiData(GET_DATA_SOURCES); + return ( + } + dataTestIds={{ key: "p_parent_data_source_key", value: "p_parent_data_source_value" }} + /> + ); +}; - const { name, type } = cloudAccounts.find((cloudAccount) => cloudAccount.id === parentId) ?? {}; +const AzureProperties = ({ config, parentId }) => { + const { client_id: clientId, tenant, expense_import_scheme: expenseImportScheme, subscription_id: subscriptionId } = config; return ( <> - {parentId && ( - } - dataTestIds={{ key: "p_parent_data_source_key", value: "p_parent_data_source_value" }} - /> - )} + {parentId && } {subscriptionId && ( { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [step, setStep] = useState(CONFIRM_VERIFICATION_CODE); + const [verificationCodeToken, setVerificationCodeToken] = useState<{ + user_id: string; + user_email: string; + token: string; + }>(); + const stepContent = { [CONFIRM_VERIFICATION_CODE]: ( - setStep(EMAIL_VERIFICATION_SUCCESS)} /> + { + setVerificationCodeToken(token); + setStep(EMAIL_VERIFICATION_SUCCESS); + }} + /> ), [EMAIL_VERIFICATION_SUCCESS]: ( @@ -28,7 +46,19 @@ const EmailVerification = () => {
- + { + const caveats = macaroon.processCaveats(macaroon.deserialize(verificationCodeToken.token).getCaveats()); + dispatch(initialize({ ...verificationCodeToken, caveats })); + navigate( + `${INITIALIZE}?${formQueryString({ + [SHOW_POLICY_QUERY_PARAM]: true + })}` + ); + }} + > diff --git a/ngui/ui/src/components/GenerateLiveDemo/GenerateLiveDemo.tsx b/ngui/ui/src/components/GenerateLiveDemo/GenerateLiveDemo.tsx index a987c05c..54421150 100644 --- a/ngui/ui/src/components/GenerateLiveDemo/GenerateLiveDemo.tsx +++ b/ngui/ui/src/components/GenerateLiveDemo/GenerateLiveDemo.tsx @@ -6,7 +6,13 @@ import Logo from "components/Logo"; import PageTitle from "components/PageTitle"; import { SPACING_4 } from "utils/layouts"; -const GenerateLiveDemo = ({ isLoading, retry, showRetry = false }) => ( +type GenerateLiveDemoProps = { + retry: () => void; + isLoading?: boolean; + showRetry?: boolean; +}; + +const GenerateLiveDemo = ({ retry, isLoading = false, showRetry = false }: GenerateLiveDemoProps) => ( diff --git a/ngui/ui/src/components/GoogleAuthButton/GoogleAuthButton.tsx b/ngui/ui/src/components/GoogleAuthButton/GoogleAuthButton.tsx index e450bb1c..d662742d 100644 --- a/ngui/ui/src/components/GoogleAuthButton/GoogleAuthButton.tsx +++ b/ngui/ui/src/components/GoogleAuthButton/GoogleAuthButton.tsx @@ -1,22 +1,21 @@ import ButtonLoader from "components/ButtonLoader"; -import { PROVIDERS } from "hooks/useNewAuthorization"; import GoogleIcon from "icons/GoogleIcon"; +import { AUTH_PROVIDERS } from "utils/constants"; import { getEnvironmentVariable } from "utils/env"; import { useGoogleLogin } from "./hooks"; -const GoogleAuthButton = ({ thirdPartySignIn, setIsAuthInProgress, isAuthInProgress, isRegistrationInProgress }) => { +const GoogleAuthButton = ({ handleSignIn, isLoading, disabled }) => { const clientId = getEnvironmentVariable("VITE_GOOGLE_OAUTH_CLIENT_ID"); - const { login, scriptLoadedSuccessfully } = useGoogleLogin({ - onSuccess: ({ code: token }) => thirdPartySignIn(PROVIDERS.GOOGLE, { token }), + const { login } = useGoogleLogin({ + onSuccess: ({ code: token }) => + handleSignIn({ provider: AUTH_PROVIDERS.GOOGLE, token, redirectUri: window.location.origin }), onError: (response = {}) => { - setIsAuthInProgress(false); const { message = "", type = "", ...rest } = response; - console.warn(`Google response failure ${message}: ${type}`, ...rest); + console.warn(`Google response failure ${message}: ${type}`, rest); }, clientId }); - const isLoading = isAuthInProgress || isRegistrationInProgress || !scriptLoadedSuccessfully; const environmentNotSet = !clientId; return ( @@ -24,18 +23,15 @@ const GoogleAuthButton = ({ thirdPartySignIn, setIsAuthInProgress, isAuthInProgr variant="outlined" messageId="google" size="medium" - onClick={() => { - setIsAuthInProgress(true); - login(); - }} + onClick={login} startIcon={} isLoading={isLoading} + disabled={disabled || environmentNotSet} fullWidth tooltip={{ show: environmentNotSet, messageId: "signInWithGoogleIsNotConfigured" }} - disabled={environmentNotSet} /> ); }; diff --git a/ngui/ui/src/components/MainMenu/MainMenu.tsx b/ngui/ui/src/components/MainMenu/MainMenu.tsx index f155298d..f9d1c5c2 100644 --- a/ngui/ui/src/components/MainMenu/MainMenu.tsx +++ b/ngui/ui/src/components/MainMenu/MainMenu.tsx @@ -4,10 +4,10 @@ import MenuGroupWrapper from "components/MenuGroupWrapper"; import MenuItem from "components/MenuItem"; import ModeWrapper from "components/ModeWrapper"; import { PRODUCT_TOUR, useProductTour, PRODUCT_TOUR_IDS } from "components/Tour"; -import { useOptScaleMode } from "hooks/useOptScaleMode"; +import { useGetOptscaleMode } from "hooks/coreData"; const SimpleItem = ({ menuItem }) => { - const optScaleMode = useOptScaleMode(); + const { optscaleMode } = useGetOptscaleMode(); return ( @@ -18,7 +18,7 @@ const SimpleItem = ({ menuItem }) => { messageId={ typeof menuItem.messageId === "function" ? menuItem.messageId({ - mode: optScaleMode + mode: optscaleMode }) : menuItem.messageId } diff --git a/ngui/ui/src/components/MicrosoftSignInButton/MicrosoftSignInButton.tsx b/ngui/ui/src/components/MicrosoftSignInButton/MicrosoftSignInButton.tsx index 581a8dda..2f8a756d 100644 --- a/ngui/ui/src/components/MicrosoftSignInButton/MicrosoftSignInButton.tsx +++ b/ngui/ui/src/components/MicrosoftSignInButton/MicrosoftSignInButton.tsx @@ -1,25 +1,22 @@ import { InteractionStatus } from "@azure/msal-browser"; import { useMsal } from "@azure/msal-react"; import ButtonLoader from "components/ButtonLoader"; -import { PROVIDERS } from "hooks/useNewAuthorization"; import MicrosoftIcon from "icons/MicrosoftIcon"; +import { AUTH_PROVIDERS } from "utils/constants"; import { microsoftOAuthConfiguration } from "utils/integrations"; -const handleClick = async (instance, callback, setIsAuthInProgress) => { +const handleClick = async (instance, callback) => { try { const { tenantId, idToken } = await instance.loginPopup({ prompt: "select_account" }); - callback(PROVIDERS.MICROSOFT, { token: idToken, tenant_id: tenantId }); + callback({ provider: AUTH_PROVIDERS.MICROSOFT, token: idToken, tenantId }); } catch (error) { console.log("Microsoft login failure ", error); - setIsAuthInProgress(false); } }; -const MicrosoftSignInButton = ({ thirdPartySignIn, setIsAuthInProgress, isAuthInProgress, isRegistrationInProgress }) => { +const MicrosoftSignInButton = ({ handleSignIn, isLoading, disabled }) => { const { instance, inProgress } = useMsal(); - const isLoading = isAuthInProgress || isRegistrationInProgress; - const environmentNotSet = !microsoftOAuthConfiguration.auth.clientId; const renderMicrosoftLogin = () => ( @@ -28,11 +25,10 @@ const MicrosoftSignInButton = ({ thirdPartySignIn, setIsAuthInProgress, isAuthIn messageId="microsoft" size="medium" onClick={() => { - setIsAuthInProgress(true); - handleClick(instance, thirdPartySignIn, setIsAuthInProgress); + handleClick(instance, handleSignIn); }} startIcon={} - disabled={inProgress === InteractionStatus.Startup || environmentNotSet} + disabled={inProgress === InteractionStatus.Startup || environmentNotSet || disabled} fullWidth isLoading={isLoading} tooltip={{ diff --git a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem copy/RecommendationListItem.test.tsx b/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem copy/RecommendationListItem.test.tsx deleted file mode 100644 index cbec58d1..00000000 --- a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem copy/RecommendationListItem.test.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoot } from "react-dom/client"; -import TestProvider from "tests/TestProvider"; -import RecommendationListItem from "./RecommendationListItem"; - -it("renders without crashing", () => { - const div = document.createElement("div"); - const root = createRoot(div); - root.render( - - - - ); - root.unmount(); -}); diff --git a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem copy/RecommendationListItem.tsx b/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem copy/RecommendationListItem.tsx deleted file mode 100644 index fea4a20b..00000000 --- a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem copy/RecommendationListItem.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Fragment } from "react"; - -const RecommendationListItem = ({ elements }) => ( -
- {elements.map(({ key, node }, i) => ( - - {node} - {i !== elements.length - 1 ? <> |  : null} - - ))} -
-); - -export default RecommendationListItem; diff --git a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem copy/index.ts b/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem copy/index.ts deleted file mode 100644 index 12f224aa..00000000 --- a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem copy/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import RecommendationListItem from "./RecommendationListItem"; - -export default RecommendationListItem; diff --git a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem/RecommendationListItem.test.tsx b/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem/RecommendationListItem.test.tsx deleted file mode 100644 index cbec58d1..00000000 --- a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem/RecommendationListItem.test.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoot } from "react-dom/client"; -import TestProvider from "tests/TestProvider"; -import RecommendationListItem from "./RecommendationListItem"; - -it("renders without crashing", () => { - const div = document.createElement("div"); - const root = createRoot(div); - root.render( - - - - ); - root.unmount(); -}); diff --git a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem/RecommendationListItem.tsx b/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem/RecommendationListItem.tsx deleted file mode 100644 index fea4a20b..00000000 --- a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem/RecommendationListItem.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Fragment } from "react"; - -const RecommendationListItem = ({ elements }) => ( -
- {elements.map(({ key, node }, i) => ( - - {node} - {i !== elements.length - 1 ? <> |  : null} - - ))} -
-); - -export default RecommendationListItem; diff --git a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem/index.ts b/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem/index.ts deleted file mode 100644 index 12f224aa..00000000 --- a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import RecommendationListItem from "./RecommendationListItem"; - -export default RecommendationListItem; diff --git a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel copy/RecommendationListItemResourceLabel.test.tsx b/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel copy/RecommendationListItemResourceLabel.test.tsx deleted file mode 100644 index ff901f9b..00000000 --- a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel copy/RecommendationListItemResourceLabel.test.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoot } from "react-dom/client"; -import TestProvider from "tests/TestProvider"; -import RecommendationListItemResourceLabel from "./RecommendationListItemResourceLabel"; - -it("renders without crashing", () => { - const div = document.createElement("div"); - const root = createRoot(div); - root.render( - - - - ); - root.unmount(); -}); diff --git a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel copy/RecommendationListItemResourceLabel.tsx b/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel copy/RecommendationListItemResourceLabel.tsx deleted file mode 100644 index bf4c8535..00000000 --- a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel copy/RecommendationListItemResourceLabel.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; -import CloudResourceId from "components/CloudResourceId"; -import CloudTypeIcon from "components/CloudTypeIcon"; -import IconLabel from "components/IconLabel"; -import { useApiData } from "hooks/useApiData"; -import { getCloudResourceIdentifier } from "utils/resources"; - -const RecommendationListItemResourceLabel = ({ item }) => { - const { cloud_type: cloudType, cloud_account_id: dataSourceId, resource_id: resourceId } = item; - - const { - apiData: { cloudAccounts: dataSources = [] } - } = useApiData(GET_DATA_SOURCES); - - return ( - } - label={ - id === dataSourceId)} - resourceId={resourceId} - cloudResourceIdentifier={getCloudResourceIdentifier(item)} - dataSourceId={dataSourceId} - /> - } - /> - ); -}; - -export default RecommendationListItemResourceLabel; diff --git a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel copy/index.ts b/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel copy/index.ts deleted file mode 100644 index 490eaa93..00000000 --- a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel copy/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import RecommendationListItemResourceLabel from "./RecommendationListItemResourceLabel"; - -export default RecommendationListItemResourceLabel; diff --git a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel/RecommendationListItemResourceLabel.test.tsx b/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel/RecommendationListItemResourceLabel.test.tsx deleted file mode 100644 index ff901f9b..00000000 --- a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel/RecommendationListItemResourceLabel.test.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { createRoot } from "react-dom/client"; -import TestProvider from "tests/TestProvider"; -import RecommendationListItemResourceLabel from "./RecommendationListItemResourceLabel"; - -it("renders without crashing", () => { - const div = document.createElement("div"); - const root = createRoot(div); - root.render( - - - - ); - root.unmount(); -}); diff --git a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel/RecommendationListItemResourceLabel.tsx b/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel/RecommendationListItemResourceLabel.tsx deleted file mode 100644 index bf4c8535..00000000 --- a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel/RecommendationListItemResourceLabel.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; -import CloudResourceId from "components/CloudResourceId"; -import CloudTypeIcon from "components/CloudTypeIcon"; -import IconLabel from "components/IconLabel"; -import { useApiData } from "hooks/useApiData"; -import { getCloudResourceIdentifier } from "utils/resources"; - -const RecommendationListItemResourceLabel = ({ item }) => { - const { cloud_type: cloudType, cloud_account_id: dataSourceId, resource_id: resourceId } = item; - - const { - apiData: { cloudAccounts: dataSources = [] } - } = useApiData(GET_DATA_SOURCES); - - return ( - } - label={ - id === dataSourceId)} - resourceId={resourceId} - cloudResourceIdentifier={getCloudResourceIdentifier(item)} - dataSourceId={dataSourceId} - /> - } - /> - ); -}; - -export default RecommendationListItemResourceLabel; diff --git a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel/index.ts b/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel/index.ts deleted file mode 100644 index 490eaa93..00000000 --- a/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItemResourceLabel/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import RecommendationListItemResourceLabel from "./RecommendationListItemResourceLabel"; - -export default RecommendationListItemResourceLabel; diff --git a/ngui/ui/src/components/MlRunHistoryChart/ResourceCountBreakdown/ResourceCountBreakdownLineChart/Layer/index.ts b/ngui/ui/src/components/MlRunHistoryChart/ResourceCountBreakdown/ResourceCountBreakdownLineChart/Layer/index.ts deleted file mode 100644 index f892ed44..00000000 --- a/ngui/ui/src/components/MlRunHistoryChart/ResourceCountBreakdown/ResourceCountBreakdownLineChart/Layer/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { useRenderWeekendsHighlightLayer } from "./useRenderWeekendsHighlightLayer"; - -export { useRenderWeekendsHighlightLayer }; diff --git a/ngui/ui/src/components/MlRunHistoryChart/ResourceCountBreakdown/ResourceCountBreakdownLineChart/Layer/useRenderWeekendsHighlightLayer.ts b/ngui/ui/src/components/MlRunHistoryChart/ResourceCountBreakdown/ResourceCountBreakdownLineChart/Layer/useRenderWeekendsHighlightLayer.ts deleted file mode 100644 index 42503f77..00000000 --- a/ngui/ui/src/components/MlRunHistoryChart/ResourceCountBreakdown/ResourceCountBreakdownLineChart/Layer/useRenderWeekendsHighlightLayer.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useTheme } from "@mui/material"; -import { useIsOrganizationWeekend } from "hooks/useIsOrganizationWeekend"; - -const getAreas = ({ shouldHighlight, xValues, getXCoordinateOfXValue }) => { - if (xValues.length <= 1) { - return []; - } - - const halfDistanceBetweenValues = (getXCoordinateOfXValue(xValues[1]) - getXCoordinateOfXValue(xValues[0])) / 2; - - const getAreaStartShift = (currentValueIndex) => (currentValueIndex === 0 ? 0 : -halfDistanceBetweenValues); - const getAreaEndShift = (currentValueIndex) => (currentValueIndex === xValues.length - 1 ? 0 : halfDistanceBetweenValues); - - return xValues - .map((xValue, currentXValueIndex) => { - if (shouldHighlight(xValue)) { - const currentValueXCoordinate = getXCoordinateOfXValue(xValue); - - const xStart = currentValueXCoordinate + getAreaStartShift(currentXValueIndex); - const xEnd = currentValueXCoordinate + getAreaEndShift(currentXValueIndex); - const width = xEnd - xStart; - - return { - xStart, - xEnd, - width - }; - } - return null; - }) - .filter(Boolean); -}; - -export const useRenderWeekendsHighlightLayer = () => { - const theme = useTheme(); - const isOrganizationWeekend = useIsOrganizationWeekend(); - - return (ctx, layerProps) => { - const { x, xScale, areaOpacity, linesAreaRectangle } = layerProps; - - const areas = getAreas({ - shouldHighlight: (xValue) => { - const date = new Date(xValue); - return isOrganizationWeekend(date); - }, - xValues: x.all, - getXCoordinateOfXValue: xScale - }); - - ctx.save(); - ctx.translate(linesAreaRectangle.xStart, linesAreaRectangle.yStart); - - ctx.globalAlpha = areaOpacity; - - areas.forEach(({ xStart, width }) => { - ctx.beginPath(); - ctx.rect(xStart, 0, width, linesAreaRectangle.height); - ctx.fillStyle = theme.palette.info.main; - ctx.fill(); - }); - - ctx.restore(); - }; -}; diff --git a/ngui/ui/src/components/Mode/Mode.tsx b/ngui/ui/src/components/Mode/Mode.tsx index 67475b97..f1c7613d 100644 --- a/ngui/ui/src/components/Mode/Mode.tsx +++ b/ngui/ui/src/components/Mode/Mode.tsx @@ -12,10 +12,7 @@ import { SPACING_2 } from "utils/layouts"; type ModeWrapperProps = { option: Record<(typeof OPTSCALE_MODE)[keyof typeof OPTSCALE_MODE], boolean>; onApply: (mode: ModeWrapperProps["option"]) => void; - isLoadingProps?: { - isGetOrganizationOptionLoading?: boolean; - isUpdateOrganizationOptionLoading?: boolean; - }; + isLoading?: boolean; }; type FeatureListProps = { @@ -66,9 +63,7 @@ const Card = ({ name, messageIds, onSelect, isSelected, isLoading, disabled = fa ); -const Mode = ({ option, onApply, isLoadingProps = {} }: ModeWrapperProps) => { - const { isGetOrganizationOptionLoading, isUpdateOrganizationOptionLoading } = isLoadingProps; - +const Mode = ({ option, onApply, isLoading }: ModeWrapperProps) => { const isApplyModeAllowed = useIsAllowed({ requiredActions: ["EDIT_PARTNER"] }); const [showApplyModeError, setShowApplyModeError] = useState(false); @@ -95,7 +90,6 @@ const Mode = ({ option, onApply, isLoadingProps = {} }: ModeWrapperProps) => { name="mlops" isSelected={mode[OPTSCALE_MODE.MLOPS]} onSelect={() => setMode(OPTSCALE_MODE.MLOPS)} - isLoading={isGetOrganizationOptionLoading} messageIds={["mode.mlops.1", "mode.mlops.2", "mode.mlops.3", "mode.mlops.4", "mode.mlops.5", "mode.mlops.6"]} disabled={!isApplyModeAllowed} /> @@ -103,7 +97,6 @@ const Mode = ({ option, onApply, isLoadingProps = {} }: ModeWrapperProps) => { setMode(OPTSCALE_MODE.FINOPS)} messageIds={["mode.finops.1", "mode.finops.2", "mode.finops.3", "mode.finops.4", "mode.finops.5", "mode.finops.6"]} disabled={!isApplyModeAllowed} @@ -122,7 +115,7 @@ const Mode = ({ option, onApply, isLoadingProps = {} }: ModeWrapperProps) => { color="primary" variant="contained" onClick={onApplyButtonClick} - isLoading={isGetOrganizationOptionLoading || isUpdateOrganizationOptionLoading} + isLoading={isLoading} /> )} diff --git a/ngui/ui/src/components/ModeWrapper/ModeWrapper.tsx b/ngui/ui/src/components/ModeWrapper/ModeWrapper.tsx index d3228d17..2443ca48 100644 --- a/ngui/ui/src/components/ModeWrapper/ModeWrapper.tsx +++ b/ngui/ui/src/components/ModeWrapper/ModeWrapper.tsx @@ -4,13 +4,13 @@ import { OPTSCALE_MODE } from "utils/constants"; type ModeWrapperProps = { children: ReactNode; - mode: (typeof OPTSCALE_MODE)[keyof typeof OPTSCALE_MODE] | undefined; + mode?: (typeof OPTSCALE_MODE)[keyof typeof OPTSCALE_MODE]; }; const ModeWrapper = ({ children, mode }: ModeWrapperProps) => { - const shouldShowChildren = useIsOptScaleModeEnabled(mode); + const isModeEnabled = useIsOptScaleModeEnabled(mode); - return shouldShowChildren ? children : null; + return isModeEnabled ? children : null; }; export default ModeWrapper; diff --git a/ngui/ui/src/components/OrganizationConstraintsTable/OrganizationConstraintsTable.tsx b/ngui/ui/src/components/OrganizationConstraintsTable/OrganizationConstraintsTable.tsx index 25f61908..646839cc 100644 --- a/ngui/ui/src/components/OrganizationConstraintsTable/OrganizationConstraintsTable.tsx +++ b/ngui/ui/src/components/OrganizationConstraintsTable/OrganizationConstraintsTable.tsx @@ -3,7 +3,6 @@ import AddOutlinedIcon from "@mui/icons-material/AddOutlined"; import ListAltOutlinedIcon from "@mui/icons-material/ListAltOutlined"; import { FormattedMessage } from "react-intl"; import { useNavigate } from "react-router-dom"; -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; import AnomaliesFilters from "components/AnomaliesFilters"; import Filters from "components/Filters"; import { RESOURCE_FILTERS } from "components/Filters/constants"; @@ -12,10 +11,10 @@ import IconButton from "components/IconButton"; import Table from "components/Table"; import TableLoader from "components/TableLoader"; import TextWithDataTestId from "components/TextWithDataTestId"; +import { useAllDataSources } from "hooks/coreData"; import { useIsAllowed } from "hooks/useAllowedActions"; -import { useApiData } from "hooks/useApiData"; import { intl } from "translations/react-intl-config"; -import { isEmpty } from "utils/arrays"; +import { isEmpty as isEmptyArray } from "utils/arrays"; import { organizationConstraintName, organizationConstraintStatus } from "utils/columns"; import { QUOTA_POLICY, @@ -116,9 +115,7 @@ const OrganizationConstraintsTable = ({ constraints, addButtonLink, isLoading = const isManageResourcesAllowed = useIsAllowed({ requiredActions: ["EDIT_PARTNER"] }); const formatter = useMoneyFormatter(); - const { - apiData: { cloudAccounts = [] } - } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); const memoizedConstraints = useMemo( () => @@ -205,7 +202,7 @@ const OrganizationConstraintsTable = ({ constraints, addButtonLink, isLoading = ) : (
{ const { currency: currencyCode } = useOrganizationInfo(); - const { - apiData: { cloudAccounts = [] } - } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); const [isEditMode, setIsEditMode] = useState(false); const enableEditMode = () => setIsEditMode(true); @@ -25,7 +22,7 @@ const OrganizationCurrency = () => { const isEditAllowed = useIsAllowed({ requiredActions: ["EDIT_PARTNER"] }); return isEditMode ? ( - + ) : ( { value={} sx={{ marginRight: 1 }} /> - {isEditAllowed && cloudAccounts.filter(({ type }) => type !== ENVIRONMENT).length === 0 ? ( + {isEditAllowed && dataSources.filter(({ type }) => type !== ENVIRONMENT).length === 0 ? ( } onClick={enableEditMode} diff --git a/ngui/ui/src/components/OrganizationLabel/OrganizationLabel.tsx b/ngui/ui/src/components/OrganizationLabel/OrganizationLabel.tsx index 0072e05d..81ed8cae 100644 --- a/ngui/ui/src/components/OrganizationLabel/OrganizationLabel.tsx +++ b/ngui/ui/src/components/OrganizationLabel/OrganizationLabel.tsx @@ -1,16 +1,47 @@ import ApartmentIcon from "@mui/icons-material/Apartment"; import Link from "@mui/material/Link"; -import { Link as RouterLink } from "react-router-dom"; import Icon from "components/Icon"; -import { getHomeUrl } from "urls"; +import { useUpdateScope } from "hooks/useUpdateScope"; +import { HOME } from "urls"; -const OrganizationLabel = ({ name, id, dataTestId, disableLink = false }) => ( +type OrganizationLabelProps = { + id: string; + name: string; + dataTestId?: string; + disableLink?: boolean; +}; + +type LabelLinkProps = { + organizationId: string; + organizationName: string; + dataTestId?: string; +}; + +const LabelLink = ({ organizationId, organizationName, dataTestId }: LabelLinkProps) => { + const updateScope = useUpdateScope(); + + return ( + + updateScope({ + newScopeId: organizationId, + redirectTo: HOME + }) + } + > + {organizationName} + + ); +}; + +const OrganizationLabel = ({ id, name, dataTestId, disableLink = false }: OrganizationLabelProps) => ( <> {!disableLink ? ( - - {name} - + ) : ( {name} )} diff --git a/ngui/ui/src/components/OrganizationSelector/OrganizationSelector.tsx b/ngui/ui/src/components/OrganizationSelector/OrganizationSelector.tsx index 8b5ff228..5991c992 100644 --- a/ngui/ui/src/components/OrganizationSelector/OrganizationSelector.tsx +++ b/ngui/ui/src/components/OrganizationSelector/OrganizationSelector.tsx @@ -43,7 +43,7 @@ const SELECTOR_SX = { }; type OrganizationSelectorProps = { - organizations?: { + organizations: { id: string; name: string; }[]; @@ -53,7 +53,7 @@ type OrganizationSelectorProps = { }; const OrganizationSelector = ({ - organizations = [], + organizations, organizationId = "", onChange, isLoading = false @@ -124,7 +124,7 @@ const OrganizationSelector = ({ ); }; -// NGUI-2198: selector is always visible and mounted with MainLayoutContainer, organizations and organizationId can be undefined +// NGUI-2198: selector is always visible and mounted with CoreDataContainer, organizations and organizationId can be undefined // TODO - consider mounting those component at different levels export default OrganizationSelector; diff --git a/ngui/ui/src/components/OrganizationsOverviewTable/OrganizationsOverviewTable.tsx b/ngui/ui/src/components/OrganizationsOverviewTable/OrganizationsOverviewTable.tsx index f851fc7a..fed7cc6c 100644 --- a/ngui/ui/src/components/OrganizationsOverviewTable/OrganizationsOverviewTable.tsx +++ b/ngui/ui/src/components/OrganizationsOverviewTable/OrganizationsOverviewTable.tsx @@ -1,13 +1,13 @@ import { useMemo } from "react"; import Link from "@mui/material/Link"; import { alpha, useTheme } from "@mui/material/styles"; -import { Link as RouterLink } from "react-router-dom"; import FormattedMoney from "components/FormattedMoney"; import KeyValueLabel from "components/KeyValueLabel/KeyValueLabel"; import OrganizationLabel from "components/OrganizationLabel"; import PoolLabel from "components/PoolLabel"; import Table from "components/Table"; import TableLoader from "components/TableLoader"; +import { useUpdateScope } from "hooks/useUpdateScope"; import { intl } from "translations/react-intl-config"; import { RECOMMENDATIONS } from "urls"; import { FORMATTED_MONEY_TYPES } from "utils/constants"; @@ -35,6 +35,8 @@ const OrganizationsOverviewTable = ({ data, total = data.length, isLoading = fal const tableData = useMemo(() => data, [data]); + const updateScope = useUpdateScope(); + const columns = useMemo( () => [ { @@ -90,7 +92,15 @@ const OrganizationsOverviewTable = ({ data, total = data.length, isLoading = fal } }) => saving ? ( - + + updateScope({ + newScopeId: organizationId, + redirectTo: RECOMMENDATIONS + }) + } + > ) : null @@ -112,7 +122,7 @@ const OrganizationsOverviewTable = ({ data, total = data.length, isLoading = fal getExceedingLimits("exceededForecasts", original).map((pool) => getExceedingLabel("forecast", pool, original)) } ], - [] + [updateScope] ); return isLoading ? ( diff --git a/ngui/ui/src/components/PasswordRecovery/PasswordRecovery.tsx b/ngui/ui/src/components/PasswordRecovery/PasswordRecovery.tsx index be80b2a9..bc7803c0 100644 --- a/ngui/ui/src/components/PasswordRecovery/PasswordRecovery.tsx +++ b/ngui/ui/src/components/PasswordRecovery/PasswordRecovery.tsx @@ -3,13 +3,16 @@ import { Stack } from "@mui/material"; import Link from "@mui/material/Link"; import Typography from "@mui/material/Typography"; import { FormattedMessage } from "react-intl"; -import { Link as RouterLink } from "react-router-dom"; +import { useDispatch } from "react-redux"; +import { useNavigate } from "react-router-dom"; import Greeter from "components/Greeter"; import ConfirmVerificationCodeContainer from "containers/ConfirmVerificationCodeContainer/ConfirmVerificationCodeContainer"; import CreateNewPasswordContainer from "containers/CreateNewPasswordContainer"; +import { initialize } from "containers/InitializeContainer/redux"; import SendVerificationCodeContainer from "containers/SendVerificationCodeContainer"; -import { HOME } from "urls"; +import { INITIALIZE } from "urls"; import { SPACING_2 } from "utils/layouts"; +import macaroon from "utils/macaroons"; import { getQueryParams, updateQueryParams } from "utils/network"; const SEND_VERIFICATION_CODE = 0; @@ -18,6 +21,9 @@ const CREATE_NEW_PASSWORD = 2; const PASSWORD_RECOVERY_SUCCESS = 3; const PasswordRecovery = () => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [step, setStep] = useState(() => { const { email } = getQueryParams() as { email: string }; @@ -28,6 +34,18 @@ const PasswordRecovery = () => { return SEND_VERIFICATION_CODE; }); + const [temporaryVerificationCodeToken, setTemporaryVerificationCodeToken] = useState<{ + user_id: string; + user_email: string; + token: string; + }>(); + + const [verificationCodeToken, setVerificationCodeToken] = useState<{ + user_id: string; + user_email: string; + token: string; + }>(); + const stepContent = { [SEND_VERIFICATION_CODE]: ( { }} /> ), - [CONFIRM_VERIFICATION_CODE]: setStep(CREATE_NEW_PASSWORD)} />, - [CREATE_NEW_PASSWORD]: setStep(PASSWORD_RECOVERY_SUCCESS)} />, + [CONFIRM_VERIFICATION_CODE]: ( + { + setStep(CREATE_NEW_PASSWORD); + setTemporaryVerificationCodeToken(token); + }} + /> + ), + [CREATE_NEW_PASSWORD]: ( + { + setVerificationCodeToken(token); + setStep(PASSWORD_RECOVERY_SUCCESS); + }} + /> + ), [PASSWORD_RECOVERY_SUCCESS]: (
@@ -50,7 +83,15 @@ const PasswordRecovery = () => {
- + { + const caveats = macaroon.processCaveats(macaroon.deserialize(verificationCodeToken.token).getCaveats()); + dispatch(initialize({ ...verificationCodeToken, caveats })); + navigate(INITIALIZE); + }} + > diff --git a/ngui/ui/src/components/PendingInvitationsAlert/PendingInvitationsAlert.tsx b/ngui/ui/src/components/PendingInvitationsAlert/PendingInvitationsAlert.tsx index 47f8d5a6..9ece929a 100644 --- a/ngui/ui/src/components/PendingInvitationsAlert/PendingInvitationsAlert.tsx +++ b/ngui/ui/src/components/PendingInvitationsAlert/PendingInvitationsAlert.tsx @@ -3,15 +3,15 @@ import Link from "@mui/material/Link"; import { useTheme } from "@mui/material/styles"; import { FormattedMessage } from "react-intl"; import { Link as RouterLink } from "react-router-dom"; -import { GET_INVITATIONS } from "api/restapi/actionTypes"; import SnackbarAlert from "components/SnackbarAlert"; -import { useApiData } from "hooks/useApiData"; +import { useInvitations } from "hooks/coreData"; import { SETTINGS_TABS } from "pages/Settings/Settings"; import { getSettingsUrl } from "urls"; import { isEmpty } from "utils/arrays"; const PendingInvitationsAlert = () => { - const { apiData: invitations } = useApiData(GET_INVITATIONS, []); + const invitations = useInvitations(); + const [open, setOpen] = useState(false); useEffect(() => { diff --git a/ngui/ui/src/components/PoolLabel/PoolLabel.tsx b/ngui/ui/src/components/PoolLabel/PoolLabel.tsx index d267968e..71262b36 100644 --- a/ngui/ui/src/components/PoolLabel/PoolLabel.tsx +++ b/ngui/ui/src/components/PoolLabel/PoolLabel.tsx @@ -1,28 +1,41 @@ import Link from "@mui/material/Link"; import { FormattedMessage } from "react-intl"; -import { Link as RouterLink } from "react-router-dom"; import IconLabel from "components/IconLabel"; import PoolTypeIcon from "components/PoolTypeIcon"; import SlicedText from "components/SlicedText"; +import { useUpdateScope } from "hooks/useUpdateScope"; import { getPoolUrl, isPoolIdWithSubPools } from "urls"; import { formQueryString } from "utils/network"; const SLICED_POOL_NAME_LENGTH = 35; -const getUrl = (poolId, organizationId) => { +const getUrl = (poolId: string, organizationId: string) => { // TODO: remove this after https://datatrendstech.atlassian.net/browse/OS-4157 const poolIdWithoutSubPoolMark = isPoolIdWithSubPools(poolId) ? poolId.slice(0, poolId.length - 1) : poolId; const baseUrl = getPoolUrl(poolIdWithoutSubPoolMark); - return organizationId ? `${baseUrl}?${formQueryString({ organizationId })}` : baseUrl; + return organizationId ? `${baseUrl}&${formQueryString({ organizationId })}` : baseUrl; }; const SlicedPoolName = ({ name }) => ; -const PoolLink = ({ id, name, dataTestId, organizationId }) => ( - - {name} - -); +const PoolLink = ({ id, name, dataTestId, organizationId }) => { + const updateScope = useUpdateScope(); + + return ( + { + updateScope({ + newScopeId: organizationId, + redirectTo: getUrl(id, organizationId) + }); + }} + data-test-id={dataTestId} + > + {name} + + ); +}; const renderLabel = ({ disableLink, name, id, dataTestId, organizationId }) => { const slicedName = ; diff --git a/ngui/ui/src/components/RecommendationDetails/SelectedCloudAccounts/SelectedCloudAccounts.tsx b/ngui/ui/src/components/RecommendationDetails/SelectedCloudAccounts/SelectedCloudAccounts.tsx index 7e5fc6a6..2d754352 100644 --- a/ngui/ui/src/components/RecommendationDetails/SelectedCloudAccounts/SelectedCloudAccounts.tsx +++ b/ngui/ui/src/components/RecommendationDetails/SelectedCloudAccounts/SelectedCloudAccounts.tsx @@ -1,17 +1,14 @@ import { Box } from "@mui/material"; -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; import CloudLabel from "components/CloudLabel"; -import { useApiData } from "hooks/useApiData"; +import { useAllDataSources } from "hooks/coreData"; import { SPACING_1 } from "utils/layouts"; const SelectedCloudAccounts = ({ cloudAccountIds }) => { - const { - apiData: { cloudAccounts = [] } - } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); return ( - {cloudAccounts + {dataSources .map(({ name, id, type: cloudType }) => { if (cloudAccountIds.indexOf(id) > -1) { return ; diff --git a/ngui/ui/src/components/RecommendationListItemResourceLabel/RecommendationListItemResourceLabel.tsx b/ngui/ui/src/components/RecommendationListItemResourceLabel/RecommendationListItemResourceLabel.tsx index bf4c8535..063a70cb 100644 --- a/ngui/ui/src/components/RecommendationListItemResourceLabel/RecommendationListItemResourceLabel.tsx +++ b/ngui/ui/src/components/RecommendationListItemResourceLabel/RecommendationListItemResourceLabel.tsx @@ -1,16 +1,13 @@ -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; import CloudResourceId from "components/CloudResourceId"; import CloudTypeIcon from "components/CloudTypeIcon"; import IconLabel from "components/IconLabel"; -import { useApiData } from "hooks/useApiData"; +import { useAllDataSources } from "hooks/coreData"; import { getCloudResourceIdentifier } from "utils/resources"; const RecommendationListItemResourceLabel = ({ item }) => { const { cloud_type: cloudType, cloud_account_id: dataSourceId, resource_id: resourceId } = item; - const { - apiData: { cloudAccounts: dataSources = [] } - } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); return ( { - const { - apiData: { cloudAccounts: dataSources = [] } - } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); return ( diff --git a/ngui/ui/src/components/ResourcesPerspectives/ResourcesPerspectives.tsx b/ngui/ui/src/components/ResourcesPerspectives/ResourcesPerspectives.tsx index eef4b165..3b1c67c0 100644 --- a/ngui/ui/src/components/ResourcesPerspectives/ResourcesPerspectives.tsx +++ b/ngui/ui/src/components/ResourcesPerspectives/ResourcesPerspectives.tsx @@ -14,10 +14,10 @@ import Table from "components/Table"; import TableCellActions from "components/TableCellActions"; import TextWithDataTestId from "components/TextWithDataTestId"; import Tooltip from "components/Tooltip"; +import { useOrganizationPerspectives } from "hooks/coreData"; import { useIsAllowed } from "hooks/useAllowedActions"; import { breakdowns } from "hooks/useBreakdownBy"; import { useOpenSideModal } from "hooks/useOpenSideModal"; -import { useOrganizationPerspectives } from "hooks/useOrganizationPerspectives"; import { getResourcesExpensesUrl } from "urls"; import { isEmpty as isEmptyArray } from "utils/arrays"; diff --git a/ngui/ui/src/components/TopAlertWrapper/TopAlertWrapper.tsx b/ngui/ui/src/components/TopAlertWrapper/TopAlertWrapper.tsx index 8b15f225..25cb93e3 100644 --- a/ngui/ui/src/components/TopAlertWrapper/TopAlertWrapper.tsx +++ b/ngui/ui/src/components/TopAlertWrapper/TopAlertWrapper.tsx @@ -3,10 +3,8 @@ import { Box } from "@mui/material"; import { render as renderGithubButton } from "github-buttons"; import { FormattedMessage, useIntl } from "react-intl"; import { useDispatch } from "react-redux"; -import { GET_TOKEN } from "api/auth/actionTypes"; -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; -import { useApiData } from "hooks/useApiData"; -import { useApiState } from "hooks/useApiState"; +import { useAllDataSources } from "hooks/coreData"; +import { useGetToken } from "hooks/useGetToken"; import { useOrganizationInfo } from "hooks/useOrganizationInfo"; import { useRootData } from "hooks/useRootData"; import { GITHUB_HYSTAX_OPTSCALE_REPO } from "urls"; @@ -55,21 +53,15 @@ const TopAlertWrapper = ({ blacklistIds = [] }) => { const { organizationId } = useOrganizationInfo(); - const { - apiData: { userId } - } = useApiData(GET_TOKEN); + const { userId } = useGetToken(); const storedAlerts = useAllAlertsSelector(organizationId); const { rootData: isExistingUser = false } = useRootData(IS_EXISTING_USER); - const { - apiData: { cloudAccounts = [] } - } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); - const { isDataReady: isDataSourceReady } = useApiState(GET_DATA_SOURCES, organizationId); - - const eligibleDataSources = getEligibleDataSources(cloudAccounts); + const eligibleDataSources = getEligibleDataSources(dataSources); const hasDataSourceInProcessing = eligibleDataSources.some(({ last_import_at: lastImportAt }) => lastImportAt === 0); @@ -86,10 +78,10 @@ const TopAlertWrapper = ({ blacklistIds = [] }) => { ); // "recharging" message about processing if closed, when no items are been processed - if (isDataSourceReady && !hasDataSourceInProcessing && isDataSourcedProcessingAlertClosed) { + if (!hasDataSourceInProcessing && isDataSourcedProcessingAlertClosed) { updateOrganizationTopAlert({ id: ALERT_TYPES.DATA_SOURCES_ARE_PROCESSING, closed: false }); } - }, [hasDataSourceInProcessing, isDataSourceReady, storedAlerts, updateOrganizationTopAlert]); + }, [hasDataSourceInProcessing, storedAlerts, updateOrganizationTopAlert]); const alerts = useMemo(() => { const isDataSourcesAreProceedingAlertTriggered = storedAlerts.some( @@ -104,7 +96,7 @@ const TopAlertWrapper = ({ blacklistIds = [] }) => { return [ { id: ALERT_TYPES.DATA_SOURCES_ARE_PROCESSING, - condition: isDataSourceReady && hasDataSourceInProcessing, + condition: hasDataSourceInProcessing, getContent: () => , onClose: () => { updateOrganizationTopAlert({ id: ALERT_TYPES.DATA_SOURCES_ARE_PROCESSING, closed: true }); @@ -117,7 +109,7 @@ const TopAlertWrapper = ({ blacklistIds = [] }) => { }, { id: ALERT_TYPES.DATA_SOURCES_PROCEEDED, - condition: isDataSourceReady && !hasDataSourceInProcessing && isDataSourcesAreProceedingAlertTriggered, + condition: !hasDataSourceInProcessing && isDataSourcesAreProceedingAlertTriggered, getContent: () => , type: "success", triggered: isTriggered(ALERT_TYPES.DATA_SOURCES_PROCEEDED), @@ -164,15 +156,7 @@ const TopAlertWrapper = ({ blacklistIds = [] }) => { dataTestId: "top_alert_open_source_announcement" } ]; - }, [ - storedAlerts, - isDataSourceReady, - hasDataSourceInProcessing, - isExistingUser, - updateOrganizationTopAlert, - userId, - organizationId - ]); + }, [storedAlerts, hasDataSourceInProcessing, isExistingUser, updateOrganizationTopAlert, userId, organizationId]); const currentAlert = useMemo( () => diff --git a/ngui/ui/src/components/TopResourcesExpensesCard/TopResourcesExpensesCard.tsx b/ngui/ui/src/components/TopResourcesExpensesCard/TopResourcesExpensesCard.tsx index b87d1951..fd50a2de 100644 --- a/ngui/ui/src/components/TopResourcesExpensesCard/TopResourcesExpensesCard.tsx +++ b/ngui/ui/src/components/TopResourcesExpensesCard/TopResourcesExpensesCard.tsx @@ -18,7 +18,7 @@ import TableLoader from "components/TableLoader"; import TitleValue from "components/TitleValue"; import Tooltip from "components/Tooltip"; import WrapperCard from "components/WrapperCard"; -import { useOrganizationPerspectives } from "hooks/useOrganizationPerspectives"; +import { useOrganizationPerspectives } from "hooks/coreData"; import { getLast30DaysResourcesUrl, getResourcesExpensesUrl, RESOURCE_PERSPECTIVES } from "urls"; import { isEmpty as isEmptyArray } from "utils/arrays"; import { FORMATTED_MONEY_TYPES } from "utils/constants"; diff --git a/ngui/ui/src/components/AcceptInvitations/AcceptInvitations.styles.ts b/ngui/ui/src/components/WrongInvitationEmailAlert/WrongInvitationEmailAlert.styles.ts similarity index 100% rename from ngui/ui/src/components/AcceptInvitations/AcceptInvitations.styles.ts rename to ngui/ui/src/components/WrongInvitationEmailAlert/WrongInvitationEmailAlert.styles.ts diff --git a/ngui/ui/src/components/WrongInvitationEmailAlert/WrongInvitationEmailAlert.tsx b/ngui/ui/src/components/WrongInvitationEmailAlert/WrongInvitationEmailAlert.tsx index fed80fa7..505273f5 100644 --- a/ngui/ui/src/components/WrongInvitationEmailAlert/WrongInvitationEmailAlert.tsx +++ b/ngui/ui/src/components/WrongInvitationEmailAlert/WrongInvitationEmailAlert.tsx @@ -2,8 +2,8 @@ import ExitToAppIcon from "@mui/icons-material/ExitToApp"; import NavigationIcon from "@mui/icons-material/Navigation"; import { Box, Typography } from "@mui/material"; import { FormattedMessage } from "react-intl"; -import useStyles from "components/AcceptInvitations/AcceptInvitations.styles"; import Button from "components/Button"; +import useStyles from "./WrongInvitationEmailAlert.styles"; type WrongInvitationEmailAlertProps = { invitationEmail: string; diff --git a/ngui/ui/src/components/forms/AddInstancesToScheduleForm/FormElements/DataSourcesField.tsx b/ngui/ui/src/components/forms/AddInstancesToScheduleForm/FormElements/DataSourcesField.tsx index 74ad9f5d..55c45cf3 100644 --- a/ngui/ui/src/components/forms/AddInstancesToScheduleForm/FormElements/DataSourcesField.tsx +++ b/ngui/ui/src/components/forms/AddInstancesToScheduleForm/FormElements/DataSourcesField.tsx @@ -1,8 +1,7 @@ import { Controller, useFormContext } from "react-hook-form"; import { useIntl } from "react-intl"; -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; import DataSourceMultiSelect from "components/DataSourceMultiSelect"; -import { useApiData } from "hooks/useApiData"; +import { useAllDataSources } from "hooks/coreData"; import { isEmpty as isEmptyArray } from "utils/arrays"; import { ALIBABA_CNR, AWS_CNR, AZURE_CNR, GCP_CNR, NEBIUS } from "utils/constants"; import { FormValues } from "../types"; @@ -13,9 +12,7 @@ export const FIELD_NAME = "dataSources"; const SUPPORTED_DATA_SOURCE_TYPES = [AWS_CNR, AZURE_CNR, GCP_CNR, ALIBABA_CNR, NEBIUS]; const useDataSources = () => { - const { - apiData: { cloudAccounts: dataSources = [] } - } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); return dataSources.filter(({ type }) => SUPPORTED_DATA_SOURCE_TYPES.includes(type)); }; diff --git a/ngui/ui/src/components/forms/LoginForm/FormElements/FormButtons.tsx b/ngui/ui/src/components/forms/LoginForm/FormElements/FormButtons.tsx index 799896a8..154096fa 100644 --- a/ngui/ui/src/components/forms/LoginForm/FormElements/FormButtons.tsx +++ b/ngui/ui/src/components/forms/LoginForm/FormElements/FormButtons.tsx @@ -1,7 +1,7 @@ import ButtonLoader from "components/ButtonLoader"; import FormButtonsWrapper from "components/FormButtonsWrapper"; -const FormButtons = ({ isLoading = false }) => ( +const FormButtons = ({ disabled = false, isLoading = false }) => ( ( variant="contained" color="lightBlue" isLoading={isLoading} + disabled={disabled} messageId="login" type="submit" size="large" diff --git a/ngui/ui/src/components/forms/LoginForm/LoginForm.tsx b/ngui/ui/src/components/forms/LoginForm/LoginForm.tsx index 5413f88a..1df9b8b6 100644 --- a/ngui/ui/src/components/forms/LoginForm/LoginForm.tsx +++ b/ngui/ui/src/components/forms/LoginForm/LoginForm.tsx @@ -10,7 +10,7 @@ import { EmailField, FormButtons, PasswordField } from "./FormElements"; import { FormValues, LoginFormProps } from "./types"; import { getDefaultValues } from "./utils"; -const LoginForm = ({ onSubmit, isLoading = false, isInvited = false }: LoginFormProps) => { +const LoginForm = ({ onSubmit, isLoading = false, disabled = false, isInvited = false }: LoginFormProps) => { const { email = "" } = getQueryParams() as { email?: string }; const methods = useForm({ @@ -28,7 +28,7 @@ const LoginForm = ({ onSubmit, isLoading = false, isInvited = false }: LoginForm
- + diff --git a/ngui/ui/src/components/forms/LoginForm/types.ts b/ngui/ui/src/components/forms/LoginForm/types.ts index 470e9d5b..721a3521 100644 --- a/ngui/ui/src/components/forms/LoginForm/types.ts +++ b/ngui/ui/src/components/forms/LoginForm/types.ts @@ -8,5 +8,6 @@ export type FormValues = { export type LoginFormProps = { onSubmit: (data: FormValues) => void; isLoading?: boolean; + disabled?: boolean; isInvited?: boolean; }; diff --git a/ngui/ui/src/components/forms/RegistrationForm/RegistrationForm.tsx b/ngui/ui/src/components/forms/RegistrationForm/RegistrationForm.tsx index 1af527f6..e0634ee9 100644 --- a/ngui/ui/src/components/forms/RegistrationForm/RegistrationForm.tsx +++ b/ngui/ui/src/components/forms/RegistrationForm/RegistrationForm.tsx @@ -12,7 +12,7 @@ import useStyles from "./RegistrationForm.styles"; import { FormValues, RegistrationFormProps } from "./types"; import { getDefaultValues } from "./utils"; -const RegistrationForm = ({ onSubmit, isLoading = false, isInvited = false }: RegistrationFormProps) => { +const RegistrationForm = ({ onSubmit, isLoading = false, disabled = false, isInvited = false }: RegistrationFormProps) => { const { classes } = useStyles(); const { email = "" } = getQueryParams() as { email?: string }; @@ -44,6 +44,7 @@ const RegistrationForm = ({ onSubmit, isLoading = false, isInvited = false }: Re color="lightBlue" customWrapperClass={classes.registerButton} isLoading={isLoading} + disabled={disabled} messageId="register" type="submit" size="large" diff --git a/ngui/ui/src/components/forms/RegistrationForm/types.ts b/ngui/ui/src/components/forms/RegistrationForm/types.ts index ddbe402f..e05336ea 100644 --- a/ngui/ui/src/components/forms/RegistrationForm/types.ts +++ b/ngui/ui/src/components/forms/RegistrationForm/types.ts @@ -10,5 +10,6 @@ export type FormValues = { export type RegistrationFormProps = { onSubmit: (data: FormValues) => void; isLoading?: boolean; + disabled?: boolean; isInvited?: boolean; }; diff --git a/ngui/ui/src/containers/AcceptInvitationsContainer/AcceptInvitationsContainer.tsx b/ngui/ui/src/containers/AcceptInvitationsContainer/AcceptInvitationsContainer.tsx deleted file mode 100644 index 813409c8..00000000 --- a/ngui/ui/src/containers/AcceptInvitationsContainer/AcceptInvitationsContainer.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useEffect } from "react"; -import CircularProgress from "@mui/material/CircularProgress"; -import { useDispatch } from "react-redux"; -import { getInvitations } from "api"; -import { GET_INVITATIONS, UPDATE_INVITATION } from "api/restapi/actionTypes"; -import AcceptInvitations from "components/AcceptInvitations"; -import Backdrop from "components/Backdrop"; -import { useApiData } from "hooks/useApiData"; -import { useApiState } from "hooks/useApiState"; -import { useNewAuthorization } from "hooks/useNewAuthorization"; - -const AcceptInvitationsContainer = () => { - const { isGetOrganizationsLoading, isGetInvitationsLoading, isCreateOrganizationLoading, activateScope } = - useNewAuthorization(); - - const { apiData: invitations } = useApiData(GET_INVITATIONS, []); - const { isLoading: isUpdateInvitationLoading } = useApiState(UPDATE_INVITATION); - - const dispatch = useDispatch(); - - useEffect(() => { - dispatch(getInvitations()); - }, [dispatch]); - - return isGetInvitationsLoading ? ( - - - - ) : ( - - ); -}; - -export default AcceptInvitationsContainer; diff --git a/ngui/ui/src/containers/AcceptInvitationsContainer/index.ts b/ngui/ui/src/containers/AcceptInvitationsContainer/index.ts deleted file mode 100644 index d46d66ff..00000000 --- a/ngui/ui/src/containers/AcceptInvitationsContainer/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import AcceptInvitationsContainer from "./AcceptInvitationsContainer"; - -export default AcceptInvitationsContainer; diff --git a/ngui/ui/src/containers/AuthorizationContainer/AuthorizationContainer.tsx b/ngui/ui/src/containers/AuthorizationContainer/AuthorizationContainer.tsx index 00471001..2b46e531 100644 --- a/ngui/ui/src/containers/AuthorizationContainer/AuthorizationContainer.tsx +++ b/ngui/ui/src/containers/AuthorizationContainer/AuthorizationContainer.tsx @@ -1,6 +1,7 @@ +import { useMutation } from "@apollo/client"; import { Stack } from "@mui/material"; -import { useLocation } from "react-router-dom"; -import { GET_TOKEN } from "api/auth/actionTypes"; +import { useDispatch } from "react-redux"; +import { useLocation, useNavigate } from "react-router-dom"; import LoginForm from "components/forms/LoginForm"; import RegistrationForm from "components/forms/RegistrationForm"; import GoogleAuthButton from "components/GoogleAuthButton"; @@ -8,79 +9,150 @@ import Greeter from "components/Greeter"; import MicrosoftSignInButton from "components/MicrosoftSignInButton"; import OAuthSignIn from "components/OAuthSignIn"; import Redirector from "components/Redirector"; -import { useApiData } from "hooks/useApiData"; -import { useNewAuthorization } from "hooks/useNewAuthorization"; +import { initialize } from "containers/InitializeContainer/redux"; +import { CREATE_TOKEN, CREATE_USER, SIGN_IN } from "graphql/api/auth/queries"; +import { useGetToken } from "hooks/useGetToken"; import { useOrganizationInfo } from "hooks/useOrganizationInfo"; -import { HOME_FIRST_TIME, HOME, REGISTER, LOGIN } from "urls"; +import VerifyEmailService from "services/VerifyEmailService"; +import { + REGISTER, + LOGIN, + INITIALIZE, + EMAIL_VERIFICATION, + SHOW_POLICY_QUERY_PARAM, + USER_EMAIL_QUERY_PARAMETER_NAME, + NEXT_QUERY_PARAMETER_NAME +} from "urls"; +import { GA_EVENT_CATEGORIES, trackEvent } from "utils/analytics"; import { SPACING_4 } from "utils/layouts"; -import { getQueryParams } from "utils/network"; +import macaroon from "utils/macaroons"; +import { formQueryString, getQueryParams, updateQueryParams } from "utils/network"; -export const getLoginRedirectionPath = (scopeUserEmail: string) => { - const { next = HOME, userEmail: userEmailQueryParameter } = getQueryParams(); - - if (userEmailQueryParameter) { - return userEmailQueryParameter === scopeUserEmail ? next : HOME; - } - - return next; -}; +const EMAIL_NOT_VERIFIED_ERROR_CODE = "OA0073"; const AuthorizationContainer = () => { const { pathname } = useLocation(); + const dispatch = useDispatch(); - const { invited: queryInvited, next = HOME } = getQueryParams(); - - const { authorize, register, isRegistrationInProgress, isAuthInProgress, thirdPartySignIn, setIsAuthInProgress } = - useNewAuthorization(); + const { invited: queryInvited } = getQueryParams(); const { isDemo } = useOrganizationInfo(); - const { - apiData: { token } - } = useApiData(GET_TOKEN); + + const { token } = useGetToken(); + + const navigate = useNavigate(); + const isTokenExists = Boolean(token); - const onSubmitRegister = ({ name, email, password }) => { - register( - { name, email, password }, - { - getOnSuccessRedirectionPath: () => HOME_FIRST_TIME + const { useSendEmailVerificationCode } = VerifyEmailService(); + + const { onSend: sendEmailVerificationCode, isLoading: isSendEmailVerificationCodeLoading } = useSendEmailVerificationCode(); + + const [createToken, { loading: loginLoading }] = useMutation(CREATE_TOKEN); + + const [createUser, { loading: registerLoading }] = useMutation(CREATE_USER); + + const [signIn, { loading: signInLoading }] = useMutation(SIGN_IN, { + onCompleted: (data) => { + const caveats = macaroon.processCaveats(macaroon.deserialize(data.signIn.token).getCaveats()); + const { register, provider } = caveats; + if (register) { + trackEvent({ category: GA_EVENT_CATEGORIES.USER, action: "Registered", label: provider }); + updateQueryParams({ + [SHOW_POLICY_QUERY_PARAM]: true + }); } - ); + dispatch(initialize({ ...data.signIn, caveats })); + } + }); + + const handleLogin = ({ email, password }) => { + createToken({ variables: { email, password } }) + .then(({ data }) => { + const caveats = macaroon.processCaveats(macaroon.deserialize(data.token.token).getCaveats()); + dispatch(initialize({ ...data.token, caveats })); + }) + .catch((error) => { + console.log(error); + if (error?.graphQLErrors?.[0].extensions.response.body.error.error_code === EMAIL_NOT_VERIFIED_ERROR_CODE) { + navigate(`${EMAIL_VERIFICATION}?${formQueryString({ email })}`); + } + }); }; - const onSubmitLogin = ({ email, password }) => { - authorize( - { email, password }, - { - getOnSuccessRedirectionPath: ({ userEmail }) => getLoginRedirectionPath(userEmail) - } - ); + const handleRegister = ({ email, password, name }) => { + createUser({ variables: { email, password, name } }) + .then(() => { + trackEvent({ category: GA_EVENT_CATEGORIES.USER, action: "Registered", label: "optscale" }); + return Promise.resolve(); + }) + .then(() => sendEmailVerificationCode(email)) + .then(() => { + navigate( + `${EMAIL_VERIFICATION}?${formQueryString({ + email + })}` + ); + }); }; - const onThirdPartySignIn = (provider, params) => - thirdPartySignIn( - { provider, params }, - { - getOnSuccessRedirectionPath: ({ userEmail }) => getLoginRedirectionPath(userEmail) + const handleThirdPartySignIn = ({ provider, token: thirdPartyToken, tenantId, redirectUri }) => { + signIn({ variables: { provider, token: thirdPartyToken, tenantId, redirectUri } }).then(({ data }) => { + const caveats = macaroon.processCaveats(macaroon.deserialize(data.signIn.token).getCaveats()); + if (caveats.register) { + trackEvent({ category: GA_EVENT_CATEGORIES.USER, action: "Registered", label: caveats.provider }); } - ); - - // isGetTokenLoading used for LoginForm, isCreateUserLoading for RegistrationForm - const isLoading = isRegistrationInProgress || isAuthInProgress; + dispatch(initialize({ ...data.signIn, caveats })); + }); + }; const isInvited = queryInvited !== undefined; const createForm = { - [LOGIN]: () => , - [REGISTER]: () => + [LOGIN]: () => ( + + ), + [REGISTER]: () => ( + + ) }[pathname] || (() => null); + // TODO: get back to the force redirect // redirecting already authorized user from /login and /register pages - const shouldRedirectAuthorizedUser = !isAuthInProgress && !isRegistrationInProgress && !isDemo && isTokenExists; + // const shouldRedirectAuthorizedUser = !isAuthInProgress && !isRegistrationInProgress && !isDemo && isTokenExists; + const shouldRedirectAuthorizedUser = !isDemo && isTokenExists; + + const getRedirectionPath = () => { + const { + [NEXT_QUERY_PARAMETER_NAME]: next, + [USER_EMAIL_QUERY_PARAMETER_NAME]: email, + [SHOW_POLICY_QUERY_PARAM]: showPolicy + } = getQueryParams() as { + [NEXT_QUERY_PARAMETER_NAME]: string; + [USER_EMAIL_QUERY_PARAMETER_NAME]: string; + [SHOW_POLICY_QUERY_PARAM]: boolean | string; + }; + + return `${INITIALIZE}?${formQueryString({ + [NEXT_QUERY_PARAMETER_NAME]: next || INITIALIZE, + [USER_EMAIL_QUERY_PARAMETER_NAME]: email, + [SHOW_POLICY_QUERY_PARAM]: showPolicy + })}`; + }; return ( - + @@ -89,18 +161,16 @@ const AuthorizationContainer = () => { } microsoftButton={ } /> diff --git a/ngui/ui/src/containers/BookEnvironmentFormContainer/BookEnvironmentFormContainer.tsx b/ngui/ui/src/containers/BookEnvironmentFormContainer/BookEnvironmentFormContainer.tsx index d7d71c1f..43137342 100644 --- a/ngui/ui/src/containers/BookEnvironmentFormContainer/BookEnvironmentFormContainer.tsx +++ b/ngui/ui/src/containers/BookEnvironmentFormContainer/BookEnvironmentFormContainer.tsx @@ -2,9 +2,10 @@ import { useEffect, useMemo } from "react"; import { millisecondsToSeconds } from "date-fns"; import { useDispatch } from "react-redux"; import { RESTAPI, bookEnvironment, createSshKey, getSshKeys } from "api"; -import { BOOK_ENVIRONMENT, GET_CURRENT_EMPLOYEE, GET_SSH_KEYS, CREATE_SSH_KEY } from "api/restapi/actionTypes"; +import { BOOK_ENVIRONMENT, GET_SSH_KEYS, CREATE_SSH_KEY } from "api/restapi/actionTypes"; import BookEnvironmentForm from "components/forms/BookEnvironmentForm"; import { FormValues } from "components/forms/BookEnvironmentForm/types"; +import { useCurrentEmployee } from "hooks/coreData"; import { useIsAllowed } from "hooks/useAllowedActions"; import { useApiData } from "hooks/useApiData"; import { useApiState } from "hooks/useApiState"; @@ -28,9 +29,7 @@ const BookEnvironmentFormContainer = ({ const { isLoading: isBookEnvironmentLoading } = useApiState(BOOK_ENVIRONMENT); const { isLoading: isCreateSshKeyLoading } = useApiState(CREATE_SSH_KEY); - const { - apiData: { currentEmployee = {} } - } = useApiData(GET_CURRENT_EMPLOYEE); + const currentEmployee = useCurrentEmployee(); const requestParams = useMemo( () => ({ diff --git a/ngui/ui/src/containers/ConfirmEmailVerificationCodeContainer/ConfirmEmailVerificationCodeContainer.tsx b/ngui/ui/src/containers/ConfirmEmailVerificationCodeContainer/ConfirmEmailVerificationCodeContainer.tsx index c8d2486f..415725f8 100644 --- a/ngui/ui/src/containers/ConfirmEmailVerificationCodeContainer/ConfirmEmailVerificationCodeContainer.tsx +++ b/ngui/ui/src/containers/ConfirmEmailVerificationCodeContainer/ConfirmEmailVerificationCodeContainer.tsx @@ -2,12 +2,11 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import { FormattedMessage } from "react-intl"; import ConfirmEmailVerificationCodeForm from "components/forms/ConfirmEmailVerificationCodeForm"; -import { useNewAuthorization } from "hooks/useNewAuthorization"; import VerifyEmailService from "services/VerifyEmailService"; import { getQueryParams } from "utils/network"; type ConfirmEmailVerificationCodeContainerProps = { - onSuccess: () => void; + onSuccess: (args: { user_id: string; user_email: string; token: string }) => void; }; const ConfirmEmailVerificationCodeContainer = ({ onSuccess }: ConfirmEmailVerificationCodeContainerProps) => { @@ -15,37 +14,17 @@ const ConfirmEmailVerificationCodeContainer = ({ onSuccess }: ConfirmEmailVerifi const { useGetEmailVerificationCodeToken } = VerifyEmailService(); - const { onGet: onGetEmailVerificationCodeToken, isLoading: isLoadingGetEmailVerificationCodeToken } = - useGetEmailVerificationCodeToken(); - - const { activateScope, isGetOrganizationsLoading, isCreateOrganizationLoading } = useNewAuthorization(); + const { onGet, isLoading } = useGetEmailVerificationCodeToken(); return ( - {chunks}, - email - }} - /> + {email} - - onGetEmailVerificationCodeToken(email, code) - .then(() => - activateScope(email, { - getOnSuccessRedirectionPath: () => undefined - }) - ) - .then(onSuccess) - } - isLoading={isLoadingGetEmailVerificationCodeToken || isGetOrganizationsLoading || isCreateOrganizationLoading} - /> + onGet(email, code).then(onSuccess)} isLoading={isLoading} /> ); }; diff --git a/ngui/ui/src/containers/ConfirmVerificationCodeContainer/ConfirmVerificationCodeContainer.tsx b/ngui/ui/src/containers/ConfirmVerificationCodeContainer/ConfirmVerificationCodeContainer.tsx index 407de991..f4cb26ae 100644 --- a/ngui/ui/src/containers/ConfirmVerificationCodeContainer/ConfirmVerificationCodeContainer.tsx +++ b/ngui/ui/src/containers/ConfirmVerificationCodeContainer/ConfirmVerificationCodeContainer.tsx @@ -6,7 +6,7 @@ import ResetPasswordServices from "services/ResetPasswordServices"; import { getQueryParams } from "utils/network"; type ConfirmVerificationCodeContainerProps = { - onSuccess: () => void; + onSuccess: (args: { user_id: string; user_email: string; token: string }) => void; }; const ConfirmVerificationCodeContainer = ({ onSuccess }: ConfirmVerificationCodeContainerProps) => { diff --git a/ngui/ui/src/containers/ConnectCloudAccountContainer/ConnectCloudAccountContainer.tsx b/ngui/ui/src/containers/ConnectCloudAccountContainer/ConnectCloudAccountContainer.tsx index 80aac025..dbb51d35 100644 --- a/ngui/ui/src/containers/ConnectCloudAccountContainer/ConnectCloudAccountContainer.tsx +++ b/ngui/ui/src/containers/ConnectCloudAccountContainer/ConnectCloudAccountContainer.tsx @@ -1,35 +1,56 @@ -import { useDispatch } from "react-redux"; +import { useMutation } from "@apollo/client"; import { useNavigate } from "react-router-dom"; -import { createDataSource } from "api"; -import { CREATE_DATA_SOURCE } from "api/restapi/actionTypes"; +import { GET_AVAILABLE_FILTERS } from "api/restapi/actionTypes"; import ConnectCloudAccount from "components/ConnectCloudAccount"; -import { useApiState } from "hooks/useApiState"; +import { CREATE_DATA_SOURCE as APOLLO_CREATE_DATA_SOURCE, GET_DATA_SOURCES } from "graphql/api/restapi/queries/restapi.queries"; import { useOrganizationInfo } from "hooks/useOrganizationInfo"; +import { useRefetchApis } from "hooks/useRefetchApis"; import { CLOUD_ACCOUNTS } from "urls"; import { trackEvent, GA_EVENT_CATEGORIES } from "utils/analytics"; -import { isError } from "utils/api"; +import { ALIBABA_CNR, AWS_CNR, AZURE_CNR, AZURE_TENANT, DATABRICKS, GCP_CNR, KUBERNETES_CNR, NEBIUS } from "utils/constants"; const ConnectCloudAccountContainer = () => { const { organizationId } = useOrganizationInfo(); - const { isLoading } = useApiState(CREATE_DATA_SOURCE); + + const refetch = useRefetchApis(); const navigate = useNavigate(); - const dispatch = useDispatch(); + + const [createDataSource, { loading }] = useMutation(APOLLO_CREATE_DATA_SOURCE); const redirectToCloudsOverview = () => navigate(CLOUD_ACCOUNTS); const onSubmit = (params) => { - dispatch((_, getState) => { - dispatch(createDataSource(organizationId, params)).then(() => { - if (!isError(CREATE_DATA_SOURCE, getState())) { - redirectToCloudsOverview(); + const configName = { + [AWS_CNR]: params.linked ? "awsLinkedConfig" : "awsRootConfig", + [AZURE_TENANT]: "azureTenantConfig", + [AZURE_CNR]: "azureSubscriptionConfig", + [GCP_CNR]: "gcpConfig", + [ALIBABA_CNR]: "alibabaConfig", + [NEBIUS]: "nebiusConfig", + [DATABRICKS]: "databricksConfig", + [KUBERNETES_CNR]: "k8sConfig" + }[params.type]; + + trackEvent({ category: GA_EVENT_CATEGORIES.DATA_SOURCE, action: "Try connect", label: params.type }); + + createDataSource({ + variables: { + organizationId, + params: { + name: params.name, + type: params.type, + [configName]: params.config } - }); + }, + refetchQueries: [GET_DATA_SOURCES] + }).then(() => { + refetch([GET_AVAILABLE_FILTERS]); + redirectToCloudsOverview(); }); - trackEvent({ category: GA_EVENT_CATEGORIES.DATA_SOURCE, action: "Try connect", label: params.type }); }; - return ; + return ; }; export default ConnectCloudAccountContainer; diff --git a/ngui/ui/src/containers/CoreDataContainer/CoreDataContainer.tsx b/ngui/ui/src/containers/CoreDataContainer/CoreDataContainer.tsx new file mode 100644 index 00000000..067674dd --- /dev/null +++ b/ngui/ui/src/containers/CoreDataContainer/CoreDataContainer.tsx @@ -0,0 +1,136 @@ +import { useQuery } from "@apollo/client"; +import { CircularProgress } from "@mui/material"; +import Backdrop from "components/Backdrop"; +import { GET_ORGANIZATION_ALLOWED_ACTIONS } from "graphql/api/auth/queries"; +import { + GET_ORGANIZATIONS, + GET_DATA_SOURCES, + GET_CURRENT_EMPLOYEE, + GET_INVITATIONS, + GET_ORGANIZATION_FEATURES, + GET_OPTSCALE_MODE, + GET_ORGANIZATION_THEME_SETTINGS, + GET_ORGANIZATION_PERSPECTIVES +} from "graphql/api/restapi/queries"; +import { useOrganizationInfo } from "hooks/useOrganizationInfo"; +import { useUpdateScope } from "hooks/useUpdateScope"; +import { getQueryParams, removeQueryParam } from "utils/network"; + +const CoreDataContainer = ({ children }) => { + const updateScope = useUpdateScope(); + + const { loading: getOrganizationsLoading, error: getOrganizationsError } = useQuery(GET_ORGANIZATIONS, { + onCompleted: (data) => { + const { organizationId } = getQueryParams() as { organizationId: string }; + + if (data.organizations.find((org) => org.id === organizationId)) { + updateScope({ + newScopeId: organizationId + }); + removeQueryParam("organizationId"); + } + } + }); + + const { organizationId } = useOrganizationInfo(); + + const skipRequest = !organizationId; + + const { loading: getOrganizationAllowedActionsLoading, error: getOrganizationAllowedActionsError } = useQuery( + GET_ORGANIZATION_ALLOWED_ACTIONS, + { + variables: { + requestParams: { + organization: organizationId + } + }, + skip: skipRequest + } + ); + + const { loading: getCurrentEmployeeLoading, error: getCurrentEmployeeError } = useQuery(GET_CURRENT_EMPLOYEE, { + variables: { + organizationId + }, + skip: skipRequest + }); + + const { loading: getDataSourcesLoading, error: getDataSourcesError } = useQuery(GET_DATA_SOURCES, { + variables: { + organizationId + }, + skip: skipRequest + }); + + const { loading: getInvitationsLoading, error: getInvitationsError } = useQuery(GET_INVITATIONS, { + skip: skipRequest + }); + + const { loading: getOrganizationFeaturesLoading, error: getOrganizationFeaturesError } = useQuery(GET_ORGANIZATION_FEATURES, { + variables: { + organizationId + }, + skip: skipRequest + }); + + const { loading: getOptscaleModeLoading, error: getOptscaleModeError } = useQuery(GET_OPTSCALE_MODE, { + skip: skipRequest, + variables: { + organizationId + } + }); + + const { loading: getOrganizationThemeSettingsLoading, error: getOrganizationThemeSettingsError } = useQuery( + GET_ORGANIZATION_THEME_SETTINGS, + { + variables: { + organizationId + }, + skip: skipRequest + } + ); + + const { loading: getOrganizationPerspectivesLoading, error: getOrganizationPerspectivesError } = useQuery( + GET_ORGANIZATION_PERSPECTIVES, + { + variables: { + organizationId + }, + skip: skipRequest + } + ); + + const isLoading = + getOrganizationsLoading || + getOrganizationAllowedActionsLoading || + getCurrentEmployeeLoading || + getDataSourcesLoading || + getInvitationsLoading || + getOrganizationFeaturesLoading || + getOptscaleModeLoading || + getOrganizationThemeSettingsLoading || + getOrganizationPerspectivesLoading; + + const error = + getOrganizationsError || + getOrganizationAllowedActionsError || + getCurrentEmployeeError || + getDataSourcesError || + getInvitationsError || + getOrganizationFeaturesError || + getOptscaleModeError || + getOrganizationThemeSettingsError || + getOrganizationPerspectivesError; + + if (isLoading) { + return ( + + + + ); + } + + return error ? "tbd error" : children; +}; + +export default CoreDataContainer; diff --git a/ngui/ui/src/containers/CoreDataContainer/index.ts b/ngui/ui/src/containers/CoreDataContainer/index.ts new file mode 100644 index 00000000..a732cd26 --- /dev/null +++ b/ngui/ui/src/containers/CoreDataContainer/index.ts @@ -0,0 +1,3 @@ +import CoreDataContainer from "./CoreDataContainer"; + +export default CoreDataContainer; diff --git a/ngui/ui/src/containers/CreateAssignmentRuleFormContainer/CreateAssignmentRuleFormContainer.tsx b/ngui/ui/src/containers/CreateAssignmentRuleFormContainer/CreateAssignmentRuleFormContainer.tsx index 294c85b4..84bf791d 100644 --- a/ngui/ui/src/containers/CreateAssignmentRuleFormContainer/CreateAssignmentRuleFormContainer.tsx +++ b/ngui/ui/src/containers/CreateAssignmentRuleFormContainer/CreateAssignmentRuleFormContainer.tsx @@ -4,11 +4,12 @@ import { FormattedMessage } from "react-intl"; import { useDispatch } from "react-redux"; import { useNavigate, Link as RouterLink } from "react-router-dom"; import { createAssignmentRule, getPoolOwners, RESTAPI, getAvailablePools } from "api"; -import { CREATE_ASSIGNMENT_RULE, GET_DATA_SOURCES, GET_POOL_OWNERS, GET_AVAILABLE_POOLS } from "api/restapi/actionTypes"; +import { CREATE_ASSIGNMENT_RULE, GET_POOL_OWNERS, GET_AVAILABLE_POOLS } from "api/restapi/actionTypes"; import ActionBar from "components/ActionBar"; import AssignmentRuleForm from "components/forms/AssignmentRuleForm"; import { FIELD_NAMES } from "components/forms/AssignmentRuleForm/utils"; import PageContentWrapper from "components/PageContentWrapper"; +import { useAllDataSources } from "hooks/coreData"; import { useApiData } from "hooks/useApiData"; import { useApiState } from "hooks/useApiState"; import { useAssignmentRulesAvailableFilters } from "hooks/useAssignmentRulesAvailableFilters"; @@ -178,10 +179,7 @@ const CreateAssignmentRuleFormContainer = () => { }); }, [dispatch, organizationPoolId, organizationId]); - // get cloud accounts - // Attention: we don't request cloud account here as they are included in the initial loader - // and we assume that they are up-to-date - const { apiData: { cloudAccounts = [] } = {} } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); const { isLoading: isAvailableFiltersLoading, resourceTypes, regions } = useAssignmentRulesAvailableFilters(); @@ -207,7 +205,7 @@ const CreateAssignmentRuleFormContainer = () => { }} onCancel={redirect} pools={pools} - cloudAccounts={cloudAccounts} + cloudAccounts={dataSources} resourceTypes={resourceTypes} regions={regions} onPoolChange={(newPoolId, callback) => { diff --git a/ngui/ui/src/containers/CreateNewPasswordContainer/CreateNewPasswordContainer.tsx b/ngui/ui/src/containers/CreateNewPasswordContainer/CreateNewPasswordContainer.tsx index a2de9f37..12df58d3 100644 --- a/ngui/ui/src/containers/CreateNewPasswordContainer/CreateNewPasswordContainer.tsx +++ b/ngui/ui/src/containers/CreateNewPasswordContainer/CreateNewPasswordContainer.tsx @@ -2,15 +2,19 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import { FormattedMessage } from "react-intl"; import CreateNewPasswordForm from "components/forms/CreateNewPasswordForm"; -import { useNewAuthorization } from "hooks/useNewAuthorization"; import ResetPasswordServices from "services/ResetPasswordServices"; import { getQueryParams } from "utils/network"; type CreateNewPasswordContainerProps = { - onSuccess: () => void; + verificationCodeToken: { + user_id: string; + user_email: string; + token: string; + }; + onSuccess: (args: { user_id: string; user_email: string; token: string }) => void; }; -const CreateNewPasswordContainer = ({ onSuccess }: CreateNewPasswordContainerProps) => { +const CreateNewPasswordContainer = ({ verificationCodeToken, onSuccess }: CreateNewPasswordContainerProps) => { const { email } = getQueryParams() as { email: string }; const { useUpdateUserPassword, useGetNewToken } = ResetPasswordServices(); @@ -18,8 +22,6 @@ const CreateNewPasswordContainer = ({ onSuccess }: CreateNewPasswordContainerPro const { onUpdate: onUpdateUserPassword, isLoading: isUpdateUserPasswordLoading } = useUpdateUserPassword(); const { onGet: onGetNewToken, isLoading: isGetNewTokenLoading } = useGetNewToken(); - const { activateScope, isGetOrganizationsLoading, isCreateOrganizationLoading } = useNewAuthorization(); - return ( @@ -27,18 +29,11 @@ const CreateNewPasswordContainer = ({ onSuccess }: CreateNewPasswordContainerPro - onUpdateUserPassword(newPassword) + onUpdateUserPassword(verificationCodeToken, newPassword) .then(() => onGetNewToken(email, newPassword)) - .then(() => - activateScope(email, { - getOnSuccessRedirectionPath: () => undefined - }) - ) - .then(() => onSuccess()) - } - isLoading={ - isUpdateUserPasswordLoading || isGetNewTokenLoading || isGetOrganizationsLoading || isCreateOrganizationLoading + .then(onSuccess) } + isLoading={isUpdateUserPasswordLoading || isGetNewTokenLoading} /> ); diff --git a/ngui/ui/src/containers/CreateOrganizationContainer/CreateOrganizationContainer.tsx b/ngui/ui/src/containers/CreateOrganizationContainer/CreateOrganizationContainer.tsx index 2822e084..4607d908 100644 --- a/ngui/ui/src/containers/CreateOrganizationContainer/CreateOrganizationContainer.tsx +++ b/ngui/ui/src/containers/CreateOrganizationContainer/CreateOrganizationContainer.tsx @@ -1,17 +1,27 @@ +import { useLazyQuery, useMutation } from "@apollo/client"; import CreateOrganizationForm from "components/forms/CreateOrganizationForm"; import { FormValues } from "components/forms/CreateOrganizationForm/types"; -import OrganizationsService from "services/OrganizationsService"; +import { CREATE_ORGANIZATION, GET_ORGANIZATIONS } from "graphql/api/restapi/queries"; const CreateOrganizationContainer = ({ onSuccess, closeSideModal }) => { - const { useCreate, useGet } = OrganizationsService(); + const [createOrganization, { loading: createOrganizationLoading }] = useMutation(CREATE_ORGANIZATION); - const { onCreate, isLoading: isCreateLoading } = useCreate(); - const { getOrganizations, isLoading: isGetLoading } = useGet(); + const [getOrganizations, { loading: isOrganizationsLoading }] = useLazyQuery(GET_ORGANIZATIONS, { + fetchPolicy: "network-only" + }); - const isLoading = isCreateLoading || isGetLoading; + const isLoading = createOrganizationLoading || isOrganizationsLoading; const onSubmit = async (formData: FormValues) => { - const { id } = await onCreate(formData.name); + const { + data: { + createOrganization: { id } + } + } = await createOrganization({ + variables: { + organizationName: formData.name + } + }); await getOrganizations(); onSuccess(id); closeSideModal(); diff --git a/ngui/ui/src/containers/CreateResourceAssignmentRuleFormContainer/CreateResourceAssignmentRuleFormContainer.tsx b/ngui/ui/src/containers/CreateResourceAssignmentRuleFormContainer/CreateResourceAssignmentRuleFormContainer.tsx index 5f115425..2c5639a6 100644 --- a/ngui/ui/src/containers/CreateResourceAssignmentRuleFormContainer/CreateResourceAssignmentRuleFormContainer.tsx +++ b/ngui/ui/src/containers/CreateResourceAssignmentRuleFormContainer/CreateResourceAssignmentRuleFormContainer.tsx @@ -4,19 +4,13 @@ import { FormattedMessage } from "react-intl"; import { useDispatch } from "react-redux"; import { useNavigate, Link as RouterLink } from "react-router-dom"; import { getResource, RESTAPI, getPoolOwners, createAssignmentRule, getAvailablePools } from "api"; -import { - GET_RESOURCE, - GET_DATA_SOURCES, - CREATE_ASSIGNMENT_RULE, - GET_POOL_OWNERS, - GET_AVAILABLE_POOLS -} from "api/restapi/actionTypes"; +import { GET_RESOURCE, CREATE_ASSIGNMENT_RULE, GET_POOL_OWNERS, GET_AVAILABLE_POOLS } from "api/restapi/actionTypes"; import ActionBar from "components/ActionBar"; import ActionBarResourceNameTitleText from "components/ActionBarResourceNameTitleText"; import AssignmentRuleForm from "components/forms/AssignmentRuleForm"; import { FIELD_NAMES } from "components/forms/AssignmentRuleForm/utils"; import PageContentWrapper from "components/PageContentWrapper"; -import { useApiData } from "hooks/useApiData"; +import { useAllDataSources } from "hooks/coreData"; import { useApiState } from "hooks/useApiState"; import { useAssignmentRulesAvailableFilters } from "hooks/useAssignmentRulesAvailableFilters"; import { useOrganizationInfo } from "hooks/useOrganizationInfo"; @@ -168,10 +162,7 @@ const CreateResourceAssignmentRuleFormContainer = ({ resourceId }) => { const { isLoading: isCreateAssignmentRuleLoading } = useApiState(CREATE_ASSIGNMENT_RULE); - // get cloud accounts - // Attention: we don't request cloud account here as they are included in the initial loader - // and we assume that they are up-to-date - const { apiData: { cloudAccounts = [] } = {} } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); const { isLoading: isAvailableFiltersLoading, resourceTypes, regions } = useAssignmentRulesAvailableFilters(); @@ -213,7 +204,7 @@ const CreateResourceAssignmentRuleFormContainer = ({ resourceId }) => { }} onCancel={redirect} pools={pools} - cloudAccounts={cloudAccounts} + cloudAccounts={dataSources} resourceTypes={resourceTypes} regions={regions} onPoolChange={(newPoolId, callback) => { diff --git a/ngui/ui/src/containers/CreateResourcePerspectiveContainer/CreateResourcePerspectiveContainer.tsx b/ngui/ui/src/containers/CreateResourcePerspectiveContainer/CreateResourcePerspectiveContainer.tsx index 2090e9fa..cbfd0a29 100644 --- a/ngui/ui/src/containers/CreateResourcePerspectiveContainer/CreateResourcePerspectiveContainer.tsx +++ b/ngui/ui/src/containers/CreateResourcePerspectiveContainer/CreateResourcePerspectiveContainer.tsx @@ -1,28 +1,41 @@ +import { useMutation } from "@apollo/client"; import CreateResourcePerspectiveForm from "components/forms/CreateResourcePerspectiveForm"; -import { useOrganizationPerspectives } from "hooks/useOrganizationPerspectives"; -import OrganizationOptionsService from "services/OrganizationOptionsService"; +import { GET_ORGANIZATION_PERSPECTIVES, UPDATE_ORGANIZATION_PERSPECTIVES } from "graphql/api/restapi/queries/restapi.queries"; +import { useOrganizationPerspectives } from "hooks/coreData"; +import { useOrganizationInfo } from "hooks/useOrganizationInfo"; const CreateResourcePerspectiveContainer = ({ filters, breakdownBy, breakdownData, onSuccess, onCancel }) => { - const { allPerspectives } = useOrganizationPerspectives(); + const { organizationId } = useOrganizationInfo(); - const { useUpdateOrganizationPerspectives } = OrganizationOptionsService(); + const { allPerspectives } = useOrganizationPerspectives(); - const { update, isLoading } = useUpdateOrganizationPerspectives(); + const [updateOrganizationPerspectives, { loading }] = useMutation(UPDATE_ORGANIZATION_PERSPECTIVES, { + update: (cache, { data }) => { + cache.writeQuery({ + query: GET_ORGANIZATION_PERSPECTIVES, + variables: { organizationId }, + data: { + organizationPerspectives: data.updateOrganizationPerspectives + } + }); + } + }); - const onSubmit = (data) => { - update( - { - ...allPerspectives, - [data.name]: data.payload - }, - onSuccess - ); - }; + const onSubmit = (data) => + updateOrganizationPerspectives({ + variables: { + organizationId, + value: { + ...allPerspectives, + [data.name]: data.payload + } + } + }).then(onSuccess); return ( { - const { apiData: { currentEmployee: { id: currentEmployeeId } = {} } = {} } = useApiData(GET_CURRENT_EMPLOYEE); + const { id: currentEmployeeId } = useCurrentEmployee(); + const { isLoading } = useApiState(DELETE_EMPLOYEE); const dispatch = useDispatch(); const { name } = useOrganizationInfo(); diff --git a/ngui/ui/src/containers/DeleteOrganizationContainer/DeleteOrganizationContainer.tsx b/ngui/ui/src/containers/DeleteOrganizationContainer/DeleteOrganizationContainer.tsx index 20bb195d..69149999 100644 --- a/ngui/ui/src/containers/DeleteOrganizationContainer/DeleteOrganizationContainer.tsx +++ b/ngui/ui/src/containers/DeleteOrganizationContainer/DeleteOrganizationContainer.tsx @@ -1,38 +1,33 @@ import { useState } from "react"; +import { useMutation } from "@apollo/client"; import Typography from "@mui/material/Typography"; import { FormattedMessage } from "react-intl"; -import { useDispatch } from "react-redux"; -import { deleteOrganization } from "api"; -import { DELETE_ORGANIZATION } from "api/restapi/actionTypes"; import DeleteEntity from "components/DeleteEntity"; import Input from "components/Input"; import OrganizationLabel from "components/OrganizationLabel"; -import { useApiState } from "hooks/useApiState"; +import { DELETE_ORGANIZATION } from "graphql/api/restapi/queries/restapi.queries"; import { useOrganizationInfo } from "hooks/useOrganizationInfo"; import { useSignOut } from "hooks/useSignOut"; -import { isError } from "utils/api"; const CONFIRMATION_TEXT = "delete"; const DeleteOrganizationContainer = ({ onCancel }) => { - const dispatch = useDispatch(); - const { name: organizationName, organizationId } = useOrganizationInfo(); const [confirmationTextInputValue, setConfirmationTextInputValue] = useState(""); - const { isLoading } = useApiState(DELETE_ORGANIZATION); - const signOut = useSignOut(); + const [deleteOrganization, { loading }] = useMutation(DELETE_ORGANIZATION); + const onDelete = () => { - dispatch((_, getState) => { - dispatch(deleteOrganization(organizationId)).then(() => { - if (!isError(DELETE_ORGANIZATION, getState())) { - onCancel(); - signOut(); - } - }); + deleteOrganization({ + variables: { + organizationId + } + }).then(() => { + onCancel(); + signOut(); }); }; @@ -49,7 +44,7 @@ const DeleteOrganizationContainer = ({ onCancel }) => { onDelete }} onCancel={onCancel} - isLoading={isLoading} + isLoading={loading} > diff --git a/ngui/ui/src/containers/DeleteResourcePerspectiveContainer/DeleteResourcePerspectiveContainer.tsx b/ngui/ui/src/containers/DeleteResourcePerspectiveContainer/DeleteResourcePerspectiveContainer.tsx index 50c72063..3d0f0474 100644 --- a/ngui/ui/src/containers/DeleteResourcePerspectiveContainer/DeleteResourcePerspectiveContainer.tsx +++ b/ngui/ui/src/containers/DeleteResourcePerspectiveContainer/DeleteResourcePerspectiveContainer.tsx @@ -1,27 +1,40 @@ +import { useMutation } from "@apollo/client"; import DeleteResourcePerspective from "components/DeleteResourcePerspective"; -import { useOrganizationPerspectives } from "hooks/useOrganizationPerspectives"; -import OrganizationOptionsService from "services/OrganizationOptionsService"; +import { GET_ORGANIZATION_PERSPECTIVES, UPDATE_ORGANIZATION_PERSPECTIVES } from "graphql/api/restapi/queries/restapi.queries"; +import { useOrganizationPerspectives } from "hooks/coreData"; +import { useOrganizationInfo } from "hooks/useOrganizationInfo"; import { removeKey } from "utils/objects"; const DeleteResourcePerspectiveContainer = ({ perspectiveName, onCancel, onSuccess }) => { - const { allPerspectives } = useOrganizationPerspectives(); + const { organizationId } = useOrganizationInfo(); - const { useUpdateOrganizationPerspectives } = OrganizationOptionsService(); + const { allPerspectives } = useOrganizationPerspectives(); - const { update, isLoading } = useUpdateOrganizationPerspectives(); + const [updateOrganizationPerspectives, { loading }] = useMutation(UPDATE_ORGANIZATION_PERSPECTIVES, { + update: (cache, { data }) => { + cache.writeQuery({ + query: GET_ORGANIZATION_PERSPECTIVES, + variables: { organizationId }, + data: { + organizationPerspectives: data.updateOrganizationPerspectives + } + }); + } + }); const onDelete = () => { const newPerspectives = removeKey(allPerspectives, perspectiveName); - update(newPerspectives, onSuccess); + + return updateOrganizationPerspectives({ + variables: { + organizationId, + value: newPerspectives + } + }).then(onSuccess); }; return ( - + ); }; diff --git a/ngui/ui/src/containers/DisconnectCloudAccountContainer/DisconnectCloudAccountContainer.tsx b/ngui/ui/src/containers/DisconnectCloudAccountContainer/DisconnectCloudAccountContainer.tsx index 3515d7b4..36e28062 100644 --- a/ngui/ui/src/containers/DisconnectCloudAccountContainer/DisconnectCloudAccountContainer.tsx +++ b/ngui/ui/src/containers/DisconnectCloudAccountContainer/DisconnectCloudAccountContainer.tsx @@ -1,16 +1,47 @@ +import { useMutation } from "@apollo/client"; import { useNavigate } from "react-router-dom"; +import { GET_AVAILABLE_FILTERS } from "api/restapi/actionTypes"; import DisconnectCloudAccountForm from "components/forms/DisconnectCloudAccountForm"; import { getReasonValue } from "components/forms/DisconnectCloudAccountForm/utils"; +import { DELETE_DATA_SOURCE, GET_DATA_SOURCES } from "graphql/api/restapi/queries/restapi.queries"; +import { useAllDataSources } from "hooks/coreData"; +import { useRefetchApis } from "hooks/useRefetchApis"; import DataSourcesService, { DATASOURCE_SURVEY_TYPES } from "services/DataSourcesService"; import { CLOUD_ACCOUNTS } from "urls"; +import { ENVIRONMENT } from "utils/constants"; -const DisconnectCloudAccountContainer = ({ id, type, parentId, onCancel }) => { +type DisconnectCloudAccountContainerProps = { + id: string; + type: string; + parentId: string; + onCancel: () => void; +}; + +const useIsLastDataSource = () => { + const dataSources = useAllDataSources(); + + return dataSources.filter(({ type }) => type !== ENVIRONMENT).length === 1; +}; + +const DisconnectCloudAccountContainer = ({ id, type, parentId, onCancel }: DisconnectCloudAccountContainerProps) => { + const refetch = useRefetchApis(); const navigate = useNavigate(); - const { useDisconnectDataSource, useCreateSurvey, useIsLastDataSource } = DataSourcesService(); + const { useCreateSurvey } = DataSourcesService(); + + const [deleteDataSource, { loading: isDisconnectDataSourceLoading }] = useMutation(DELETE_DATA_SOURCE, { + refetchQueries: [GET_DATA_SOURCES] + }); - const { isLoading: isDisconnectDataSourceLoading, disconnectDataSource } = useDisconnectDataSource(); - const disconnectAndRedirect = () => disconnectDataSource(id).then(() => navigate(CLOUD_ACCOUNTS)); + const disconnect = () => + deleteDataSource({ + variables: { + dataSourceId: id + } + }).then(() => { + refetch([GET_AVAILABLE_FILTERS]); + navigate(CLOUD_ACCOUNTS); + }); const { isLoading: isCreateSurveyLoading, createSurvey } = useCreateSurvey(); @@ -35,9 +66,9 @@ const DisconnectCloudAccountContainer = ({ id, type, parentId, onCancel }) => { other: otherReason, capabilities: missingCapabilities }; - createSurvey(DATASOURCE_SURVEY_TYPES.DISCONNECT_LAST_DATA_SOURCE, data).then(disconnectAndRedirect); + createSurvey(DATASOURCE_SURVEY_TYPES.DISCONNECT_LAST_DATA_SOURCE, data).then(disconnect); } else { - disconnectAndRedirect(); + disconnect(); } }} /> diff --git a/ngui/ui/src/containers/EditAssignmentRuleFormContainer/EditAssignmentRuleFormContainer.tsx b/ngui/ui/src/containers/EditAssignmentRuleFormContainer/EditAssignmentRuleFormContainer.tsx index 8b7f4a64..34586221 100644 --- a/ngui/ui/src/containers/EditAssignmentRuleFormContainer/EditAssignmentRuleFormContainer.tsx +++ b/ngui/ui/src/containers/EditAssignmentRuleFormContainer/EditAssignmentRuleFormContainer.tsx @@ -4,17 +4,12 @@ import { FormattedMessage } from "react-intl"; import { useDispatch } from "react-redux"; import { useNavigate, Link as RouterLink } from "react-router-dom"; import { getPoolOwners, getAvailablePools, getAssignmentRule, updateAssignmentRule, RESTAPI } from "api"; -import { - GET_ASSIGNMENT_RULE, - UPDATE_ASSIGNMENT_RULE, - GET_DATA_SOURCES, - GET_POOL_OWNERS, - GET_AVAILABLE_POOLS -} from "api/restapi/actionTypes"; +import { GET_ASSIGNMENT_RULE, UPDATE_ASSIGNMENT_RULE, GET_POOL_OWNERS, GET_AVAILABLE_POOLS } from "api/restapi/actionTypes"; import ActionBar from "components/ActionBar"; import AssignmentRuleForm from "components/forms/AssignmentRuleForm"; import { FIELD_NAMES } from "components/forms/AssignmentRuleForm/utils"; import PageContentWrapper from "components/PageContentWrapper"; +import { useAllDataSources } from "hooks/coreData"; import { useApiData } from "hooks/useApiData"; import { useApiState } from "hooks/useApiState"; import { useAssignmentRulesAvailableFilters } from "hooks/useAssignmentRulesAvailableFilters"; @@ -103,10 +98,7 @@ const EditAssignmentRuleFormContainer = ({ assignmentRuleId }) => { apiData: { poolOwners = [] } } = useApiData(GET_POOL_OWNERS); - // get cloud accounts - // Attention: we don't request cloud account here as they are included in the initial loader - // and we assume that they are up-to-date - const { apiData: { cloudAccounts = [] } = {} } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); const { isLoading: isAvailableFiltersLoading, resourceTypes, regions } = useAssignmentRulesAvailableFilters(); @@ -192,7 +184,7 @@ const EditAssignmentRuleFormContainer = ({ assignmentRuleId }) => { }} onCancel={redirect} pools={pools} - cloudAccounts={cloudAccounts} + cloudAccounts={dataSources} resourceTypes={resourceTypes} regions={regions} isEdit diff --git a/ngui/ui/src/containers/EditOrganizationCurrencyFormContainer/EditOrganizationCurrencyFormContainer.tsx b/ngui/ui/src/containers/EditOrganizationCurrencyFormContainer/EditOrganizationCurrencyFormContainer.tsx index 432d9daa..c878d509 100644 --- a/ngui/ui/src/containers/EditOrganizationCurrencyFormContainer/EditOrganizationCurrencyFormContainer.tsx +++ b/ngui/ui/src/containers/EditOrganizationCurrencyFormContainer/EditOrganizationCurrencyFormContainer.tsx @@ -1,26 +1,32 @@ -import { useDispatch } from "react-redux"; -import { updateOrganization } from "api"; -import { UPDATE_ORGANIZATION } from "api/restapi/actionTypes"; +import { useMutation } from "@apollo/client"; import EditOrganizationCurrencyForm from "components/forms/EditOrganizationCurrencyForm"; import { FormValues } from "components/forms/EditOrganizationCurrencyForm/types"; -import { useApiState } from "hooks/useApiState"; +import { UPDATE_ORGANIZATION } from "graphql/api/restapi/queries/restapi.queries"; import { useOrganizationInfo } from "hooks/useOrganizationInfo"; -const EditOrganizationCurrencyFormContainer = ({ onCancel }) => { - const dispatch = useDispatch(); - - const { isLoading } = useApiState(UPDATE_ORGANIZATION); +type EditOrganizationCurrencyFormContainerProps = { + onCancel: () => void; + onSuccess: () => void; +}; +const EditOrganizationCurrencyFormContainer = ({ onCancel, onSuccess }: EditOrganizationCurrencyFormContainerProps) => { const { currency, organizationId } = useOrganizationInfo(); + const [updateOrganization, { loading }] = useMutation(UPDATE_ORGANIZATION); + const onSubmit = (formData: FormValues) => { - // There is no need to handle edit mode close since organization will be re-requested after editing - // and backdrop loader for the entire page will be rendered => edit form will be automatically unmounted - dispatch(updateOrganization(organizationId, { currency: formData.currency })); + updateOrganization({ + variables: { + organizationId, + params: { + currency: formData.currency + } + } + }).then(onSuccess); }; return ( - + ); }; diff --git a/ngui/ui/src/containers/EditOrganizationFormContainer/EditOrganizationFormContainer.tsx b/ngui/ui/src/containers/EditOrganizationFormContainer/EditOrganizationFormContainer.tsx index e43e4462..f1bdefa4 100644 --- a/ngui/ui/src/containers/EditOrganizationFormContainer/EditOrganizationFormContainer.tsx +++ b/ngui/ui/src/containers/EditOrganizationFormContainer/EditOrganizationFormContainer.tsx @@ -1,26 +1,28 @@ -import { useDispatch } from "react-redux"; -import { updateOrganization } from "api"; -import { UPDATE_ORGANIZATION } from "api/restapi/actionTypes"; +import { useMutation } from "@apollo/client"; import EditOrganizationForm from "components/forms/EditOrganizationForm"; import { FormValues } from "components/forms/EditOrganizationForm/types"; -import { useApiState } from "hooks/useApiState"; +import { UPDATE_ORGANIZATION } from "graphql/api/restapi/queries/restapi.queries"; import { useOrganizationInfo } from "hooks/useOrganizationInfo"; type EditOrganizationFormContainerProps = { onCancel: () => void; + onSuccess: () => void; }; -const EditOrganizationFormContainer = ({ onCancel }: EditOrganizationFormContainerProps) => { - const dispatch = useDispatch(); - - const { isLoading } = useApiState(UPDATE_ORGANIZATION); - +const EditOrganizationFormContainer = ({ onCancel, onSuccess }: EditOrganizationFormContainerProps) => { const { name: currentOrganizationName, organizationId } = useOrganizationInfo(); + const [updateOrganization, { loading }] = useMutation(UPDATE_ORGANIZATION); + const onSubmit = ({ organizationName }: FormValues) => { - // There is no need to handle edit mode close since organization will be re-requested after deletion - // and backdrop loader for the entire page will be rendered => edit form will be automatically unmounted - dispatch(updateOrganization(organizationId, { name: organizationName })); + updateOrganization({ + variables: { + organizationId, + params: { + name: organizationName + } + } + }).then(onSuccess); }; return ( @@ -28,7 +30,7 @@ const EditOrganizationFormContainer = ({ onCancel }: EditOrganizationFormContain currentOrganizationName={currentOrganizationName} onSubmit={onSubmit} onCancel={onCancel} - isLoading={isLoading} + isLoading={loading} /> ); }; diff --git a/ngui/ui/src/containers/GenerateLiveDemoContainer/GenerateLiveDemoContainer.tsx b/ngui/ui/src/containers/GenerateLiveDemoContainer/GenerateLiveDemoContainer.tsx index 0ad83b28..91e0b797 100644 --- a/ngui/ui/src/containers/GenerateLiveDemoContainer/GenerateLiveDemoContainer.tsx +++ b/ngui/ui/src/containers/GenerateLiveDemoContainer/GenerateLiveDemoContainer.tsx @@ -1,25 +1,38 @@ import { useEffect, useState } from "react"; +import { useMutation } from "@apollo/client"; import { useDispatch } from "react-redux"; import { useNavigate } from "react-router-dom"; -import { getLiveDemo, createLiveDemo, RESTAPI, AUTH } from "api"; -import { GET_TOKEN } from "api/auth/actionTypes"; +import { getLiveDemo, createLiveDemo, RESTAPI } from "api"; import { GET_LIVE_DEMO, CREATE_LIVE_DEMO } from "api/restapi/actionTypes"; import GenerateLiveDemo from "components/GenerateLiveDemo"; +import { initialize } from "containers/InitializeContainer/redux"; +import { CREATE_TOKEN } from "graphql/api/auth/queries"; import { useApiState } from "hooks/useApiState"; -import { useAuthorization } from "hooks/useAuthorization"; import { reset } from "reducers/route"; import { HOME } from "urls"; import { isError } from "utils/api"; +import macaroon from "utils/macaroons"; import { getQueryParams } from "utils/network"; -const GenerateLiveDemoContainer = ({ email, subscribeToNewsletter }) => { +type GenerateLiveDemoContainerProps = { + email?: string; + subscribeToNewsletter?: boolean; +}; + +const GenerateLiveDemoContainer = ({ email, subscribeToNewsletter }: GenerateLiveDemoContainerProps) => { const dispatch = useDispatch(); const navigate = useNavigate(); const [hasError, setHasError] = useState(false); const { isLoading: isGetLiveDemoLoading } = useApiState(GET_LIVE_DEMO); const { isLoading: isCreateLiveDemoLoading } = useApiState(CREATE_LIVE_DEMO); - const { authorize, isLoading: isAuthorizationLoading } = useAuthorization(); + + const [createToken, { loading: loginLoading }] = useMutation(CREATE_TOKEN, { + onCompleted: (data) => { + const caveats = macaroon.processCaveats(macaroon.deserialize(data.token.token).getCaveats()); + dispatch(initialize({ ...data.token, caveats })); + } + }); useEffect(() => { const activeLiveDemo = (handlers) => (_, getState) => { @@ -53,13 +66,13 @@ const GenerateLiveDemoContainer = ({ email, subscribeToNewsletter }) => { const generatedEmail = getState()?.[RESTAPI]?.[GET_LIVE_DEMO].email ?? ""; const generatedPassword = getState()?.[RESTAPI]?.[GET_LIVE_DEMO].password ?? ""; if (generatedEmail && generatedPassword) { - return authorize(generatedEmail, generatedPassword); + return createToken({ variables: { email: generatedEmail, password: generatedPassword } }); } return Promise.reject(); }) .then(() => { - const token = getState()?.[AUTH]?.[GET_TOKEN]?.token ?? ""; + const { token } = getState()?.initial ?? {}; if (token) { return Promise.reject(handlers.redirect); @@ -80,11 +93,11 @@ const GenerateLiveDemoContainer = ({ email, subscribeToNewsletter }) => { }) ); } - }, [hasError, dispatch, navigate, authorize, email, subscribeToNewsletter]); + }, [hasError, dispatch, navigate, email, subscribeToNewsletter, createToken]); return ( setHasError(false)} /> diff --git a/ngui/ui/src/containers/GetCloudAccountsContainer/GetCloudAccountsContainer.tsx b/ngui/ui/src/containers/GetCloudAccountsContainer/GetCloudAccountsContainer.tsx index 2409046d..de8f0e8d 100644 --- a/ngui/ui/src/containers/GetCloudAccountsContainer/GetCloudAccountsContainer.tsx +++ b/ngui/ui/src/containers/GetCloudAccountsContainer/GetCloudAccountsContainer.tsx @@ -1,16 +1,15 @@ import { useEffect } from "react"; import { useDispatch } from "react-redux"; import { getPool } from "api"; -import { GET_DATA_SOURCES, GET_POOL } from "api/restapi/actionTypes"; +import { GET_POOL } from "api/restapi/actionTypes"; import CloudAccountsOverview from "components/CloudAccountsOverview"; +import { useAllDataSources } from "hooks/coreData"; import { useApiData } from "hooks/useApiData"; import { useApiState } from "hooks/useApiState"; import { useOrganizationInfo } from "hooks/useOrganizationInfo"; const GetCloudAccountsContainer = () => { - const { - apiData: { cloudAccounts = [] } - } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); const { organizationPoolId } = useOrganizationInfo(); @@ -31,7 +30,7 @@ const GetCloudAccountsContainer = () => { }, [shouldInvokeGetPool, dispatch, organizationPoolId]); return ( - + ); }; diff --git a/ngui/ui/src/containers/InitializeContainer/AcceptInvitations.tsx b/ngui/ui/src/containers/InitializeContainer/AcceptInvitations.tsx new file mode 100644 index 00000000..4d608e35 --- /dev/null +++ b/ngui/ui/src/containers/InitializeContainer/AcceptInvitations.tsx @@ -0,0 +1,88 @@ +import { useMutation } from "@apollo/client"; +import NavigationIcon from "@mui/icons-material/Navigation"; +import { Box } from "@mui/material"; +import { useNavigate } from "react-router-dom"; +import { makeStyles } from "tss-react/mui"; +import ButtonLoader from "components/ButtonLoader"; +import Invitations from "components/Invitations"; +import { CREATE_ORGANIZATION } from "graphql/api/restapi/queries"; +import { isEmpty as isEmptyArray } from "utils/arrays"; +import { SPACING_1, SPACING_2 } from "utils/layouts"; + +const useStyles = makeStyles()((theme) => ({ + dashboardButton: { + [theme.breakpoints.up("md")]: { + position: "absolute", + right: 40, + bottom: 40 + }, + padding: theme.spacing(SPACING_2), + "& > *:not(:last-child)": { + marginRight: theme.spacing(SPACING_1) + } + } +})); + +const AcceptInvitations = ({ + invitations, + refetchInvitations, + refetchOrganizations, + userEmail, + organizations, + getRedirectionPath +}) => { + const navigate = useNavigate(); + + const { classes } = useStyles(); + + const [createOrganization, { loading: createOrganizationLoading }] = useMutation(CREATE_ORGANIZATION); + + return ( + <> + + { + refetchInvitations(); + refetchOrganizations(); + }} + onSuccessDecline={() => { + refetchInvitations(); + }} + isLoading={false} + /> + + + { + const redirect = () => navigate(getRedirectionPath(userEmail)); + + if (isEmptyArray(organizations)) { + createOrganization({ + variables: { + organizationName: `${userEmail}'s Organization` + } + }) + .then(() => refetchOrganizations()) + .then(() => { + redirect(); + }); + } else { + redirect(); + } + }} + isLoading={createOrganizationLoading} + startIcon={} + customWrapperClass={classes.dashboardButton} + /> + + + ); +}; + +export default AcceptInvitations; diff --git a/ngui/ui/src/containers/InitializeContainer/InitializeContainer.tsx b/ngui/ui/src/containers/InitializeContainer/InitializeContainer.tsx new file mode 100644 index 00000000..c876c926 --- /dev/null +++ b/ngui/ui/src/containers/InitializeContainer/InitializeContainer.tsx @@ -0,0 +1,112 @@ +import { useQuery } from "@apollo/client"; +import { Box, Stack, CircularProgress } from "@mui/material"; +import { FormattedMessage } from "react-intl"; +import { Navigate } from "react-router-dom"; +import Logo from "components/Logo"; +import PageTitle from "components/PageTitle"; +import { GET_ORGANIZATIONS, GET_INVITATIONS } from "graphql/api/restapi/queries"; +import { useGetToken } from "hooks/useGetToken"; +import { HOME, NEXT_QUERY_PARAMETER_NAME, SHOW_POLICY_QUERY_PARAM, USER_EMAIL_QUERY_PARAMETER_NAME } from "urls"; +import { isEmpty as isEmptyArray } from "utils/arrays"; +import { SPACING_6 } from "utils/layouts"; +import { getQueryParams } from "utils/network"; +import AcceptInvitations from "./AcceptInvitations"; +import SetupOrganization from "./SetupOrganization"; + +const getRedirectionPath = (scopeUserEmail: string) => { + const { + [NEXT_QUERY_PARAMETER_NAME]: next = HOME, + [USER_EMAIL_QUERY_PARAMETER_NAME]: userEmailQueryParameter, + [SHOW_POLICY_QUERY_PARAM]: showPolicyQueryParameter = false + } = getQueryParams() as { + [NEXT_QUERY_PARAMETER_NAME]: string; + [USER_EMAIL_QUERY_PARAMETER_NAME]: string; + [SHOW_POLICY_QUERY_PARAM]: string; + }; + + const getNextPath = () => { + if (userEmailQueryParameter) { + return userEmailQueryParameter === scopeUserEmail ? next : HOME; + } + + return next; + }; + + const getSearchParams = () => { + const searchParams = [showPolicyQueryParameter ? `${SHOW_POLICY_QUERY_PARAM}=${showPolicyQueryParameter}` : ""].join("&"); + return searchParams; + }; + + return `${getNextPath()}?${getSearchParams()}`; +}; + +const InitializeContainer = () => { + const { userEmail } = useGetToken(); + + const { + data: organizations, + loading: getOrganizationsLoading, + error: getOrganizationsError, + refetch: refetchOrganizations + } = useQuery(GET_ORGANIZATIONS, { + fetchPolicy: "network-only" + }); + + const { + data: invitations, + loading: getInvitationsLoading, + error: getInvitationsError, + refetch: refetchInvitations + } = useQuery(GET_INVITATIONS, { + fetchPolicy: "network-only" + }); + + const isLoading = getOrganizationsLoading || getInvitationsLoading; + + const error = getOrganizationsError || getInvitationsError; + + const renderContent = () => { + if (!isEmptyArray(invitations?.invitations ?? [])) { + return ( + + ); + } + + if (isEmptyArray(organizations?.organizations ?? [])) { + return ; + } + + return ; + }; + + return ( + + + + + {isLoading ? ( + <> + + + + + + + + + + ) : ( + <>{error ?
Display error(s), possible actions are TBD
: renderContent()} + )} +
+ ); +}; + +export default InitializeContainer; diff --git a/ngui/ui/src/containers/InitializeContainer/SetupOrganization.tsx b/ngui/ui/src/containers/InitializeContainer/SetupOrganization.tsx new file mode 100644 index 00000000..afe748e4 --- /dev/null +++ b/ngui/ui/src/containers/InitializeContainer/SetupOrganization.tsx @@ -0,0 +1,33 @@ +import { useEffect } from "react"; +import { useMutation } from "@apollo/client"; +import { Box, CircularProgress } from "@mui/material"; +import { FormattedMessage } from "react-intl"; +import PageTitle from "components/PageTitle"; +import { CREATE_ORGANIZATION } from "graphql/api/restapi/queries/restapi.queries"; + +const SetupOrganization = ({ userEmail, refetchOrganizations }) => { + const [createOrganization] = useMutation(CREATE_ORGANIZATION); + + useEffect(() => { + createOrganization({ + variables: { + organizationName: `${userEmail}'s Organization` + } + }).then(() => refetchOrganizations()); + }, [createOrganization, refetchOrganizations, userEmail]); + + return ( + <> + + + + + + + + + + ); +}; + +export default SetupOrganization; diff --git a/ngui/ui/src/containers/InitializeContainer/index.ts b/ngui/ui/src/containers/InitializeContainer/index.ts new file mode 100644 index 00000000..f344d2b6 --- /dev/null +++ b/ngui/ui/src/containers/InitializeContainer/index.ts @@ -0,0 +1,3 @@ +import InitializeContainer from "./InitializeContainer"; + +export default InitializeContainer; diff --git a/ngui/ui/src/containers/InitializeContainer/redux/actionCreators.ts b/ngui/ui/src/containers/InitializeContainer/redux/actionCreators.ts new file mode 100644 index 00000000..bed9aa20 --- /dev/null +++ b/ngui/ui/src/containers/InitializeContainer/redux/actionCreators.ts @@ -0,0 +1,6 @@ +import { INITIALIZE } from "./actionTypes"; + +export const initialize = (value) => ({ + type: INITIALIZE, + payload: value +}); diff --git a/ngui/ui/src/containers/InitializeContainer/redux/actionTypes.ts b/ngui/ui/src/containers/InitializeContainer/redux/actionTypes.ts new file mode 100644 index 00000000..af6b2635 --- /dev/null +++ b/ngui/ui/src/containers/InitializeContainer/redux/actionTypes.ts @@ -0,0 +1 @@ +export const INITIALIZE = "INITIALIZE"; diff --git a/ngui/ui/src/containers/InitializeContainer/redux/index.ts b/ngui/ui/src/containers/InitializeContainer/redux/index.ts new file mode 100644 index 00000000..6f60349a --- /dev/null +++ b/ngui/ui/src/containers/InitializeContainer/redux/index.ts @@ -0,0 +1,6 @@ +import { initialize } from "./actionCreators"; +import { INITIALIZE } from "./actionTypes"; +import reducer, { INITIAL } from "./reducer"; + +export { INITIAL, INITIALIZE, initialize }; +export default reducer; diff --git a/ngui/ui/src/containers/InitializeContainer/redux/reducer.ts b/ngui/ui/src/containers/InitializeContainer/redux/reducer.ts new file mode 100644 index 00000000..e7bbadad --- /dev/null +++ b/ngui/ui/src/containers/InitializeContainer/redux/reducer.ts @@ -0,0 +1,14 @@ +import { INITIALIZE } from "./actionTypes"; + +export const INITIAL = "initial"; + +const reducer = (state = {}, action) => { + switch (action.type) { + case INITIALIZE: + return { ...state, ...action.payload }; + default: + return state; + } +}; + +export default reducer; diff --git a/ngui/ui/src/containers/IntegrationJiraContainer/IntegrationJiraContainer.tsx b/ngui/ui/src/containers/IntegrationJiraContainer/IntegrationJiraContainer.tsx index e0787f7c..1d5f0007 100644 --- a/ngui/ui/src/containers/IntegrationJiraContainer/IntegrationJiraContainer.tsx +++ b/ngui/ui/src/containers/IntegrationJiraContainer/IntegrationJiraContainer.tsx @@ -1,6 +1,5 @@ -import { GET_CURRENT_EMPLOYEE } from "api/restapi/actionTypes"; import Jira from "components/Integrations/Jira"; -import { useApiData } from "hooks/useApiData"; +import { useCurrentEmployee } from "hooks/coreData"; import EmployeesService from "services/EmployeesService"; import JiraOrganizationStatusService from "services/JiraOrganizationStatusService"; @@ -11,8 +10,7 @@ const IntegrationJiraContainer = () => { const { useGet: useGetJiraOrganizationStatus } = JiraOrganizationStatusService(); const { isLoading: isGetJiraOrganizationStatusLoading, connectedTenants } = useGetJiraOrganizationStatus(); - const { apiData: { currentEmployee: { jira_connected: isCurrentEmployeeConnectedToJira = false } = {} } = {} } = - useApiData(GET_CURRENT_EMPLOYEE); + const { jira_connected: isCurrentEmployeeConnectedToJira = false } = useCurrentEmployee(); return ( { const { useGet: useGetEmployees } = EmployeesService(); const { isLoading: isGetEmployeesLoading, employees } = useGetEmployees(); - const { apiData: { currentEmployee: { slack_connected: isCurrentEmployeeConnectedToSlack = false } = {} } = {} } = - useApiData(GET_CURRENT_EMPLOYEE); const { loading: isGetSlackInstallationPathLoading, data } = useQuery(GET_INSTALLATION_PATH); + const { slack_connected: isCurrentEmployeeConnectedToSlack = false } = useCurrentEmployee(); + return ( { - const dispatch = useDispatch(); - - const { isLoading, entityId } = useApiState(UPDATE_INVITATION); + const [updateInvitation, { loading: loginLoading }] = useMutation(UPDATE_INVITATION); const onAccept = () => { - dispatch((_, getState) => { - dispatch(updateInvitation(invitationId, "accept")).then(() => { - if (typeof onSuccessAccept === "function" && !isError(UPDATE_INVITATION, getState())) { - onSuccessAccept(); - } - return undefined; - }); + updateInvitation({ variables: { invitationId, action: "accept" } }).then(() => { + if (typeof onSuccessAccept === "function") { + onSuccessAccept(); + } }); }; const onDecline = () => { - dispatch((_, getState) => { - dispatch(updateInvitation(invitationId, "decline")).then(() => { - if (typeof onSuccessDecline === "function" && !isError(UPDATE_INVITATION, getState())) { - onSuccessDecline(); - } - return undefined; - }); + updateInvitation({ variables: { invitationId, action: "decline" } }).then(() => { + if (typeof onSuccessAccept === "function") { + onSuccessDecline(); + } }); }; - const isButtonLoading = entityId === invitationId && isLoading; + const isButtonLoading = loginLoading; return ( <> diff --git a/ngui/ui/src/containers/InvitationsContainer/InvitationsContainer.tsx b/ngui/ui/src/containers/InvitationsContainer/InvitationsContainer.tsx index 3418f5c3..569c177e 100644 --- a/ngui/ui/src/containers/InvitationsContainer/InvitationsContainer.tsx +++ b/ngui/ui/src/containers/InvitationsContainer/InvitationsContainer.tsx @@ -1,12 +1,36 @@ +import { NetworkStatus, useLazyQuery, useQuery } from "@apollo/client"; import Invitations from "components/Invitations"; -import InvitationsService from "services/InvitationsService"; +import { GET_INVITATIONS, GET_ORGANIZATIONS } from "graphql/api/restapi/queries"; +import { useOrganizationInfo } from "hooks/useOrganizationInfo"; const InvitationsContainer = () => { - const { useGet } = InvitationsService(); + const { organizationId } = useOrganizationInfo(); - const { isLoading, invitations } = useGet(); + const { + data: { invitations = [] } = {}, + networkStatus, + refetch: refetchInvitations + } = useQuery(GET_INVITATIONS, { + variables: { + organizationId + }, + notifyOnNetworkStatusChange: true + }); - return ; + const [getOrganizations] = useLazyQuery(GET_ORGANIZATIONS, { + fetchPolicy: "network-only" + }); + + const isLoading = networkStatus === NetworkStatus.loading || networkStatus === NetworkStatus.refetch; + + const onSuccess = () => { + refetchInvitations(); + getOrganizations(); + }; + + return ( + + ); }; export default InvitationsContainer; diff --git a/ngui/ui/src/containers/InvitedContainer/InvitedContainer.tsx b/ngui/ui/src/containers/InvitedContainer/InvitedContainer.tsx index 36ced27f..c6ddcf2d 100644 --- a/ngui/ui/src/containers/InvitedContainer/InvitedContainer.tsx +++ b/ngui/ui/src/containers/InvitedContainer/InvitedContainer.tsx @@ -1,25 +1,22 @@ import { Box, CircularProgress } from "@mui/material"; -import { GET_TOKEN } from "api/auth/actionTypes"; +import { useNavigate } from "react-router-dom"; import Logo from "components/Logo"; import Redirector from "components/Redirector"; import WrongInvitationEmailAlert from "components/WrongInvitationEmailAlert"; -import { useApiData } from "hooks/useApiData"; -import { useNewAuthorization } from "hooks/useNewAuthorization"; +import { useGetToken } from "hooks/useGetToken"; import { useSignOut } from "hooks/useSignOut"; import { SETTINGS_TABS } from "pages/Settings/Settings"; import UserService from "services/UserService"; -import { REGISTER, getSettingsUrl } from "urls"; +import { HOME, REGISTER, getSettingsUrl } from "urls"; import { SPACING_6 } from "utils/layouts"; import { getQueryParams, getStringUrl } from "utils/network"; const InvitedContainer = () => { const signOut = useSignOut(); - const { - apiData: { token, userId } - } = useApiData(GET_TOKEN); + const { token, userId } = useGetToken(); - const { activateScope } = useNewAuthorization(); + const navigate = useNavigate(); const { useGet } = UserService(); const { isDataReady, user } = useGet(userId); @@ -59,7 +56,7 @@ const InvitedContainer = () => { activateScope(currentEmail)} + onGoToDashboard={() => navigate(HOME)} onSignOut={onSignOut} /> ) : ( diff --git a/ngui/ui/src/containers/MainLayoutContainer/MainLayoutContainer.tsx b/ngui/ui/src/containers/MainLayoutContainer/MainLayoutContainer.tsx deleted file mode 100644 index 51d09935..00000000 --- a/ngui/ui/src/containers/MainLayoutContainer/MainLayoutContainer.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { useEffect } from "react"; -import CircularProgress from "@mui/material/CircularProgress"; -import { useDispatch } from "react-redux"; -import { - getOrganizations, - getCurrentEmployee, - getDataSources, - getOrganizationAllowedActions, - getOrganizationFeatures, - getOrganizationThemeSettings, - getInvitations, - getOrganizationPerspectives -} from "api"; -import { GET_ORGANIZATION_ALLOWED_ACTIONS } from "api/auth/actionTypes"; -import { - GET_ORGANIZATIONS, - GET_CURRENT_EMPLOYEE, - GET_DATA_SOURCES, - GET_ORGANIZATION_FEATURES, - GET_ORGANIZATION_THEME_SETTINGS, - GET_INVITATIONS, - GET_ORGANIZATION_PERSPECTIVES -} from "api/restapi/actionTypes"; -import Backdrop from "components/Backdrop"; -import { useApiState } from "hooks/useApiState"; -import { useInitialMount } from "hooks/useInitialMount"; -import { useIsLoading } from "hooks/useIsLoading"; -import { useOrganizationInfo } from "hooks/useOrganizationInfo"; -import { useShouldRenderLoader } from "hooks/useShouldRenderLoader"; - -const MainLayoutContainer = ({ children }) => { - const dispatch = useDispatch(); - - const { isInitialMount, setIsInitialMount } = useInitialMount(); - - useEffect(() => { - if (isInitialMount) { - setIsInitialMount(false); - } - }, [isInitialMount, setIsInitialMount]); - - const { organizationId } = useOrganizationInfo(); - - const { shouldInvoke: shouldInvokeGetOrganizations } = useApiState(GET_ORGANIZATIONS); - const { shouldInvoke: shouldInvokeGetAllowedActions } = useApiState(GET_ORGANIZATION_ALLOWED_ACTIONS, organizationId); - const { shouldInvoke: shouldInvokeGetCurrentEmployee } = useApiState(GET_CURRENT_EMPLOYEE, organizationId); - const { shouldInvoke: shouldInvokeCloudAccounts } = useApiState(GET_DATA_SOURCES, organizationId); - const { shouldInvoke: shouldInvokeGetOrganizationsFeatures } = useApiState(GET_ORGANIZATION_FEATURES, organizationId); - const { shouldInvoke: shouldInvokeGetOrganizationThemeSettings } = useApiState( - GET_ORGANIZATION_THEME_SETTINGS, - organizationId - ); - const { shouldInvoke: shouldInvokeGetInvitations } = useApiState(GET_INVITATIONS); - const { shouldInvoke: shouldInvokeGetOrganizationPerspectives } = useApiState(GET_ORGANIZATION_PERSPECTIVES, organizationId); - - useEffect(() => { - if (shouldInvokeGetOrganizations) { - dispatch(getOrganizations()); - } - }, [shouldInvokeGetOrganizations, dispatch]); - - useEffect(() => { - if (organizationId && shouldInvokeGetAllowedActions) { - dispatch(getOrganizationAllowedActions(organizationId)); - } - }, [dispatch, organizationId, shouldInvokeGetAllowedActions]); - - useEffect(() => { - if (organizationId && shouldInvokeGetCurrentEmployee) { - dispatch(getCurrentEmployee(organizationId)); - } - }, [dispatch, organizationId, shouldInvokeGetCurrentEmployee]); - - useEffect(() => { - if (organizationId && shouldInvokeCloudAccounts) { - dispatch(getDataSources(organizationId)); - } - }, [dispatch, organizationId, shouldInvokeCloudAccounts]); - - useEffect(() => { - if (organizationId && shouldInvokeGetOrganizationsFeatures) { - dispatch(getOrganizationFeatures(organizationId)); - } - }, [dispatch, organizationId, shouldInvokeGetOrganizationsFeatures]); - - useEffect(() => { - if (organizationId && shouldInvokeGetOrganizationThemeSettings) { - dispatch(getOrganizationThemeSettings(organizationId)); - } - }, [dispatch, organizationId, shouldInvokeGetOrganizationThemeSettings]); - - useEffect(() => { - if (shouldInvokeGetInvitations) { - dispatch(getInvitations()); - } - }, [dispatch, shouldInvokeGetInvitations]); - - useEffect(() => { - if (organizationId && shouldInvokeGetOrganizationPerspectives) { - dispatch(getOrganizationPerspectives(organizationId)); - } - }, [dispatch, organizationId, shouldInvokeGetOrganizationPerspectives]); - - const apiIsLoading = useIsLoading([ - GET_ORGANIZATIONS, - GET_ORGANIZATION_ALLOWED_ACTIONS, - GET_CURRENT_EMPLOYEE, - GET_DATA_SOURCES, - GET_ORGANIZATION_FEATURES - ]); - const isLoading = useShouldRenderLoader(isInitialMount, [apiIsLoading]); - - return isLoading ? ( - - - - ) : ( - children - ); -}; - -export default MainLayoutContainer; diff --git a/ngui/ui/src/containers/MainLayoutContainer/index.ts b/ngui/ui/src/containers/MainLayoutContainer/index.ts deleted file mode 100644 index 481205ab..00000000 --- a/ngui/ui/src/containers/MainLayoutContainer/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import MainLayoutContainer from "./MainLayoutContainer"; - -export default MainLayoutContainer; diff --git a/ngui/ui/src/containers/MlRunsetTemplateCreateFormContainer/MlRunsetTemplateCreateFormContainer.tsx b/ngui/ui/src/containers/MlRunsetTemplateCreateFormContainer/MlRunsetTemplateCreateFormContainer.tsx index c5c6c5f9..14a89273 100644 --- a/ngui/ui/src/containers/MlRunsetTemplateCreateFormContainer/MlRunsetTemplateCreateFormContainer.tsx +++ b/ngui/ui/src/containers/MlRunsetTemplateCreateFormContainer/MlRunsetTemplateCreateFormContainer.tsx @@ -1,8 +1,7 @@ import { useNavigate } from "react-router-dom"; -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; import MlRunsetTemplateForm from "components/forms/MlRunsetTemplateForm"; import { FIELD_NAMES } from "components/forms/MlRunsetTemplateForm/constants"; -import { useApiData } from "hooks/useApiData"; +import { useAllDataSources } from "hooks/coreData"; import MlRunsetTemplatesService from "services/MlRunsetTemplatesService"; import MlTasksService from "services/MlTasksService"; import { ML_RUNSET_TEMPLATES } from "urls"; @@ -33,9 +32,7 @@ const MlRunsetTemplateCreateFormContainer = () => { const { useGetAll } = MlTasksService(); const { isLoading: isGetAllTasksLoading, tasks } = useGetAll(); - const { - apiData: { cloudAccounts: dataSources = [] } - } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); const { useCreateMlRunsetTemplate } = MlRunsetTemplatesService(); const { onCreate, isLoading: isCreateMlRunsetTemplateLoading } = useCreateMlRunsetTemplate(); diff --git a/ngui/ui/src/containers/MlRunsetTemplateEditContainer/MlRunsetTemplateEditContainer.tsx b/ngui/ui/src/containers/MlRunsetTemplateEditContainer/MlRunsetTemplateEditContainer.tsx index ce035373..c88ddb7c 100644 --- a/ngui/ui/src/containers/MlRunsetTemplateEditContainer/MlRunsetTemplateEditContainer.tsx +++ b/ngui/ui/src/containers/MlRunsetTemplateEditContainer/MlRunsetTemplateEditContainer.tsx @@ -1,7 +1,6 @@ import { useNavigate, useParams } from "react-router-dom"; -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; import MlRunsetTemplateEdit from "components/MlRunsetTemplateEdit"; -import { useApiData } from "hooks/useApiData"; +import { useAllDataSources } from "hooks/coreData"; import MlRunsetTemplatesService from "services/MlRunsetTemplatesService"; import MlTasksService from "services/MlTasksService"; import { getMlRunsetTemplateUrl } from "urls"; @@ -16,9 +15,7 @@ const MlRunsetTemplateEditContainer = () => { const { useGetAll } = MlTasksService(); const { isLoading: isGetAllTasksLoading, tasks } = useGetAll(); - const { - apiData: { cloudAccounts: dataSources = [] } - } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); const { useUpdateRunsetTemplate, useGetOne } = MlRunsetTemplatesService(); const { onUpdate, isLoading: isUpdateMlRunsetTemplateLoading } = useUpdateRunsetTemplate(); diff --git a/ngui/ui/src/containers/ModeContainer/ModeContainer.tsx b/ngui/ui/src/containers/ModeContainer/ModeContainer.tsx index 1a16cda1..bb291659 100644 --- a/ngui/ui/src/containers/ModeContainer/ModeContainer.tsx +++ b/ngui/ui/src/containers/ModeContainer/ModeContainer.tsx @@ -1,27 +1,35 @@ +import { useMutation } from "@apollo/client"; import Mode from "components/Mode"; -import OrganizationOptionsService from "services/OrganizationOptionsService"; -import { OPTSCALE_MODE_OPTION } from "utils/constants"; +import { GET_OPTSCALE_MODE, UPDATE_OPTSCALE_MODE } from "graphql/api/restapi/queries"; +import { useGetOptscaleMode } from "hooks/coreData"; +import { useOrganizationInfo } from "hooks/useOrganizationInfo"; const ModeContainer = () => { - const { useGetOptscaleMode, useUpdateOption } = OrganizationOptionsService(); + const { organizationId } = useOrganizationInfo(); + const { optscaleMode } = useGetOptscaleMode(); - const { - isGetOrganizationOptionLoading, - option: { value } - } = useGetOptscaleMode(OPTSCALE_MODE_OPTION); - const { isUpdateOrganizationOptionLoading, updateOption } = useUpdateOption(); + const [updateOptscaleModeMutation, { loading }] = useMutation(UPDATE_OPTSCALE_MODE, { + update: (cache, { data: { updateOptscaleMode } }) => { + const { optscaleMode: cacheOptscaleMode } = cache.readQuery({ query: GET_OPTSCALE_MODE, variables: { organizationId } }); + + cache.writeQuery({ + query: GET_OPTSCALE_MODE, + variables: { organizationId }, + data: { + optscaleMode: { + ...cacheOptscaleMode, + ...updateOptscaleMode + } + } + }); + } + }); const onApply = (option) => { - updateOption(OPTSCALE_MODE_OPTION, { value: option }); + updateOptscaleModeMutation({ variables: { organizationId, value: option } }); }; - return ( - - ); + return ; }; export default ModeContainer; diff --git a/ngui/ui/src/containers/OrganizationSelectorContainer/OrganizationSelectorContainer.tsx b/ngui/ui/src/containers/OrganizationSelectorContainer/OrganizationSelectorContainer.tsx index 86d0ab43..bbf6bdf9 100644 --- a/ngui/ui/src/containers/OrganizationSelectorContainer/OrganizationSelectorContainer.tsx +++ b/ngui/ui/src/containers/OrganizationSelectorContainer/OrganizationSelectorContainer.tsx @@ -1,52 +1,27 @@ -import { useDispatch } from "react-redux"; -import { useNavigate } from "react-router-dom"; -import { GET_ORGANIZATIONS } from "api/restapi/actionTypes"; +import { useQuery } from "@apollo/client"; import OrganizationSelector from "components/OrganizationSelector"; -import { useApiData } from "hooks/useApiData"; -import { useApiState } from "hooks/useApiState"; +import { GET_ORGANIZATIONS } from "graphql/api/restapi/queries"; import { useOrganizationInfo } from "hooks/useOrganizationInfo"; -import { formQueryString, getMenuRootUrl, getQueryParams, removeQueryParam } from "utils/network"; -import requestManager from "utils/requestManager"; -import { setScopeId } from "./actionCreators"; -import { SCOPE_ID } from "./reducer"; +import { useUpdateScope } from "hooks/useUpdateScope"; +import { HOME } from "urls"; -const OrganizationSelectorContainer = ({ mainMenu }) => { - const dispatch = useDispatch(); - const { - apiData: { organizations } - } = useApiData(GET_ORGANIZATIONS); - - const { isLoading } = useApiState(GET_ORGANIZATIONS); +const OrganizationSelectorContainer = () => { + const { data: { organizations = [] } = {} } = useQuery(GET_ORGANIZATIONS, { + fetchPolicy: "cache-only" + }); const { organizationId } = useOrganizationInfo(); - const navigate = useNavigate(); - - const handleScopeChange = (scopeId) => { - requestManager.cancelAllPendingRequests(); - removeQueryParam(SCOPE_ID); - - // The straightforward solution to persist query parameters when changing the organization - // More context: - // * https://gitlab.com/hystax/ngui/-/merge_requests/2773 - // * https://datatrendstech.atlassian.net/browse/OS-4786 - const { type } = getQueryParams(); - - const to = [getMenuRootUrl(mainMenu), formQueryString({ type })].join("?"); - - navigate(to); + const updateScope = useUpdateScope(); - dispatch(setScopeId(scopeId)); + const handleScopeChange = (scopeId: string) => { + updateScope({ + newScopeId: scopeId, + redirectTo: HOME + }); }; - return ( - - ); + return ; }; export default OrganizationSelectorContainer; diff --git a/ngui/ui/src/containers/OrganizationThemeSettingsContainer/OrganizationThemeSettingsContainer.tsx b/ngui/ui/src/containers/OrganizationThemeSettingsContainer/OrganizationThemeSettingsContainer.tsx index 02b22f81..c7797e80 100644 --- a/ngui/ui/src/containers/OrganizationThemeSettingsContainer/OrganizationThemeSettingsContainer.tsx +++ b/ngui/ui/src/containers/OrganizationThemeSettingsContainer/OrganizationThemeSettingsContainer.tsx @@ -1,15 +1,35 @@ +import { useMutation } from "@apollo/client"; import ContentBackdropLoader from "components/ContentBackdropLoader"; -import OrganizationOptionsService from "services/OrganizationOptionsService"; +import { + GET_ORGANIZATION_THEME_SETTINGS, + UPDATE_ORGANIZATION_THEME_SETTINGS +} from "graphql/api/restapi/queries/restapi.queries"; +import { useOrganizationInfo } from "hooks/useOrganizationInfo"; const OrganizationThemeSettingsContainer = ({ children }) => { - const { useUpdateThemeSettings } = OrganizationOptionsService(); - const { update, isLoading } = useUpdateThemeSettings(); + const { organizationId } = useOrganizationInfo(); - const onUpdate = (data) => { - update(data); - }; + const [updateOrganizationThemeSettingsMutation, { loading }] = useMutation(UPDATE_ORGANIZATION_THEME_SETTINGS, { + update: (cache, { data: { updateOrganizationThemeSettings } }) => { + cache.writeQuery({ + query: GET_ORGANIZATION_THEME_SETTINGS, + variables: { organizationId }, + data: { + organizationThemeSettings: updateOrganizationThemeSettings + } + }); + } + }); - return {children(onUpdate)}; + const onUpdate = (data) => + updateOrganizationThemeSettingsMutation({ + variables: { + organizationId, + value: data + } + }); + + return {children(onUpdate)}; }; export default OrganizationThemeSettingsContainer; diff --git a/ngui/ui/src/containers/ProfileMenuContainer/ProfileMenuContainer.test.tsx b/ngui/ui/src/containers/ProfileMenuContainer/ProfileMenuContainer.test.tsx index 03ce57c0..ec2e53e2 100644 --- a/ngui/ui/src/containers/ProfileMenuContainer/ProfileMenuContainer.test.tsx +++ b/ngui/ui/src/containers/ProfileMenuContainer/ProfileMenuContainer.test.tsx @@ -8,7 +8,9 @@ it("renders without crashing", () => { root.render( diff --git a/ngui/ui/src/containers/ProfileMenuContainer/ProfileMenuContainer.tsx b/ngui/ui/src/containers/ProfileMenuContainer/ProfileMenuContainer.tsx index 42432e3c..953eeb05 100644 --- a/ngui/ui/src/containers/ProfileMenuContainer/ProfileMenuContainer.tsx +++ b/ngui/ui/src/containers/ProfileMenuContainer/ProfileMenuContainer.tsx @@ -1,12 +1,9 @@ -import { GET_TOKEN } from "api/auth/actionTypes"; import ProfileMenu from "components/ProfileMenu"; -import { useApiData } from "hooks/useApiData"; +import { useGetToken } from "hooks/useGetToken"; import UserService from "services/UserService"; const ProfileMenuContainer = () => { - const { - apiData: { userId } - } = useApiData(GET_TOKEN); + const { userId } = useGetToken(); const { useGet } = UserService(); const { diff --git a/ngui/ui/src/containers/RenameDataSourceContainer/RenameDataSourceContainer.tsx b/ngui/ui/src/containers/RenameDataSourceContainer/RenameDataSourceContainer.tsx index 850f0b03..43c83c17 100644 --- a/ngui/ui/src/containers/RenameDataSourceContainer/RenameDataSourceContainer.tsx +++ b/ngui/ui/src/containers/RenameDataSourceContainer/RenameDataSourceContainer.tsx @@ -1,11 +1,15 @@ import { useMutation } from "@apollo/client"; +import { GET_AVAILABLE_FILTERS } from "api/restapi/actionTypes"; import RenameDataSourceForm from "components/forms/RenameDataSourceForm"; import { FormValues } from "components/forms/RenameDataSourceForm/types"; -import { GET_DATA_SOURCE, UPDATE_DATA_SOURCE } from "graphql/api/restapi/queries/restapi.queries"; +import { UPDATE_DATA_SOURCE } from "graphql/api/restapi/queries/restapi.queries"; +import { useRefetchApis } from "hooks/useRefetchApis"; const RenameDataSourceContainer = ({ id, name, closeSideModal }) => { const [updateDataSource, { loading }] = useMutation(UPDATE_DATA_SOURCE); + const refetch = useRefetchApis(); + const onSubmit = (formData: FormValues) => { updateDataSource({ variables: { @@ -13,9 +17,11 @@ const RenameDataSourceContainer = ({ id, name, closeSideModal }) => { params: { name: formData.name } - }, - refetchQueries: [GET_DATA_SOURCE] - }).then(() => closeSideModal()); + } + }).then(() => { + refetch([GET_AVAILABLE_FILTERS]); + closeSideModal(); + }); }; return ; diff --git a/ngui/ui/src/containers/ResourcesContainer/ResourcesContainer.tsx b/ngui/ui/src/containers/ResourcesContainer/ResourcesContainer.tsx index 22a10757..c1ee3a37 100644 --- a/ngui/ui/src/containers/ResourcesContainer/ResourcesContainer.tsx +++ b/ngui/ui/src/containers/ResourcesContainer/ResourcesContainer.tsx @@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom"; import { areSearchParamsEqual } from "api/utils"; import { RESOURCE_FILTERS_NAMES } from "components/Filters/constants"; import Resources from "components/Resources"; -import { useOrganizationPerspectives } from "hooks/useOrganizationPerspectives"; +import { useOrganizationPerspectives } from "hooks/coreData"; import { useReactiveDefaultDateRange } from "hooks/useReactiveDefaultDateRange"; import { useReactiveSearchParams } from "hooks/useReactiveSearchParams"; import AvailableFiltersService from "services/AvailableFiltersService"; diff --git a/ngui/ui/src/containers/SshSettingsContainer/SshSettingsContainer.tsx b/ngui/ui/src/containers/SshSettingsContainer/SshSettingsContainer.tsx index 062730ff..3c539d76 100644 --- a/ngui/ui/src/containers/SshSettingsContainer/SshSettingsContainer.tsx +++ b/ngui/ui/src/containers/SshSettingsContainer/SshSettingsContainer.tsx @@ -1,8 +1,9 @@ import { useState, useEffect } from "react"; import { useDispatch } from "react-redux"; import { getSshKeys, createSshKey, updateSshKey } from "api"; -import { GET_CURRENT_EMPLOYEE, GET_SSH_KEYS } from "api/restapi/actionTypes"; +import { GET_SSH_KEYS } from "api/restapi/actionTypes"; import SshSettings from "components/SshSettings"; +import { useCurrentEmployee } from "hooks/coreData"; import { useApiData } from "hooks/useApiData"; import { useApiState } from "hooks/useApiState"; @@ -10,7 +11,7 @@ const SshSettingsContainer = () => { const dispatch = useDispatch(); // employee id - const { apiData: { currentEmployee: { id: currentEmployeeId } = {} } = {} } = useApiData(GET_CURRENT_EMPLOYEE); + const { id: currentEmployeeId } = useCurrentEmployee(); // get ssh keys const { shouldInvoke, isLoading: isGetSshKeysLoading } = useApiState(GET_SSH_KEYS, currentEmployeeId); diff --git a/ngui/ui/src/containers/UpdateDataSourceCredentialsContainer/UpdateDataSourceCredentialsContainer.tsx b/ngui/ui/src/containers/UpdateDataSourceCredentialsContainer/UpdateDataSourceCredentialsContainer.tsx index 29bbc981..602a8b5f 100644 --- a/ngui/ui/src/containers/UpdateDataSourceCredentialsContainer/UpdateDataSourceCredentialsContainer.tsx +++ b/ngui/ui/src/containers/UpdateDataSourceCredentialsContainer/UpdateDataSourceCredentialsContainer.tsx @@ -1,9 +1,13 @@ import { useMutation } from "@apollo/client"; +import { GET_AVAILABLE_FILTERS } from "api/restapi/actionTypes"; import UpdateDataSourceCredentialsForm from "components/forms/UpdateDataSourceCredentialsForm"; -import { GET_DATA_SOURCE, UPDATE_DATA_SOURCE } from "graphql/api/restapi/queries/restapi.queries"; +import { UPDATE_DATA_SOURCE } from "graphql/api/restapi/queries/restapi.queries"; +import { useRefetchApis } from "hooks/useRefetchApis"; import { ALIBABA_CNR, AWS_CNR, AZURE_CNR, AZURE_TENANT, DATABRICKS, GCP_CNR, KUBERNETES_CNR, NEBIUS } from "utils/constants"; const UpdateDataSourceCredentialsContainer = ({ id, type, config, closeSideModal }) => { + const refetch = useRefetchApis(); + const [updateDataSource, { loading }] = useMutation(UPDATE_DATA_SOURCE); const onSubmit = (dataSourceId, { config: newConfig }) => { @@ -24,9 +28,11 @@ const UpdateDataSourceCredentialsContainer = ({ id, type, config, closeSideModal params: { [configName]: newConfig } - }, - refetchQueries: [GET_DATA_SOURCE] - }).then(() => closeSideModal()); + } + }).then(() => { + refetch([GET_AVAILABLE_FILTERS]); + closeSideModal(); + }); }; return ( diff --git a/ngui/ui/src/containers/UserEmailNotificationSettingsContainer/UserEmailNotificationSettingsContainer.tsx b/ngui/ui/src/containers/UserEmailNotificationSettingsContainer/UserEmailNotificationSettingsContainer.tsx index a129f234..8fce3478 100644 --- a/ngui/ui/src/containers/UserEmailNotificationSettingsContainer/UserEmailNotificationSettingsContainer.tsx +++ b/ngui/ui/src/containers/UserEmailNotificationSettingsContainer/UserEmailNotificationSettingsContainer.tsx @@ -1,13 +1,10 @@ import { useQuery } from "@apollo/client"; -import { GET_CURRENT_EMPLOYEE } from "api/restapi/actionTypes"; import UserEmailNotificationSettings from "components/UserEmailNotificationSettings"; import { GET_EMPLOYEE_EMAILS } from "graphql/api/restapi/queries/restapi.queries"; -import { useApiData } from "hooks/useApiData"; +import { useCurrentEmployee } from "hooks/coreData"; const UserEmailNotificationSettingsContainer = () => { - const { - apiData: { currentEmployee = {} } - } = useApiData(GET_CURRENT_EMPLOYEE); + const currentEmployee = useCurrentEmployee(); const { loading, data } = useQuery(GET_EMPLOYEE_EMAILS, { variables: { diff --git a/ngui/ui/src/graphql/api/auth/queries/auth.queries.ts b/ngui/ui/src/graphql/api/auth/queries/auth.queries.ts new file mode 100644 index 00000000..e9a92067 --- /dev/null +++ b/ngui/ui/src/graphql/api/auth/queries/auth.queries.ts @@ -0,0 +1,49 @@ +import { gql } from "@apollo/client"; + +const GET_ORGANIZATION_ALLOWED_ACTIONS = gql` + query OrganizationAllowedActions($requestParams: OrganizationAllowedActionsRequestParams) { + organizationAllowedActions(requestParams: $requestParams) + } +`; + +const CREATE_TOKEN = gql` + mutation CreateToken($email: String!, $password: String, $code: String) { + token(email: $email, password: $password, code: $code) { + token + user_id + user_email + } + } +`; + +const CREATE_USER = gql` + mutation CreateUser($email: String!, $password: String!, $name: String!) { + user(email: $email, password: $password, name: $name) { + token + user_id + user_email + } + } +`; + +const SIGN_IN = gql` + mutation SignIn($provider: String!, $token: String!, $tenantId: String, $redirectUri: String) { + signIn(provider: $provider, token: $token, tenantId: $tenantId, redirectUri: $redirectUri) { + token + user_id + user_email + } + } +`; + +const UPDATE_USER = gql` + mutation UpdateUser($id: ID!, $params: UpdateUserParams!) { + updateUser(id: $id, params: $params) { + token + user_id + user_email + } + } +`; + +export { GET_ORGANIZATION_ALLOWED_ACTIONS, CREATE_TOKEN, CREATE_USER, SIGN_IN, UPDATE_USER }; diff --git a/ngui/ui/src/graphql/api/auth/queries/index.ts b/ngui/ui/src/graphql/api/auth/queries/index.ts new file mode 100644 index 00000000..3b5a0f41 --- /dev/null +++ b/ngui/ui/src/graphql/api/auth/queries/index.ts @@ -0,0 +1,3 @@ +import { CREATE_TOKEN, CREATE_USER, GET_ORGANIZATION_ALLOWED_ACTIONS, SIGN_IN, UPDATE_USER } from "./auth.queries"; + +export { CREATE_TOKEN, CREATE_USER, GET_ORGANIZATION_ALLOWED_ACTIONS, SIGN_IN, UPDATE_USER }; diff --git a/ngui/ui/src/graphql/api/restapi/queries/index.ts b/ngui/ui/src/graphql/api/restapi/queries/index.ts index ee27c5be..c9f2fcec 100644 --- a/ngui/ui/src/graphql/api/restapi/queries/index.ts +++ b/ngui/ui/src/graphql/api/restapi/queries/index.ts @@ -1,3 +1,27 @@ -import { GET_DATA_SOURCE } from "./restapi.queries"; +import { + GET_ORGANIZATIONS, + CREATE_ORGANIZATION, + GET_DATA_SOURCES, + GET_CURRENT_EMPLOYEE, + GET_DATA_SOURCE, + GET_INVITATIONS, + GET_ORGANIZATION_FEATURES, + GET_OPTSCALE_MODE, + UPDATE_OPTSCALE_MODE, + GET_ORGANIZATION_THEME_SETTINGS, + GET_ORGANIZATION_PERSPECTIVES +} from "./restapi.queries"; -export { GET_DATA_SOURCE }; +export { + GET_ORGANIZATIONS, + CREATE_ORGANIZATION, + GET_DATA_SOURCES, + GET_CURRENT_EMPLOYEE, + GET_DATA_SOURCE, + GET_INVITATIONS, + GET_ORGANIZATION_FEATURES, + GET_OPTSCALE_MODE, + UPDATE_OPTSCALE_MODE, + GET_ORGANIZATION_THEME_SETTINGS, + GET_ORGANIZATION_PERSPECTIVES +}; diff --git a/ngui/ui/src/graphql/api/restapi/queries/restapi.queries.ts b/ngui/ui/src/graphql/api/restapi/queries/restapi.queries.ts index 8c27e4c4..8316d030 100644 --- a/ngui/ui/src/graphql/api/restapi/queries/restapi.queries.ts +++ b/ngui/ui/src/graphql/api/restapi/queries/restapi.queries.ts @@ -1,5 +1,182 @@ import { gql } from "@apollo/client"; +const AwsDataSourceConfigFragment = gql` + fragment AwsDataSourceConfigFragment on AwsDataSource { + config { + access_key_id + linked + use_edp_discount + cur_version + bucket_name + bucket_prefix + config_scheme + region_name + report_name + } + } +`; + +const AzureTenantDataSourceConfigFragment = gql` + fragment AzureTenantDataSourceConfigFragment on AzureTenantDataSource { + config { + client_id + tenant + } + } +`; + +const AzureSubscriptionDataSourceConfigFragment = gql` + fragment AzureSubscriptionDataSourceConfigFragment on AzureSubscriptionDataSource { + config { + client_id + expense_import_scheme + subscription_id + tenant + } + } +`; + +const GcpDataSourceConfigFragment = gql` + fragment GcpDataSourceConfigFragment on GcpDataSource { + config { + billing_data { + dataset_name + table_name + project_id + } + } + } +`; + +const AlibabaDataSourceConfigFragment = gql` + fragment AlibabaDataSourceConfigFragment on AlibabaDataSource { + config { + access_key_id + } + } +`; + +const NebiusDataSourceConfigFragment = gql` + fragment NebiusDataSourceConfigFragment on NebiusDataSource { + config { + cloud_name + service_account_id + key_id + access_key_id + bucket_name + bucket_prefix + } + } +`; + +const DatabricksDataSourceConfigFragment = gql` + fragment DatabricksDataSourceConfigFragment on DatabricksDataSource { + config { + account_id + client_id + } + } +`; + +const K8sDataSourceConfigFragment = gql` + fragment K8sDataSourceConfigFragment on K8sDataSource { + config { + cost_model { + cpu_hourly_cost + memory_hourly_cost + } + user + } + } +`; + +const GET_ORGANIZATIONS = gql` + query Organizations { + organizations { + id + name + pool_id + currency + is_demo + } + } +`; + +const CREATE_ORGANIZATION = gql` + mutation CreateOrganization($organizationName: String!) { + createOrganization(organizationName: $organizationName) { + id + name + } + } +`; + +const UPDATE_ORGANIZATION = gql` + mutation UpdateOrganization($organizationId: ID!, $params: UpdateOrganizationInput!) { + updateOrganization(organizationId: $organizationId, params: $params) { + id + name + currency + } + } +`; + +const DELETE_ORGANIZATION = gql` + mutation DeleteOrganization($organizationId: ID!) { + deleteOrganization(organizationId: $organizationId) + } +`; + +const GET_CURRENT_EMPLOYEE = gql` + query CurrentEmployee($organizationId: ID!) { + currentEmployee(organizationId: $organizationId) { + id + jira_connected + slack_connected + } + } +`; + +const GET_DATA_SOURCES = gql` + query DataSources($organizationId: ID!) { + dataSources(organizationId: $organizationId) { + account_id + id + last_getting_metric_attempt_at + last_getting_metric_attempt_error + last_getting_metrics_at + last_import_at + last_import_attempt_at + last_import_attempt_error + name + parent_id + type + details { + cost + resources + forecast + last_month_cost + } + ...AwsDataSourceConfigFragment + ...AzureTenantDataSourceConfigFragment + ...AzureSubscriptionDataSourceConfigFragment + ...GcpDataSourceConfigFragment + ...AlibabaDataSourceConfigFragment + ...NebiusDataSourceConfigFragment + ...DatabricksDataSourceConfigFragment + ...K8sDataSourceConfigFragment + } + } + ${AwsDataSourceConfigFragment} + ${AzureTenantDataSourceConfigFragment} + ${AzureSubscriptionDataSourceConfigFragment} + ${GcpDataSourceConfigFragment} + ${AlibabaDataSourceConfigFragment} + ${NebiusDataSourceConfigFragment} + ${DatabricksDataSourceConfigFragment} + ${K8sDataSourceConfigFragment} +`; + const GET_DATA_SOURCE = gql` query DataSource($dataSourceId: ID!, $requestParams: DataSourceRequestParams) { dataSource(dataSourceId: $dataSourceId, requestParams: $requestParams) { @@ -32,79 +209,101 @@ const GET_DATA_SOURCE = gql` last_month_cost resources } - ... on AwsDataSource { - config { - access_key_id - linked - use_edp_discount - cur_version - bucket_name - bucket_prefix - config_scheme - region_name - report_name - } - } - ... on AzureTenantDataSource { - config { - client_id - tenant - } - } - ... on AzureSubscriptionDataSource { - config { - client_id - expense_import_scheme - subscription_id - tenant - } - } - ... on GcpDataSource { - config { - billing_data { - dataset_name - table_name - project_id - } - } - } - ... on AlibabaDataSource { - config { - access_key_id - } - } - ... on NebiusDataSource { - config { - cloud_name - service_account_id - key_id - access_key_id - bucket_name - bucket_prefix - } - } - ... on DatabricksDataSource { - config { - account_id - client_id - } - } - ... on K8sDataSource { - config { - cost_model { - cpu_hourly_cost - memory_hourly_cost - } - user - } + ...AwsDataSourceConfigFragment + ...AzureTenantDataSourceConfigFragment + ...AzureSubscriptionDataSourceConfigFragment + ...GcpDataSourceConfigFragment + ...AlibabaDataSourceConfigFragment + ...NebiusDataSourceConfigFragment + ...DatabricksDataSourceConfigFragment + ...K8sDataSourceConfigFragment + } + } + ${AwsDataSourceConfigFragment} + ${AzureTenantDataSourceConfigFragment} + ${AzureSubscriptionDataSourceConfigFragment} + ${GcpDataSourceConfigFragment} + ${AlibabaDataSourceConfigFragment} + ${NebiusDataSourceConfigFragment} + ${DatabricksDataSourceConfigFragment} + ${K8sDataSourceConfigFragment} +`; + +const GET_INVITATIONS = gql` + query Invitations { + invitations { + id + owner_name + owner_email + organization + invite_assignments { + id + scope_id + scope_type + purpose } } } `; -const UPDATE_DATA_SOURCE = gql` - mutation UpdateDataSource($dataSourceId: ID!, $params: UpdateDataSourceInput!) { - updateDataSource(dataSourceId: $dataSourceId, params: $params) { +const UPDATE_INVITATION = gql` + mutation UpdateInvitation($invitationId: String!, $action: String!) { + updateInvitation(invitationId: $invitationId, action: $action) + } +`; + +const GET_ORGANIZATION_FEATURES = gql` + query OrganizationFeatures($organizationId: ID!) { + organizationFeatures(organizationId: $organizationId) + } +`; + +const GET_OPTSCALE_MODE = gql` + query OptscaleMode($organizationId: ID!) { + optscaleMode(organizationId: $organizationId) { + finops + mlops + } + } +`; + +const UPDATE_OPTSCALE_MODE = gql` + mutation UpdateOptscaleMode($organizationId: ID!, $value: OptscaleModeParams) { + updateOptscaleMode(organizationId: $organizationId, value: $value) { + finops + mlops + } + } +`; + +const GET_ORGANIZATION_THEME_SETTINGS = gql` + query OrganizationThemeSettings($organizationId: ID!) { + organizationThemeSettings(organizationId: $organizationId) + } +`; + +const UPDATE_ORGANIZATION_THEME_SETTINGS = gql` + mutation UpdateOrganizationThemeSettings($organizationId: ID!, $value: JSONObject!) { + updateOrganizationThemeSettings(organizationId: $organizationId, value: $value) + } +`; + +const GET_ORGANIZATION_PERSPECTIVES = gql` + query OrganizationPerspectives($organizationId: ID!) { + organizationPerspectives(organizationId: $organizationId) + } +`; + +const UPDATE_ORGANIZATION_PERSPECTIVES = gql` + mutation UpdateOrganizationPerspectives($organizationId: ID!, $value: JSONObject!) { + updateOrganizationPerspectives(organizationId: $organizationId, value: $value) + } +`; + +const CREATE_DATA_SOURCE = gql` + mutation CreateDataSource($organizationId: ID!, $params: CreateDataSourceInput!) { + createDataSource(organizationId: $organizationId, params: $params) { + id name } } @@ -146,4 +345,58 @@ const UPDATE_EMPLOYEE_EMAIL = gql` } `; -export { GET_DATA_SOURCE, UPDATE_DATA_SOURCE, GET_EMPLOYEE_EMAILS, UPDATE_EMPLOYEE_EMAILS, UPDATE_EMPLOYEE_EMAIL }; +const UPDATE_DATA_SOURCE = gql` + mutation UpdateDataSource($dataSourceId: ID!, $params: UpdateDataSourceInput!) { + updateDataSource(dataSourceId: $dataSourceId, params: $params) { + id + name + ...AwsDataSourceConfigFragment + ...AzureTenantDataSourceConfigFragment + ...AzureSubscriptionDataSourceConfigFragment + ...GcpDataSourceConfigFragment + ...AlibabaDataSourceConfigFragment + ...NebiusDataSourceConfigFragment + ...DatabricksDataSourceConfigFragment + ...K8sDataSourceConfigFragment + } + } + ${AwsDataSourceConfigFragment} + ${AzureTenantDataSourceConfigFragment} + ${AzureSubscriptionDataSourceConfigFragment} + ${GcpDataSourceConfigFragment} + ${AlibabaDataSourceConfigFragment} + ${NebiusDataSourceConfigFragment} + ${DatabricksDataSourceConfigFragment} + ${K8sDataSourceConfigFragment} +`; + +const DELETE_DATA_SOURCE = gql` + mutation DeleteDataSource($dataSourceId: ID!) { + deleteDataSource(dataSourceId: $dataSourceId) + } +`; + +export { + CREATE_DATA_SOURCE, + UPDATE_DATA_SOURCE, + DELETE_DATA_SOURCE, + GET_ORGANIZATIONS, + CREATE_ORGANIZATION, + UPDATE_ORGANIZATION, + DELETE_ORGANIZATION, + GET_DATA_SOURCES, + GET_DATA_SOURCE, + GET_INVITATIONS, + UPDATE_INVITATION, + GET_CURRENT_EMPLOYEE, + GET_ORGANIZATION_FEATURES, + GET_OPTSCALE_MODE, + UPDATE_OPTSCALE_MODE, + GET_ORGANIZATION_THEME_SETTINGS, + GET_EMPLOYEE_EMAILS, + UPDATE_EMPLOYEE_EMAILS, + UPDATE_EMPLOYEE_EMAIL, + UPDATE_ORGANIZATION_THEME_SETTINGS, + GET_ORGANIZATION_PERSPECTIVES, + UPDATE_ORGANIZATION_PERSPECTIVES +}; diff --git a/ngui/ui/src/hooks/coreData/index.ts b/ngui/ui/src/hooks/coreData/index.ts new file mode 100644 index 00000000..94c4dac8 --- /dev/null +++ b/ngui/ui/src/hooks/coreData/index.ts @@ -0,0 +1,21 @@ +import { useAllDataSources } from "./useAllDataSources"; +import { useCurrentEmployee } from "./useCurrentEmployee"; +import { useGetOptscaleMode } from "./useGetOptscaleMode"; +import { useInvitations } from "./useInvitations"; +import { useOrganizationAllowedActions } from "./useOrganizationAllowedActions"; +import { useOrganizationFeatures } from "./useOrganizationFeatures"; +import { useOrganizationPerspectives } from "./useOrganizationPerspectives"; +import { useOrganizations } from "./useOrganizations"; +import { useOrganizationThemeSettings } from "./useOrganizationThemeSettings"; + +export { + useOrganizations, + useOrganizationAllowedActions, + useCurrentEmployee, + useAllDataSources, + useInvitations, + useOrganizationFeatures, + useGetOptscaleMode, + useOrganizationThemeSettings, + useOrganizationPerspectives +}; diff --git a/ngui/ui/src/hooks/coreData/useAllDataSources.ts b/ngui/ui/src/hooks/coreData/useAllDataSources.ts new file mode 100644 index 00000000..7eaefe4d --- /dev/null +++ b/ngui/ui/src/hooks/coreData/useAllDataSources.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@apollo/client"; +import { GET_DATA_SOURCES } from "graphql/api/restapi/queries"; +import { useOrganizationInfo } from "../useOrganizationInfo"; + +export const useAllDataSources = () => { + const { organizationId } = useOrganizationInfo(); + + const { data: { dataSources = [] } = {} } = useQuery(GET_DATA_SOURCES, { + variables: { + organizationId + }, + fetchPolicy: "cache-only" + }); + + return dataSources; +}; diff --git a/ngui/ui/src/hooks/coreData/useCurrentEmployee.ts b/ngui/ui/src/hooks/coreData/useCurrentEmployee.ts new file mode 100644 index 00000000..9e5af114 --- /dev/null +++ b/ngui/ui/src/hooks/coreData/useCurrentEmployee.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@apollo/client"; +import { GET_CURRENT_EMPLOYEE } from "graphql/api/restapi/queries"; +import { useOrganizationInfo } from "../useOrganizationInfo"; + +export const useCurrentEmployee = () => { + const { organizationId } = useOrganizationInfo(); + + const { data: { currentEmployee = {} } = {} } = useQuery(GET_CURRENT_EMPLOYEE, { + variables: { + organizationId + }, + fetchPolicy: "cache-only" + }); + + return currentEmployee; +}; diff --git a/ngui/ui/src/hooks/coreData/useGetOptscaleMode.ts b/ngui/ui/src/hooks/coreData/useGetOptscaleMode.ts new file mode 100644 index 00000000..81b00b21 --- /dev/null +++ b/ngui/ui/src/hooks/coreData/useGetOptscaleMode.ts @@ -0,0 +1,18 @@ +import { useQuery } from "@apollo/client"; +import { GET_OPTSCALE_MODE } from "graphql/api/restapi/queries"; +import { useOrganizationInfo } from "../useOrganizationInfo"; + +export const useGetOptscaleMode = () => { + const { organizationId } = useOrganizationInfo(); + + const { data } = useQuery(GET_OPTSCALE_MODE, { + fetchPolicy: "cache-only", + variables: { + organizationId + } + }); + + return { + optscaleMode: data?.optscaleMode ?? {} + }; +}; diff --git a/ngui/ui/src/hooks/coreData/useInvitations.ts b/ngui/ui/src/hooks/coreData/useInvitations.ts new file mode 100644 index 00000000..183e5088 --- /dev/null +++ b/ngui/ui/src/hooks/coreData/useInvitations.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@apollo/client"; +import { GET_INVITATIONS } from "graphql/api/restapi/queries"; +import { useOrganizationInfo } from "../useOrganizationInfo"; + +export const useInvitations = () => { + const { organizationId } = useOrganizationInfo(); + + const { data: { invitations = [] } = {} } = useQuery(GET_INVITATIONS, { + variables: { + organizationId + }, + fetchPolicy: "cache-only" + }); + + return invitations; +}; diff --git a/ngui/ui/src/hooks/coreData/useOrganizationAllowedActions.ts b/ngui/ui/src/hooks/coreData/useOrganizationAllowedActions.ts new file mode 100644 index 00000000..a60e4674 --- /dev/null +++ b/ngui/ui/src/hooks/coreData/useOrganizationAllowedActions.ts @@ -0,0 +1,20 @@ +import { useQuery } from "@apollo/client"; +import { GET_ORGANIZATION_ALLOWED_ACTIONS } from "graphql/api/auth/queries"; +import { useOrganizationInfo } from "../useOrganizationInfo"; + +export const useOrganizationAllowedActions = () => { + const { organizationId } = useOrganizationInfo(); + + const { + data: { organizationAllowedActions } + } = useQuery(GET_ORGANIZATION_ALLOWED_ACTIONS, { + fetchPolicy: "cache-only", + variables: { + requestParams: { + organization: organizationId + } + } + }); + + return organizationAllowedActions; +}; diff --git a/ngui/ui/src/hooks/coreData/useOrganizationFeatures.ts b/ngui/ui/src/hooks/coreData/useOrganizationFeatures.ts new file mode 100644 index 00000000..eb46f9bb --- /dev/null +++ b/ngui/ui/src/hooks/coreData/useOrganizationFeatures.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@apollo/client"; +import { GET_ORGANIZATION_FEATURES } from "graphql/api/restapi/queries"; +import { useOrganizationInfo } from "../useOrganizationInfo"; + +export const useOrganizationFeatures = () => { + const { organizationId } = useOrganizationInfo(); + + const { data: { organizationFeatures = {} } = {} } = useQuery(GET_ORGANIZATION_FEATURES, { + fetchPolicy: "cache-only", + variables: { + organizationId + } + }); + + return organizationFeatures; +}; diff --git a/ngui/ui/src/hooks/useOrganizationPerspectives.ts b/ngui/ui/src/hooks/coreData/useOrganizationPerspectives.ts similarity index 58% rename from ngui/ui/src/hooks/useOrganizationPerspectives.ts rename to ngui/ui/src/hooks/coreData/useOrganizationPerspectives.ts index c824d641..2d80e969 100644 --- a/ngui/ui/src/hooks/useOrganizationPerspectives.ts +++ b/ngui/ui/src/hooks/coreData/useOrganizationPerspectives.ts @@ -1,8 +1,8 @@ import { useMemo } from "react"; -import { GET_ORGANIZATION_PERSPECTIVES } from "api/restapi/actionTypes"; -import { parseJSON } from "utils/strings"; +import { useQuery } from "@apollo/client"; +import { GET_ORGANIZATION_PERSPECTIVES } from "graphql/api/restapi/queries"; import { validatePerspectiveSchema } from "utils/validation"; -import { useApiData } from "./useApiData"; +import { useOrganizationInfo } from "../useOrganizationInfo"; const validatePerspectives = (perspectives) => { const validPerspectives = {}; @@ -20,19 +20,22 @@ const validatePerspectives = (perspectives) => { }; export const useOrganizationPerspectives = () => { - const { - apiData: { value = "{}" } - } = useApiData(GET_ORGANIZATION_PERSPECTIVES, {}); + const { organizationId } = useOrganizationInfo(); - return useMemo(() => { - const perspectivesJson = parseJSON(value); + const { data: { organizationPerspectives = {} } = {} } = useQuery(GET_ORGANIZATION_PERSPECTIVES, { + variables: { + organizationId + }, + fetchPolicy: "cache-only" + }); - const { validPerspectives, invalidPerspectives } = validatePerspectives(perspectivesJson); + return useMemo(() => { + const { validPerspectives, invalidPerspectives } = validatePerspectives(organizationPerspectives); return { - allPerspectives: perspectivesJson, + allPerspectives: organizationPerspectives, validPerspectives, invalidPerspectives }; - }, [value]); + }, [organizationPerspectives]); }; diff --git a/ngui/ui/src/hooks/coreData/useOrganizationThemeSettings.ts b/ngui/ui/src/hooks/coreData/useOrganizationThemeSettings.ts new file mode 100644 index 00000000..ce95c034 --- /dev/null +++ b/ngui/ui/src/hooks/coreData/useOrganizationThemeSettings.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@apollo/client"; +import { GET_ORGANIZATION_THEME_SETTINGS } from "graphql/api/restapi/queries"; +import { useOrganizationInfo } from "../useOrganizationInfo"; + +export const useOrganizationThemeSettings = () => { + const { organizationId } = useOrganizationInfo(); + + const { data: { organizationThemeSettings = {} } = {} } = useQuery(GET_ORGANIZATION_THEME_SETTINGS, { + fetchPolicy: "cache-only", + variables: { + organizationId + } + }); + + return organizationThemeSettings; +}; diff --git a/ngui/ui/src/hooks/coreData/useOrganizations.ts b/ngui/ui/src/hooks/coreData/useOrganizations.ts new file mode 100644 index 00000000..1f21dbb5 --- /dev/null +++ b/ngui/ui/src/hooks/coreData/useOrganizations.ts @@ -0,0 +1,10 @@ +import { useQuery } from "@apollo/client"; +import { GET_ORGANIZATIONS } from "graphql/api/restapi/queries"; + +export const useOrganizations = () => { + const { data: { organizations = [] } = {} } = useQuery(GET_ORGANIZATIONS, { + fetchPolicy: "cache-only" + }); + + return organizations; +}; diff --git a/ngui/ui/src/hooks/useAllowedActions.ts b/ngui/ui/src/hooks/useAllowedActions.ts index 6497ed6b..90c08774 100644 --- a/ngui/ui/src/hooks/useAllowedActions.ts +++ b/ngui/ui/src/hooks/useAllowedActions.ts @@ -1,8 +1,9 @@ -import { GET_ORGANIZATION_ALLOWED_ACTIONS, GET_POOL_ALLOWED_ACTIONS, GET_RESOURCE_ALLOWED_ACTIONS } from "api/auth/actionTypes"; +import { GET_POOL_ALLOWED_ACTIONS, GET_RESOURCE_ALLOWED_ACTIONS } from "api/auth/actionTypes"; import { useApiData } from "hooks/useApiData"; import { useOrganizationInfo } from "hooks/useOrganizationInfo"; import { hasIntersection, getLength, isIdentical } from "utils/arrays"; import { SCOPE_TYPES } from "utils/constants"; +import { useOrganizationAllowedActions } from "./coreData"; const CHECK_PERMISSION_CONDITION = Object.freeze({ OR: "or", @@ -34,7 +35,7 @@ const getLabel = (entityType) => { case SCOPE_TYPES.RESOURCE: return GET_RESOURCE_ALLOWED_ACTIONS; default: - return GET_ORGANIZATION_ALLOWED_ACTIONS; + return ""; } }; @@ -49,6 +50,12 @@ const useScopedAllowedActions = (entityType, entityId) => { apiData: { allowedActions = {} } } = useApiData(label); + const organizationAllowedActions = useOrganizationAllowedActions(); + + if (entityType === SCOPE_TYPES.ORGANIZATION) { + return organizationAllowedActions[organizationId] || []; + } + return allowedActions[id] || []; }; @@ -61,14 +68,12 @@ export const useAllAllowedActions = () => { apiData: { allowedActions: resourceAllowedAction = {} } } = useApiData(GET_RESOURCE_ALLOWED_ACTIONS); - const { - apiData: { allowedActions: organizationAllowedAction = {} } - } = useApiData(GET_ORGANIZATION_ALLOWED_ACTIONS); + const organizationAllowedActions = useOrganizationAllowedActions(); return { [SCOPE_TYPES.POOL]: poolAllowedAction, [SCOPE_TYPES.RESOURCE]: resourceAllowedAction, - [SCOPE_TYPES.ORGANIZATION]: organizationAllowedAction + [SCOPE_TYPES.ORGANIZATION]: organizationAllowedActions }; }; @@ -120,5 +125,9 @@ export const useFilterByPermissions = ({ entitiesIds, entitiesType, permissions, apiData: { allowedActions = {} } } = useApiData(label); - return entitiesIds.filter((id) => isAllowed(permissions, allowedActions[id] || [], condition)); + const organizationAllowedActions = useOrganizationAllowedActions(); + + const allowedEntityActions = entitiesType === SCOPE_TYPES.ORGANIZATION ? organizationAllowedActions : allowedActions; + + return entitiesIds.filter((entityId) => isAllowed(permissions, allowedEntityActions[entityId] || [], condition)); }; diff --git a/ngui/ui/src/hooks/useAuthorization.ts b/ngui/ui/src/hooks/useAuthorization.ts deleted file mode 100644 index fa35a19b..00000000 --- a/ngui/ui/src/hooks/useAuthorization.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { useCallback } from "react"; -import { useDispatch } from "react-redux"; -import { useNavigate } from "react-router-dom"; -import { getToken, getOrganizations } from "api"; -import { GET_TOKEN } from "api/auth/actionTypes"; -import { GET_ORGANIZATIONS } from "api/restapi/actionTypes"; -import { setScopeId } from "containers/OrganizationSelectorContainer/actionCreators"; -import { SCOPE_ID } from "containers/OrganizationSelectorContainer/reducer"; -import { checkError } from "utils/api"; -import { getQueryParams } from "utils/network"; -import { useApiState } from "./useApiState"; - -export const useAuthorization = ({ onSuccessRedirectionPath } = {}) => { - const navigate = useNavigate(); - const dispatch = useDispatch(); - - const { isLoading: isGetTokenLoading } = useApiState(GET_TOKEN); - const { isLoading: isGetOrganizationLoading } = useApiState(GET_ORGANIZATIONS); - - const updateScopeId = useCallback( - (currentState) => { - const { [SCOPE_ID]: organizationIdQueryParam } = getQueryParams(); - const { organizations = [] } = currentState.restapi[GET_ORGANIZATIONS]; - const { organizationId: currentOrganizationId } = currentState; - const targetOrganizationId = organizationIdQueryParam || currentOrganizationId; - - if (organizations.find((organization) => organization.id === targetOrganizationId)) { - dispatch(setScopeId(targetOrganizationId)); - return Promise.resolve(); - } - - dispatch(setScopeId(organizations[0]?.id)); - return Promise.resolve(); - }, - [dispatch] - ); - - const authorize = useCallback( - (email, password) => - dispatch((_, getState) => - dispatch(getToken({ email, password })) - .then(() => checkError(GET_TOKEN, getState())) - .then(() => dispatch(getOrganizations())) - .then(() => checkError(GET_ORGANIZATIONS, getState())) - .then(() => updateScopeId(getState())) - .then(() => { - if (onSuccessRedirectionPath) { - navigate(onSuccessRedirectionPath); - } - return Promise.resolve(); - }) - // Return rejection since this chain can be part of another chain => reject entire chain if something went wrong here - .catch(() => Promise.reject()) - ), - [dispatch, navigate, onSuccessRedirectionPath, updateScopeId] - ); - - return { - authorize, - isLoading: isGetTokenLoading || isGetOrganizationLoading - }; -}; diff --git a/ngui/ui/src/hooks/useAwsDataSources.ts b/ngui/ui/src/hooks/useAwsDataSources.ts index f2df5f64..2e070296 100644 --- a/ngui/ui/src/hooks/useAwsDataSources.ts +++ b/ngui/ui/src/hooks/useAwsDataSources.ts @@ -1,12 +1,9 @@ import { useMemo } from "react"; -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; import { AWS_CNR } from "utils/constants"; -import { useApiData } from "./useApiData"; +import { useAllDataSources } from "./coreData/useAllDataSources"; export const useAwsDataSources = () => { - const { - apiData: { cloudAccounts: dataSources = [] } - } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); return useMemo(() => dataSources.filter(({ type }) => type === AWS_CNR), [dataSources]); }; diff --git a/ngui/ui/src/hooks/useConstraints.ts b/ngui/ui/src/hooks/useConstraints.ts index 46cb66dc..1948f6b1 100644 --- a/ngui/ui/src/hooks/useConstraints.ts +++ b/ngui/ui/src/hooks/useConstraints.ts @@ -1,9 +1,9 @@ import { useMemo } from "react"; import { CONSTRAINTS, TOTAL_EXPENSE_LIMIT } from "utils/constraints"; -import { useOrganizationFeatures } from "./useOrganizationFeatures"; +import { useIsFeatureEnabled } from "./useIsFeatureEnabled"; export const useConstraints = () => { - const { total_expense_limit_enabled: totalExpenseLimitEnabled = 0 } = useOrganizationFeatures(); + const totalExpenseLimitEnabled = useIsFeatureEnabled("total_expense_limit_enabled"); return useMemo( () => CONSTRAINTS.filter((type) => type !== TOTAL_EXPENSE_LIMIT || totalExpenseLimitEnabled), diff --git a/ngui/ui/src/hooks/useCustomOrganizationWeekends.ts b/ngui/ui/src/hooks/useCustomOrganizationWeekends.ts index 83366991..7cad1bf6 100644 --- a/ngui/ui/src/hooks/useCustomOrganizationWeekends.ts +++ b/ngui/ui/src/hooks/useCustomOrganizationWeekends.ts @@ -1,5 +1,5 @@ import { SHORT_WEEK_DAYS } from "utils/datetime"; -import { useOrganizationFeatures } from "./useOrganizationFeatures"; +import { useOrganizationFeatures } from "./coreData"; export const getValidCustomWeekends = (customWeekends) => { if (Array.isArray(customWeekends)) { diff --git a/ngui/ui/src/hooks/useFetchAndDownload.ts b/ngui/ui/src/hooks/useFetchAndDownload.ts index e1b1362a..b8eef5bb 100644 --- a/ngui/ui/src/hooks/useFetchAndDownload.ts +++ b/ngui/ui/src/hooks/useFetchAndDownload.ts @@ -1,6 +1,5 @@ import { useState } from "react"; -import { GET_TOKEN } from "api/auth/actionTypes"; -import { useApiData } from "./useApiData"; +import { useGetToken } from "./useGetToken"; const getFilenameFromHeader = (header) => header?.match(/filename="(.+)"/)[1]; @@ -35,9 +34,7 @@ const download = (data, filename, type) => { }; export const useFetchAndDownload = () => { - const { - apiData: { token } - } = useApiData(GET_TOKEN); + const { token } = useGetToken(); const [isFileDownloading, setIsFileDownloading] = useState(false); diff --git a/ngui/ui/src/hooks/useGetToken.ts b/ngui/ui/src/hooks/useGetToken.ts new file mode 100644 index 00000000..43d820e1 --- /dev/null +++ b/ngui/ui/src/hooks/useGetToken.ts @@ -0,0 +1,16 @@ +import { useSelector } from "react-redux"; +import { INITIAL } from "containers/InitializeContainer/redux"; + +export const useGetToken = () => { + const userId = useSelector((state) => state[INITIAL]?.user_id); + const token = useSelector((state) => state[INITIAL]?.token); + const userEmail = useSelector((state) => state[INITIAL]?.user_email); + const caveats = useSelector((state) => state[INITIAL]?.caveats); + + return { + userId, + token, + userEmail, + caveats + }; +}; diff --git a/ngui/ui/src/hooks/useIsFeatureEnabled.ts b/ngui/ui/src/hooks/useIsFeatureEnabled.ts index 99c160c8..48ef6aac 100644 --- a/ngui/ui/src/hooks/useIsFeatureEnabled.ts +++ b/ngui/ui/src/hooks/useIsFeatureEnabled.ts @@ -1,4 +1,4 @@ -import { useOrganizationFeatures } from "./useOrganizationFeatures"; +import { useOrganizationFeatures } from "./coreData"; export const useIsFeatureEnabled = (featureName) => { const { [featureName]: featureFlag = 0 } = useOrganizationFeatures(); diff --git a/ngui/ui/src/hooks/useIsNebiusConnectionEnabled.ts b/ngui/ui/src/hooks/useIsNebiusConnectionEnabled.ts index 100049f0..a8909e92 100644 --- a/ngui/ui/src/hooks/useIsNebiusConnectionEnabled.ts +++ b/ngui/ui/src/hooks/useIsNebiusConnectionEnabled.ts @@ -1,4 +1,4 @@ -import { useOrganizationFeatures } from "./useOrganizationFeatures"; +import { useOrganizationFeatures } from "./coreData"; export const useIsNebiusConnectionEnabled = () => { const { nebius_connection_enabled: nebiusConnectionEnabled = 0 } = useOrganizationFeatures(); diff --git a/ngui/ui/src/hooks/useIsOptScaleModeEnabled.ts b/ngui/ui/src/hooks/useIsOptScaleModeEnabled.ts index 6d7c1964..0b165a65 100644 --- a/ngui/ui/src/hooks/useIsOptScaleModeEnabled.ts +++ b/ngui/ui/src/hooks/useIsOptScaleModeEnabled.ts @@ -1,17 +1,17 @@ import { useEffect, useState } from "react"; -import { useOptScaleMode } from "./useOptScaleMode"; +import { useGetOptscaleMode } from "./coreData"; export const useIsOptScaleModeEnabled = (mode) => { const [isEnabled, setIsEnabled] = useState(false); - const value = useOptScaleMode(); + const { optscaleMode } = useGetOptscaleMode(); useEffect(() => { // This handles 2 cases, in both of them we need to display children. // 1. If there is no mode explicitly defined for a component // 2. if there is no OPTSCALE_MODE_OPTION defined at all - setIsEnabled(value?.[mode] ?? true); - }, [value, mode]); + setIsEnabled(optscaleMode?.[mode] ?? true); + }, [optscaleMode, mode]); return isEnabled; }; diff --git a/ngui/ui/src/hooks/useNewAuthorization.ts b/ngui/ui/src/hooks/useNewAuthorization.ts deleted file mode 100644 index 90342b8c..00000000 --- a/ngui/ui/src/hooks/useNewAuthorization.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { useCallback, useState } from "react"; -import { useDispatch } from "react-redux"; -import { useNavigate } from "react-router-dom"; -import { getToken, getOrganizations, getInvitations, createOrganization, signIn, createUser, AUTH, RESTAPI } from "api"; -import { GET_TOKEN, SIGN_IN, CREATE_USER } from "api/auth/actionTypes"; -import { API } from "api/reducer"; -import { GET_ORGANIZATIONS, GET_INVITATIONS, CREATE_ORGANIZATION, VERIFY_EMAIL } from "api/restapi/actionTypes"; -import { setScopeId } from "containers/OrganizationSelectorContainer/actionCreators"; -import { SCOPE_ID } from "containers/OrganizationSelectorContainer/reducer"; -import VerifyEmailService from "services/VerifyEmailService"; -import { ACCEPT_INVITATIONS, EMAIL_VERIFICATION, HOME } from "urls"; -import { trackEvent, GA_EVENT_CATEGORIES } from "utils/analytics"; -import { checkError, isError } from "utils/api"; -import { isEmpty } from "utils/arrays"; -import { formQueryString, getQueryParams } from "utils/network"; -import { useApiState } from "./useApiState"; - -export const PROVIDERS = Object.freeze({ - GOOGLE: "google", - MICROSOFT: "microsoft" -}); - -const EMAIL_NOT_VERIFIED_ERROR_CODE = "OA0073"; - -// TODO - after Live Demo auth is updated: -// - remove useAuthorization and rename this one -// - refactor/generalize - -export const useNewAuthorization = () => { - const dispatch = useDispatch(); - const navigate = useNavigate(); - - const [isAuthInProgress, setIsAuthInProgress] = useState(false); - const [isRegistrationInProgress, setIsRegistrationInProgress] = useState(false); - - const { isLoading: isGetTokenLoading } = useApiState(GET_TOKEN); - const { isLoading: isGetInvitationsLoading, isDataReady: isGetInvitationsDataReady } = useApiState(GET_INVITATIONS); - const { isLoading: isGetOrganizationsLoading, isDataReady: isGetOrganizationsDataReady } = useApiState(GET_ORGANIZATIONS); - const { isLoading: isCreateOrganizationLoading } = useApiState(CREATE_ORGANIZATION); - const { isLoading: isCreateUserLoading } = useApiState(CREATE_USER); - const { isLoading: isSignInLoading } = useApiState(SIGN_IN); - - const { useSendEmailVerificationCode } = VerifyEmailService(); - const { onSend: sendEmailVerificationCode } = useSendEmailVerificationCode(); - - const redirectOnSuccess = useCallback( - (to) => { - navigate(to); - }, - [navigate] - ); - - const updateScopeId = useCallback( - (currentState) => { - const { [SCOPE_ID]: organizationIdQueryParam } = getQueryParams(); - const { organizations = [] } = currentState.restapi[GET_ORGANIZATIONS]; - const { organizationId: currentOrganizationId } = currentState; - const targetOrganizationId = organizationIdQueryParam || currentOrganizationId; - - if (organizations.find((organization) => organization.id === targetOrganizationId)) { - dispatch(setScopeId(targetOrganizationId)); - return Promise.resolve(); - } - - dispatch(setScopeId(organizations[0]?.id)); - return Promise.resolve(); - }, - [dispatch] - ); - - const activateScope = useCallback( - (email, { getOnSuccessRedirectionPath } = {}) => - dispatch((_, getState) => - dispatch(getOrganizations()) - .then(() => checkError(GET_ORGANIZATIONS, getState())) - .then(() => getState()?.[RESTAPI]?.[GET_ORGANIZATIONS]?.organizations) - .then((existingOrganizations) => { - if (isEmpty(existingOrganizations)) { - return dispatch(createOrganization(`${email}'s Organization`)) - .then(() => checkError(CREATE_ORGANIZATION, getState())) - .then(() => dispatch(getOrganizations())) - .then(() => checkError(GET_ORGANIZATIONS, getState())); - } - return undefined; - }) - .then(() => updateScopeId(getState())) - .then(() => { - const redirectPath = - typeof getOnSuccessRedirectionPath === "function" - ? getOnSuccessRedirectionPath({ userEmail: email }) - : getQueryParams().next || HOME; - - if (!redirectPath) { - return Promise.resolve(); - } - - return redirectOnSuccess(redirectPath); - }) - .then(() => { - const { register, provider } = getState()?.[AUTH]?.[GET_TOKEN] ?? {}; - if (register) { - Promise.resolve(trackEvent({ category: GA_EVENT_CATEGORIES.USER, action: "Registered", label: provider })); - } - }) - .catch(() => Promise.reject()) - .finally(() => { - setIsAuthInProgress(false); - setIsRegistrationInProgress(false); - }) - ), - [dispatch, redirectOnSuccess, updateScopeId] - ); - - const authorize = useCallback( - ({ email, password }, { getOnSuccessRedirectionPath }) => { - setIsAuthInProgress(true); - dispatch((_, getState) => - dispatch(getToken({ email, password })) - .then(() => { - const state = getState(); - - if (isError(GET_TOKEN, getState())) { - const { error_code: errorCode } = state?.[API]?.[GET_TOKEN]?.status?.response?.data?.error ?? {}; - - if (errorCode === EMAIL_NOT_VERIFIED_ERROR_CODE) { - return sendEmailVerificationCode(email).then(() => { - if (isError(VERIFY_EMAIL, getState())) { - return Promise.reject(); - } - - navigate( - `${EMAIL_VERIFICATION}?${formQueryString({ - email - })}` - ); - return Promise.reject(); - }); - } - - return Promise.reject(); - } - return Promise.resolve(); - }) - .then(() => dispatch(getInvitations())) - .then(() => checkError(GET_INVITATIONS, getState())) - .then(() => getState()?.[RESTAPI]?.[GET_INVITATIONS]) - .then((pendingInvitations) => { - if (isEmpty(pendingInvitations)) { - const { userEmail } = getState()?.[AUTH]?.[GET_TOKEN] ?? {}; - Promise.resolve(activateScope(userEmail, { getOnSuccessRedirectionPath })); - } else { - navigate(`${ACCEPT_INVITATIONS}?${formQueryString(getQueryParams())}`); - } - }) - .catch(() => { - setIsAuthInProgress(false); - }) - ); - }, - [dispatch, sendEmailVerificationCode, navigate, activateScope] - ); - - const register = useCallback( - ({ name, email, password }) => { - setIsRegistrationInProgress(true); - dispatch((_, getState) => - dispatch(createUser(name, email, password)) - .then(() => checkError(CREATE_USER, getState())) - .then(() => { - trackEvent({ category: GA_EVENT_CATEGORIES.USER, action: "Registered", label: "optscale" }); - return Promise.resolve(); - }) - .then(() => - sendEmailVerificationCode(email).then(() => { - if (isError(VERIFY_EMAIL, getState())) { - return Promise.reject(); - } - - navigate( - `${EMAIL_VERIFICATION}?${formQueryString({ - email - })}` - ); - return Promise.reject(); - }) - ) - .catch(() => { - setIsRegistrationInProgress(false); - }) - ); - }, - [dispatch, navigate, sendEmailVerificationCode] - ); - - const thirdPartySignIn = useCallback( - ({ provider, params }, { getOnSuccessRedirectionPath }) => { - setIsAuthInProgress(true); - dispatch((_, getState) => - dispatch(signIn(provider, params)) - .then(() => checkError(SIGN_IN, getState())) - .then(() => dispatch(getInvitations())) - .then(() => checkError(GET_INVITATIONS, getState())) - .then(() => getState()?.[RESTAPI]?.[GET_INVITATIONS]) - .then((pendingInvitations) => { - if (isEmpty(pendingInvitations)) { - const useEmail = getState()?.[AUTH]?.[GET_TOKEN]?.userEmail; - Promise.resolve(activateScope(useEmail, { getOnSuccessRedirectionPath })); - } else { - navigate(ACCEPT_INVITATIONS); - } - }) - .catch(() => { - setIsAuthInProgress(false); - }) - ); - }, - [dispatch, activateScope, navigate] - ); - - return { - authorize, - register, - thirdPartySignIn, - setIsAuthInProgress, - isGetTokenLoading, - isGetOrganizationsLoading, - isGetInvitationsLoading, - isGetInvitationsDataReady, - isCreateOrganizationLoading, - isCreateUserLoading, - isSignInLoading, - isGetOrganizationsDataReady, - activateScope, - isAuthInProgress, - isRegistrationInProgress - }; -}; diff --git a/ngui/ui/src/hooks/useOptScaleMode.ts b/ngui/ui/src/hooks/useOptScaleMode.ts deleted file mode 100644 index bf1b17c2..00000000 --- a/ngui/ui/src/hooks/useOptScaleMode.ts +++ /dev/null @@ -1,13 +0,0 @@ -import OrganizationOptionsService from "services/OrganizationOptionsService"; -import { OPTSCALE_MODE_OPTION } from "utils/constants"; - -export const useOptScaleMode = () => { - const { useGetOptscaleMode } = OrganizationOptionsService(); - - const { - // Intentionally ignore loading state to update the state 'silently' - option: { value: optScaleMode } - } = useGetOptscaleMode(OPTSCALE_MODE_OPTION); - - return optScaleMode; -}; diff --git a/ngui/ui/src/hooks/useOrganizationFeatures.ts b/ngui/ui/src/hooks/useOrganizationFeatures.ts deleted file mode 100644 index b86e77ca..00000000 --- a/ngui/ui/src/hooks/useOrganizationFeatures.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { GET_ORGANIZATION_FEATURES } from "api/restapi/actionTypes"; -import { parseJSON } from "utils/strings"; -import { useApiData } from "./useApiData"; - -export const useOrganizationFeatures = () => { - const { - apiData: { value: features = "{}" } - } = useApiData(GET_ORGANIZATION_FEATURES, {}); - - return parseJSON(features); -}; diff --git a/ngui/ui/src/hooks/useOrganizationIdQueryParameterListener.ts b/ngui/ui/src/hooks/useOrganizationIdQueryParameterListener.ts deleted file mode 100644 index 64d05977..00000000 --- a/ngui/ui/src/hooks/useOrganizationIdQueryParameterListener.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useEffect } from "react"; -import { useDispatch } from "react-redux"; -import { GET_ORGANIZATIONS } from "api/restapi/actionTypes"; -import { setScopeId } from "containers/OrganizationSelectorContainer/actionCreators"; -import { getQueryParams } from "utils/network"; -import { useApiData } from "./useApiData"; - -const ORGANIZATION_ID_QUERY_PARAMETER_NAME = "organizationId"; - -export const useOrganizationIdQueryParameterListener = () => { - const { [ORGANIZATION_ID_QUERY_PARAMETER_NAME]: organizationIdQueryParameter } = getQueryParams(); - const dispatch = useDispatch(); - - const { - apiData: { organizations = [] } - } = useApiData(GET_ORGANIZATIONS); - - useEffect(() => { - if ( - organizationIdQueryParameter && - organizations.find((organization) => organization.id === organizationIdQueryParameter) - ) { - dispatch(setScopeId(organizationIdQueryParameter)); - } - }, [dispatch, organizationIdQueryParameter, organizations]); -}; diff --git a/ngui/ui/src/hooks/useOrganizationInfo.ts b/ngui/ui/src/hooks/useOrganizationInfo.ts index 008feb6f..aac81e3b 100644 --- a/ngui/ui/src/hooks/useOrganizationInfo.ts +++ b/ngui/ui/src/hooks/useOrganizationInfo.ts @@ -1,47 +1,35 @@ -import { GET_ORGANIZATIONS } from "api/restapi/actionTypes"; +import { useSelector } from "react-redux"; import { SCOPE_ID } from "containers/OrganizationSelectorContainer/reducer"; -import { useApiData } from "hooks/useApiData"; -import { useRootData } from "hooks/useRootData"; import localeManager from "translations/localeManager"; +import { useOrganizations } from "./coreData"; -const getActiveOrganization = (organizationId, organizations) => { - // 1. Take organization by id from storage - // 2. Take first organization from storage - // 3. Take empty object - - let organization = organizations.find((org) => org.id === organizationId); - - if (!organization) { - [organization = {}] = organizations; - } - - return organization; -}; - -export const useOrganizationInfo = () => { - // TODO: need to check setScopeId function, which is not being called - // after authorization, so old organization id is still persisted, - // even after login with another user (with another organizations set) - const { rootData: organizationId } = useRootData(SCOPE_ID); - - const { - apiData: { organizations = [] } - } = useApiData(GET_ORGANIZATIONS); +const useCurrentOrganization = (organizations = []) => { + // Take current/active organization ID from storage + const currentOrganizationId = useSelector((state) => state[SCOPE_ID]); + // If there is no organization found by that ID, take the first one from storage const { + id: organizationId, pool_id: organizationPoolId, - name, + name: organizationName, is_demo: isDemo = false, - id: newOrganizationId, currency = "USD" - } = getActiveOrganization(organizationId, organizations); + } = organizations.find((org) => org.id === currentOrganizationId) ?? organizations?.[0] ?? {}; return { - organizationId: newOrganizationId, - name, + organizationId, + name: organizationName, organizationPoolId, isDemo, currency, currencySymbol: currency ? localeManager.getCurrencySymbol(currency) : undefined }; }; + +export const useOrganizationInfo = () => { + const organizations = useOrganizations(); + + const currentOrganization = useCurrentOrganization(organizations); + + return currentOrganization; +}; diff --git a/ngui/ui/src/hooks/useOrganizationThemeSettings.ts b/ngui/ui/src/hooks/useOrganizationThemeSettings.ts deleted file mode 100644 index c0a52574..00000000 --- a/ngui/ui/src/hooks/useOrganizationThemeSettings.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { GET_ORGANIZATION_THEME_SETTINGS } from "api/restapi/actionTypes"; -import { parseJSON } from "utils/strings"; -import { useApiData } from "./useApiData"; - -export const useOrganizationThemeSettings = () => { - const { - apiData: { value: themeSettings = "{}" } - } = useApiData(GET_ORGANIZATION_THEME_SETTINGS, {}); - - return parseJSON(themeSettings); -}; diff --git a/ngui/ui/src/hooks/useResourceConstraintPermissions.ts b/ngui/ui/src/hooks/useResourceConstraintPermissions.ts index 061cac65..0c4658e4 100644 --- a/ngui/ui/src/hooks/useResourceConstraintPermissions.ts +++ b/ngui/ui/src/hooks/useResourceConstraintPermissions.ts @@ -1,7 +1,6 @@ -import { GET_CURRENT_EMPLOYEE } from "api/restapi/actionTypes"; import { SCOPE_TYPES } from "utils/constants"; +import { useCurrentEmployee } from "./coreData/useCurrentEmployee"; import { useIsAllowed, useIsAllowedForSome } from "./useAllowedActions"; -import { useApiData } from "./useApiData"; // TODO: There are thoughts to change the approach here, to make it more readable and flexible. The discussed options can be seen at the link - https://gitlab.com/hystax/ngui/-/merge_requests/1669 const getAllowedActionsConfiguration = (employeeId, resourceId, currentEmployeeId) => { @@ -15,7 +14,7 @@ const getAllowedActionsConfiguration = (employeeId, resourceId, currentEmployeeI }; export const useIsAllowedToManageResourceConstraint = (employeeId, resourceId) => { - const { apiData: { currentEmployee: { id: currentEmployeeId } = {} } = {} } = useApiData(GET_CURRENT_EMPLOYEE); + const { id: currentEmployeeId } = useCurrentEmployee(); const configuration = getAllowedActionsConfiguration(employeeId, resourceId, currentEmployeeId); @@ -30,7 +29,7 @@ export const useIsAllowedToManageResourceConstraint = (employeeId, resourceId) = * @returns a boolean flag which indicates if a user is able to manage some resource constraint */ export const useIsAllowedToManageAnyResourceConstraint = (configuration) => { - const { apiData: { currentEmployee: { id: currentEmployeeId } = {} } = {} } = useApiData(GET_CURRENT_EMPLOYEE); + const { id: currentEmployeeId } = useCurrentEmployee(); const allowedActionsConfiguration = configuration.map(({ resourceId, employeeId }) => getAllowedActionsConfiguration(employeeId, resourceId, currentEmployeeId) diff --git a/ngui/ui/src/hooks/useResourceFilters.ts b/ngui/ui/src/hooks/useResourceFilters.ts index 5a3b38a3..ee8bbf8b 100644 --- a/ngui/ui/src/hooks/useResourceFilters.ts +++ b/ngui/ui/src/hooks/useResourceFilters.ts @@ -1,10 +1,9 @@ -import { GET_CURRENT_EMPLOYEE } from "api/restapi/actionTypes"; import Filters from "components/Filters"; import { RESOURCE_FILTERS } from "components/Filters/constants"; -import { useApiData } from "./useApiData"; +import { useCurrentEmployee } from "./coreData/useCurrentEmployee"; export const useResourceFilters = (filterValues, appliedFilters) => { - const { apiData: { currentEmployee: { id: currentEmployeeId } = {} } = {} } = useApiData(GET_CURRENT_EMPLOYEE); + const { id: currentEmployeeId } = useCurrentEmployee(); const scopeInfo = { currentEmployeeId }; diff --git a/ngui/ui/src/hooks/useShouldRenderConnectCloudAccountMock.ts b/ngui/ui/src/hooks/useShouldRenderConnectCloudAccountMock.ts index 8234c7b1..6f2919d3 100644 --- a/ngui/ui/src/hooks/useShouldRenderConnectCloudAccountMock.ts +++ b/ngui/ui/src/hooks/useShouldRenderConnectCloudAccountMock.ts @@ -1,10 +1,10 @@ -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; -import { useApiData } from "hooks/useApiData"; import { isEmpty } from "utils/arrays"; +import { useAllDataSources } from "./coreData/useAllDataSources"; -export const useShouldRenderConnectCloudAccountMock = (cloudAccountType) => { - const { apiData: { cloudAccounts = [] } = {} } = useApiData(GET_DATA_SOURCES); - return cloudAccountType - ? cloudAccounts.findIndex((cloudAccount) => cloudAccount.type === cloudAccountType) === -1 - : isEmpty(cloudAccounts); +export const useShouldRenderConnectCloudAccountMock = (dataSourceType) => { + const dataSources = useAllDataSources(); + + return dataSourceType + ? dataSources.findIndex((dataSource) => dataSource.type === dataSourceType) === -1 + : isEmpty(dataSources); }; diff --git a/ngui/ui/src/hooks/useSignOut.ts b/ngui/ui/src/hooks/useSignOut.ts index ece8371d..b91637c8 100644 --- a/ngui/ui/src/hooks/useSignOut.ts +++ b/ngui/ui/src/hooks/useSignOut.ts @@ -1,14 +1,11 @@ import { useDispatch } from "react-redux"; -import { GET_TOKEN } from "api/auth/actionTypes"; import { signOut } from "utils/api"; -import { useApiData } from "./useApiData"; +import { useGetToken } from "./useGetToken"; export const useSignOut = () => { const dispatch = useDispatch(); - const { - apiData: { userEmail } - } = useApiData(GET_TOKEN); + const { userEmail } = useGetToken(); return () => signOut(dispatch, { diff --git a/ngui/ui/src/hooks/useThemeSettingsOptions.ts b/ngui/ui/src/hooks/useThemeSettingsOptions.ts index dafb5cfe..1b59fb01 100644 --- a/ngui/ui/src/hooks/useThemeSettingsOptions.ts +++ b/ngui/ui/src/hooks/useThemeSettingsOptions.ts @@ -2,7 +2,7 @@ import { getUnit } from "@mui/material/styles/cssUtils"; import { isMedia } from "theme"; import { isEmpty as isEmptyArray } from "utils/arrays"; import { isObject } from "utils/objects"; -import { useOrganizationThemeSettings } from "./useOrganizationThemeSettings"; +import { useOrganizationThemeSettings } from "./coreData"; const validateObject = (obj, validator) => { if (!isObject(obj)) { diff --git a/ngui/ui/src/hooks/useUpdateScope.ts b/ngui/ui/src/hooks/useUpdateScope.ts new file mode 100644 index 00000000..0eff19cb --- /dev/null +++ b/ngui/ui/src/hooks/useUpdateScope.ts @@ -0,0 +1,20 @@ +import { useCallback } from "react"; +import { useDispatch } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { setScopeId } from "containers/OrganizationSelectorContainer/actionCreators"; + +export const useUpdateScope = () => { + const navigate = useNavigate(); + const dispatch = useDispatch(); + + return useCallback( + ({ newScopeId, redirectTo }: { newScopeId: string; redirectTo?: string }) => { + dispatch(setScopeId(newScopeId)); + + if (redirectTo) { + navigate(redirectTo); + } + }, + [dispatch, navigate] + ); +}; diff --git a/ngui/ui/src/layouts/BaseLayout/BaseLayout.tsx b/ngui/ui/src/layouts/BaseLayout/BaseLayout.tsx index 92f1d4bd..0dadd808 100644 --- a/ngui/ui/src/layouts/BaseLayout/BaseLayout.tsx +++ b/ngui/ui/src/layouts/BaseLayout/BaseLayout.tsx @@ -1,4 +1,4 @@ -import { useState, Children } from "react"; +import { useState } from "react"; import MenuIcon from "@mui/icons-material/Menu"; import AppBar from "@mui/material/AppBar"; import Box from "@mui/material/Box"; @@ -19,7 +19,7 @@ import Logo from "components/Logo"; import MainMenu from "components/MainMenu"; import PendingInvitationsAlert from "components/PendingInvitationsAlert"; import TopAlertWrapper from "components/TopAlertWrapper"; -import MainLayoutContainer from "containers/MainLayoutContainer"; +import CoreDataContainer from "containers/CoreDataContainer"; import OrganizationSelectorContainer from "containers/OrganizationSelectorContainer"; import { useCommunityDocsContext } from "contexts/CommunityDocsContext"; import { useIsDownMediaQuery } from "hooks/useMediaQueries"; @@ -38,7 +38,7 @@ const getLogoSize = (isDemo, isDownMd, isDownSm) => { return isDownSm ? LOGO_SIZE.SHORT : LOGO_SIZE.FULL; }; -const AppToolbar = ({ onMenuIconClick, mainMenu, showMainMenu = false, showOrganizationSelector = false }) => { +const AppToolbar = ({ onMenuIconClick, showMainMenu = false, showOrganizationSelector = false }) => { const { classes, cx } = useStyles(); const navigate = useNavigate(); const isDownMd = useIsDownMediaQuery("md"); @@ -86,7 +86,7 @@ const AppToolbar = ({ onMenuIconClick, mainMenu, showMainMenu = false, showOrgan {showOrganizationSelector && ( - + )} @@ -119,7 +119,6 @@ const BaseLayout = ({ children, showMainMenu = false, showOrganizationSelector = showMainMenu={showMainMenu} onMenuIconClick={handleDrawerToggle} showOrganizationSelector={showOrganizationSelector} - mainMenu={mainMenu} /> @@ -149,7 +148,7 @@ const BaseLayout = ({ children, showMainMenu = false, showOrganizationSelector = )} - {Children.only(children)} + {children} diff --git a/ngui/ui/src/middleware/api.ts b/ngui/ui/src/middleware/api.ts index 0e0a1da5..a7162309 100644 --- a/ngui/ui/src/middleware/api.ts +++ b/ngui/ui/src/middleware/api.ts @@ -3,7 +3,6 @@ import queryString from "query-string"; import { v4 as uuidv4 } from "uuid"; import { apiEnd, apiError, apiStart, apiSuccess, resetTtl } from "api"; import { API } from "api/actionTypes"; -import { GET_TOKEN } from "api/auth/actionTypes"; import { signOut } from "utils/api"; import { ALERT_SEVERITY } from "utils/constants"; import { getEnvironmentVariable } from "utils/env"; @@ -51,8 +50,7 @@ const apiMiddleware = const state = getState(); - const { token, temporaryToken, userEmail } = state?.auth?.[GET_TOKEN] ?? {}; - const accessToken = temporaryToken || token; + const { token: accessToken, user_email: userEmail } = state?.initial ?? {}; const dataOrParams = ["GET", "DELETE"].includes(method) ? "params" : "data"; diff --git a/ngui/ui/src/pages/AcceptInvitations/AcceptInvitations.tsx b/ngui/ui/src/pages/AcceptInvitations/AcceptInvitations.tsx deleted file mode 100644 index 122764a9..00000000 --- a/ngui/ui/src/pages/AcceptInvitations/AcceptInvitations.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import AcceptInvitationsContainer from "containers/AcceptInvitationsContainer"; - -const AcceptInvitations = () => ; - -export default AcceptInvitations; diff --git a/ngui/ui/src/pages/AcceptInvitations/index.ts b/ngui/ui/src/pages/AcceptInvitations/index.ts deleted file mode 100644 index 79a6a7aa..00000000 --- a/ngui/ui/src/pages/AcceptInvitations/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import AcceptInvitations from "./AcceptInvitations"; - -export default AcceptInvitations; diff --git a/ngui/ui/src/pages/Initialize/Initialize.tsx b/ngui/ui/src/pages/Initialize/Initialize.tsx new file mode 100644 index 00000000..07f4ce25 --- /dev/null +++ b/ngui/ui/src/pages/Initialize/Initialize.tsx @@ -0,0 +1,5 @@ +import InitializeContainer from "containers/InitializeContainer"; + +const Initialize = () => ; + +export default Initialize; diff --git a/ngui/ui/src/pages/Initialize/index.ts b/ngui/ui/src/pages/Initialize/index.ts new file mode 100644 index 00000000..8ebad971 --- /dev/null +++ b/ngui/ui/src/pages/Initialize/index.ts @@ -0,0 +1,3 @@ +import Initialize from "./Initialize"; + +export default Initialize; diff --git a/ngui/ui/src/pages/Recommendations/Recommendations.tsx b/ngui/ui/src/pages/Recommendations/Recommendations.tsx index 97efaf76..7caeaaf5 100644 --- a/ngui/ui/src/pages/Recommendations/Recommendations.tsx +++ b/ngui/ui/src/pages/Recommendations/Recommendations.tsx @@ -1,14 +1,13 @@ import CachedOutlinedIcon from "@mui/icons-material/CachedOutlined"; import RestoreOutlinedIcon from "@mui/icons-material/RestoreOutlined"; import Stack from "@mui/material/Stack"; -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; import ActionBar from "components/ActionBar"; import DataSourceMultiSelect from "components/DataSourceMultiSelect"; import Mocked, { MESSAGE_TYPES } from "components/Mocked"; import PageContentWrapper from "components/PageContentWrapper"; import RecommendationsOverviewContainer from "containers/RecommendationsOverviewContainer"; import RecommendationsOverviewContainerMocked from "containers/RecommendationsOverviewContainer/RecommendationsOverviewContainerMocked"; -import { useApiData } from "hooks/useApiData"; +import { useAllDataSources } from "hooks/coreData"; import { useIsNebiusConnectionEnabled } from "hooks/useIsNebiusConnectionEnabled"; import { useSyncQueryParamWithState } from "hooks/useSyncQueryParamWithState"; import RecommendationsOverviewService from "services/RecommendationsOverviewService"; @@ -47,9 +46,7 @@ const getActionBar = ({ forceCheck, isForceCheckAvailable }) => ({ const RECOMMENDABLE_DATA_SOURCES_BASE = [AWS_CNR, AZURE_CNR, ALIBABA_CNR, GCP_CNR]; const RecommendationsPage = ({ isMock }) => { - const { - apiData: { cloudAccounts = [] } - } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); const isNebiusConnectionEnabled = useIsNebiusConnectionEnabled(); const [selectedDataSourceIds, setSelectedDataSourceIds] = useSyncQueryParamWithState({ @@ -60,7 +57,7 @@ const RecommendationsPage = ({ isMock }) => { const selectedDataSourceTypes: string[] = Array.from( new Set( - cloudAccounts + dataSources .filter((cloudAccount) => selectedDataSourceIds.includes(cloudAccount.id)) .map((cloudAccount) => cloudAccount.type) ) @@ -76,8 +73,8 @@ const RecommendationsPage = ({ isMock }) => {
- [...RECOMMENDABLE_DATA_SOURCES_BASE, ...(isNebiusConnectionEnabled ? [NEBIUS] : [])].includes(cloudAccount.type) + allDataSources={dataSources.filter((dataSource) => + [...RECOMMENDABLE_DATA_SOURCES_BASE, ...(isNebiusConnectionEnabled ? [NEBIUS] : [])].includes(dataSource.type) )} dataSourceIds={selectedDataSourceIds} onChange={setSelectedDataSourceIds} diff --git a/ngui/ui/src/pages/Resources/Resources.tsx b/ngui/ui/src/pages/Resources/Resources.tsx index 83cbca02..7d1f20ed 100644 --- a/ngui/ui/src/pages/Resources/Resources.tsx +++ b/ngui/ui/src/pages/Resources/Resources.tsx @@ -2,7 +2,7 @@ import { Navigate, useSearchParams } from "react-router-dom"; import Mocked from "components/Mocked"; import { ResourcesMocked } from "components/Resources"; import ResourcesContainer from "containers/ResourcesContainer"; -import { useOrganizationPerspectives } from "hooks/useOrganizationPerspectives"; +import { useOrganizationPerspectives } from "hooks/coreData"; import { DAILY_EXPENSES_BREAKDOWN_BY_PARAMETER_NAME, DAILY_RESOURCE_COUNT_BREAKDOWN_BY_PARAMETER_NAME, diff --git a/ngui/ui/src/pages/RiSpCoverage/RiSpCoverage.tsx b/ngui/ui/src/pages/RiSpCoverage/RiSpCoverage.tsx index 1140076a..5bda66d7 100644 --- a/ngui/ui/src/pages/RiSpCoverage/RiSpCoverage.tsx +++ b/ngui/ui/src/pages/RiSpCoverage/RiSpCoverage.tsx @@ -3,14 +3,13 @@ import FormControl from "@mui/material/FormControl"; import Stack from "@mui/material/Stack"; import { FormattedMessage } from "react-intl"; import { Link as RouterLink } from "react-router-dom"; -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; import ActionBar from "components/ActionBar"; import DataSourceMultiSelect from "components/DataSourceMultiSelect"; import { getBasicRangesSet } from "components/DateRangePicker/defaults"; import PageContentWrapper from "components/PageContentWrapper"; import RangePickerFormContainer from "containers/RangePickerFormContainer"; import RiSpCoverageContainer from "containers/RiSpCoverageContainer"; -import { useApiData } from "hooks/useApiData"; +import { useAllDataSources } from "hooks/coreData"; import { useReactiveDefaultDateRange } from "hooks/useReactiveDefaultDateRange"; import { useSyncQueryParamWithState } from "hooks/useSyncQueryParamWithState"; import { RECOMMENDATIONS, RI_SP_QUERY_PARAMETERS } from "urls"; @@ -30,9 +29,7 @@ const actionBarDefinition = { const TARGET_DATA_SOURCES_TYPES = [AWS_CNR]; const RiSpCoverage = () => { - const { - apiData: { cloudAccounts = [] } - } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); const [selectedDataSourceIds, setSelectedDataSources] = useSyncQueryParamWithState({ queryParamName: RI_SP_QUERY_PARAMETERS.DATA_SOURCE_ID, @@ -57,7 +54,7 @@ const RiSpCoverage = () => { setSelectedEndDate(endDate); }; - const allDataSources = cloudAccounts.filter((cloudAccount) => TARGET_DATA_SOURCES_TYPES.includes(cloudAccount.type)); + const allDataSources = dataSources.filter((dataSource) => TARGET_DATA_SOURCES_TYPES.includes(dataSource.type)); return ( <> diff --git a/ngui/ui/src/reducers.ts b/ngui/ui/src/reducers.ts index 5e86f4b9..10865508 100644 --- a/ngui/ui/src/reducers.ts +++ b/ngui/ui/src/reducers.ts @@ -18,6 +18,7 @@ import PoolsTableReducer, { EXPANDED_POOL_ROWS } from "components/PoolsTable/red import TopAlertReducer, { ALERTS } from "components/TopAlertWrapper/reducer"; import { IS_EXISTING_USER } from "components/TopAlertWrapper/TopAlertWrapper"; import { reducer as TourReducer, TOURS } from "components/Tour"; +import InitializeReducer, { INITIAL } from "containers/InitializeContainer/redux"; import ScopeIdReducer, { SCOPE_ID } from "containers/OrganizationSelectorContainer/reducer"; import RangeDatesReducer, { RANGE_DATES } from "containers/RangePickerFormContainer/reducer"; import RecommendationsControlsStateReducer, { @@ -61,6 +62,7 @@ const authPersistConfig = { }; const appReducer = combineReducers({ + [INITIAL]: InitializeReducer, [API]: ApiReducer, [AUTH]: persistReducer(authPersistConfig, AuthReducer), [RESTAPI]: RestapiReducer, @@ -93,6 +95,7 @@ const rootReducer = (incomingState, action) => { // Do not persist the following keys after signing out const { + [INITIAL]: initial, [API]: api, [RESTAPI]: restapi, [JIRA_BUS]: jiraBus, diff --git a/ngui/ui/src/services/DataSourcesService.ts b/ngui/ui/src/services/DataSourcesService.ts index 0dd3ddfd..fda12734 100644 --- a/ngui/ui/src/services/DataSourcesService.ts +++ b/ngui/ui/src/services/DataSourcesService.ts @@ -1,11 +1,9 @@ import { useDispatch } from "react-redux"; -import { updateDataSource, disconnectDataSource as disconnectDataSourceApi, createSurvey as createSurveyApi } from "api"; -import { DELETE_DATA_SOURCE, GET_DATA_SOURCES, UPDATE_DATA_SOURCE, CREATE_SURVEY } from "api/restapi/actionTypes"; -import { useApiData } from "hooks/useApiData"; +import { updateDataSource, createSurvey as createSurveyApi } from "api"; +import { UPDATE_DATA_SOURCE, CREATE_SURVEY } from "api/restapi/actionTypes"; import { useApiState } from "hooks/useApiState"; import { useOrganizationInfo } from "hooks/useOrganizationInfo"; import { checkError } from "utils/api"; -import { ENVIRONMENT } from "utils/constants"; export const DATASOURCE_SURVEY_TYPES = Object.freeze({ DISCONNECT_LAST_DATA_SOURCE: "disconnect_last_account" @@ -29,32 +27,6 @@ const useUpdateDataSource = () => { return { onUpdate, isLoading }; }; -const useDisconnectDataSource = () => { - const dispatch = useDispatch(); - - const { isLoading } = useApiState(DELETE_DATA_SOURCE); - - const disconnectDataSource = (id) => - new Promise((resolve, reject) => { - dispatch((_, getState) => { - dispatch(disconnectDataSourceApi(id)) - .then(() => checkError(DELETE_DATA_SOURCE, getState())) - .then(() => resolve()) - .catch(() => reject()); - }); - }); - - return { disconnectDataSource, isLoading }; -}; - -const useIsLastDataSource = () => { - const { - apiData: { cloudAccounts = [] } - } = useApiData(GET_DATA_SOURCES); - - return cloudAccounts.filter(({ type }) => type !== ENVIRONMENT).length === 1; -}; - const useCreateSurvey = () => { const dispatch = useDispatch(); const { organizationId } = useOrganizationInfo(); @@ -77,8 +49,6 @@ const useCreateSurvey = () => { function DataSourcesService() { return { useUpdateDataSource, - useDisconnectDataSource, - useIsLastDataSource, useCreateSurvey }; } diff --git a/ngui/ui/src/services/InvitationsService.ts b/ngui/ui/src/services/InvitationsService.ts deleted file mode 100644 index b2378a49..00000000 --- a/ngui/ui/src/services/InvitationsService.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useEffect } from "react"; -import { useDispatch } from "react-redux"; -import { getInvitations } from "api"; -import { GET_TOKEN } from "api/auth/actionTypes"; -import { GET_INVITATIONS } from "api/restapi/actionTypes"; -import { useApiData } from "hooks/useApiData"; -import { useApiState } from "hooks/useApiState"; - -export const useGet = () => { - const dispatch = useDispatch(); - - const { isLoading, shouldInvoke } = useApiState(GET_INVITATIONS); - const { - apiData: { userId } - } = useApiData(GET_TOKEN); - - useEffect(() => { - if (shouldInvoke && userId) { - dispatch(getInvitations()); - } - }, [dispatch, shouldInvoke, userId]); - - const { apiData: invitations } = useApiData(GET_INVITATIONS, []); - - return { isLoading, invitations }; -}; - -function InvitationsService() { - return { useGet }; -} - -export default InvitationsService; diff --git a/ngui/ui/src/services/OrganizationOptionsService.ts b/ngui/ui/src/services/OrganizationOptionsService.ts index cff243bd..d9342f70 100644 --- a/ngui/ui/src/services/OrganizationOptionsService.ts +++ b/ngui/ui/src/services/OrganizationOptionsService.ts @@ -11,8 +11,6 @@ import { } from "api"; import { getRecommendationsDownloadLimit, - updateOrganizationThemeSettings, - updateOrganizationPerspectives, getS3DuplicatesOrganizationSettings, updateS3DuplicatesOrganizationSettings } from "api/restapi/actionCreators"; @@ -25,8 +23,6 @@ import { CREATE_ORGANIZATION_OPTION, DELETE_ORGANIZATION_OPTION, GET_RECOMMENDATIONS_DOWNLOAD_OPTIONS, - UPDATE_ORGANIZATION_THEME_SETTINGS, - UPDATE_ORGANIZATION_PERSPECTIVES, GET_S3_DUPLICATES_ORGANIZATION_SETTINGS, UPDATE_S3_DUPLICATES_ORGANIZATION_SETTINGS } from "api/restapi/actionTypes"; @@ -34,7 +30,6 @@ import { useApiData } from "hooks/useApiData"; import { useApiState } from "hooks/useApiState"; import { useOrganizationInfo } from "hooks/useOrganizationInfo"; import { isError, checkError } from "utils/api"; -import { OPTSCALE_MODE_OPTION } from "utils/constants"; import { parseJSON } from "utils/strings"; const useGet = (withValues) => { @@ -72,30 +67,6 @@ const useGetOption = () => { return { isGetOrganizationOptionLoading: isLoading, value: jsonValue, getOption }; }; -// OptScale mode is a special option, it is "global", meaning that other components visibility might rely on it -// They are wrapped with ModeWrapper, which might cause side effects. One that is known is a conflict between optscale_mode and other options. -// This is a "quick" fix, the implementation will most likely to be changed once we migrate to Apollo and implement a new initialization process. -// Note that there is no name passed to useApiState intentionally. -const useGetOptscaleMode = () => { - const dispatch = useDispatch(); - const { organizationId } = useOrganizationInfo(); - - const { apiData: option } = useApiData(GET_ORGANIZATION_OPTION, "{}"); - - const { isLoading, shouldInvoke } = useApiState(GET_ORGANIZATION_OPTION, { organizationId }); - - useEffect(() => { - if (shouldInvoke) { - dispatch(getOrganizationOption(organizationId, OPTSCALE_MODE_OPTION)); - } - }, [dispatch, organizationId, shouldInvoke]); - - return { - isGetOrganizationOptionLoading: isLoading, - option: parseJSON(option) - }; -}; - const useDeleteOption = () => { const dispatch = useDispatch(); const { organizationId } = useOrganizationInfo(); @@ -138,23 +109,6 @@ const useUpdateOption = () => { return { isUpdateOrganizationOptionLoading: isLoading, updateOption }; }; -const useUpdateThemeSettings = () => { - const dispatch = useDispatch(); - const { organizationId } = useOrganizationInfo(); - - const { isLoading } = useApiState(UPDATE_ORGANIZATION_THEME_SETTINGS); - - const update = (value) => { - dispatch((_, getState) => { - dispatch(updateOrganizationThemeSettings(organizationId, value)).then(() => - checkError(UPDATE_ORGANIZATION_THEME_SETTINGS, getState()) - ); - }); - }; - - return { isLoading, update }; -}; - const useCreateOption = () => { const dispatch = useDispatch(); const { organizationId } = useOrganizationInfo(); @@ -246,27 +200,6 @@ const useUpdateRecommendationOptions = () => { return { isLoading, updateRecommendationOptions }; }; -const useUpdateOrganizationPerspectives = () => { - const dispatch = useDispatch(); - const { organizationId } = useOrganizationInfo(); - - const { isLoading } = useApiState(UPDATE_ORGANIZATION_PERSPECTIVES); - - const update = (value, onSuccess) => { - dispatch((_, getState) => { - dispatch(updateOrganizationPerspectives(organizationId, value)) - .then(() => checkError(UPDATE_ORGANIZATION_PERSPECTIVES, getState())) - .then(() => { - if (typeof onSuccess === "function") { - onSuccess(); - } - }); - }); - }; - - return { isLoading, update }; -}; - const useGetRecommendationsDownloadOptions = () => { const dispatch = useDispatch(); const { organizationId } = useOrganizationInfo(); @@ -332,7 +265,6 @@ function OrganizationOptionsService() { return { useGet, useGetOption, - useGetOptscaleMode, useUpdateOption, useCreateOption, useDeleteOption, @@ -340,8 +272,6 @@ function OrganizationOptionsService() { useGetRecommendationOptionsOnce, useUpdateRecommendationOptions, useGetRecommendationsDownloadOptions, - useUpdateThemeSettings, - useUpdateOrganizationPerspectives, useGetS3DuplicatesOrganizationSettings, useUpdateS3DuplicatedOrganizationSettings }; diff --git a/ngui/ui/src/services/OrganizationsService.ts b/ngui/ui/src/services/OrganizationsService.ts deleted file mode 100644 index b854b7f8..00000000 --- a/ngui/ui/src/services/OrganizationsService.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { useDispatch } from "react-redux"; -import { createOrganization, getOrganizations as getOrganizationsActionCreator, RESTAPI } from "api"; -import { CREATE_ORGANIZATION, GET_ORGANIZATIONS } from "api/restapi/actionTypes"; -import { useApiState } from "hooks/useApiState"; -import { checkError } from "utils/api"; - -const useCreate = () => { - const dispatch = useDispatch(); - - const { isLoading } = useApiState(CREATE_ORGANIZATION); - - const onCreate = (name) => - new Promise((resolve, reject) => { - dispatch((_, getState) => { - dispatch(createOrganization(name)) - .then(() => checkError(CREATE_ORGANIZATION, getState())) - .then(() => resolve(getState()[RESTAPI].CREATE_ORGANIZATION)) - .catch(() => reject()); - }); - }); - - return { onCreate, isLoading }; -}; - -const useGet = () => { - const dispatch = useDispatch(); - - const { isLoading } = useApiState(GET_ORGANIZATIONS); - - const getOrganizations = () => - new Promise((resolve, reject) => { - dispatch((_, getState) => { - dispatch(getOrganizationsActionCreator()) - .then(() => checkError(GET_ORGANIZATIONS, getState())) - .then(() => resolve()) - .catch(() => reject()); - }); - }); - - return { getOrganizations, isLoading }; -}; - -function OrganizationsService() { - return { useGet, useCreate }; -} - -export default OrganizationsService; diff --git a/ngui/ui/src/services/ResetPasswordServices.ts b/ngui/ui/src/services/ResetPasswordServices.ts index 95865873..359305b5 100644 --- a/ngui/ui/src/services/ResetPasswordServices.ts +++ b/ngui/ui/src/services/ResetPasswordServices.ts @@ -1,8 +1,8 @@ +import { useMutation } from "@apollo/client"; import { useDispatch } from "react-redux"; -import { restorePassword, getToken, updateUser } from "api"; -import { GET_TOKEN, UPDATE_USER } from "api/auth/actionTypes"; +import { restorePassword } from "api"; import { RESTORE_PASSWORD } from "api/restapi/actionTypes"; -import { useApiData } from "hooks/useApiData"; +import { CREATE_TOKEN, UPDATE_USER } from "graphql/api/auth/queries"; import { useApiState } from "hooks/useApiState"; import { isError } from "utils/api"; @@ -27,77 +27,47 @@ const useSendVerificationCode = () => { }; const useGetVerificationCodeToken = () => { - const dispatch = useDispatch(); - - const { isLoading } = useApiState(GET_TOKEN); + const [createToken, { loading: loginLoading }] = useMutation(CREATE_TOKEN); const onGet = (email: string, code: string) => - new Promise((resolve, reject) => { - dispatch((_, getState) => { - dispatch( - getToken({ - email, - code, - isTokenTemporary: true - }) - ).then(() => { - if (!isError(GET_TOKEN, getState())) { - return resolve(); - } - return reject(); - }); - }); - }); + createToken({ variables: { email, code } }).then(({ data: { token } }) => Promise.resolve(token)); - return { onGet, isLoading }; + return { onGet, isLoading: loginLoading }; }; const useUpdateUserPassword = () => { - const dispatch = useDispatch(); - - const { isLoading } = useApiState(UPDATE_USER); - - const { - apiData: { userId } - } = useApiData(GET_TOKEN); - - const onUpdate = (newPassword: string) => - new Promise((resolve, reject) => { - dispatch((_, getState) => { - dispatch( - updateUser(userId, { - password: newPassword - }) - ).then(() => { - if (!isError(UPDATE_USER, getState())) { - return resolve(); - } - return reject(); - }); - }); + const [updateUser, { loading: updateUserLoading }] = useMutation(UPDATE_USER); + + const onUpdate = ( + token: { + user_id: string; + user_email: string; + token: string; + }, + newPassword: string + ) => + updateUser({ + variables: { + id: token.user_id, + params: { password: newPassword } + }, + context: { + headers: { + "x-optscale-token": token.token + } + } }); - return { onUpdate, isLoading }; + return { onUpdate, isLoading: updateUserLoading }; }; const useGetNewToken = () => { - const dispatch = useDispatch(); - - const { isLoading } = useApiState(GET_TOKEN); + const [createToken, { loading: loginLoading }] = useMutation(CREATE_TOKEN); const onGet = (email: string, password: string) => - new Promise((resolve, reject) => { - dispatch((_, getState) => { - dispatch(getToken({ email, password })).then(() => { - if (!isError(GET_TOKEN, getState())) { - return resolve(); - } - return reject(); - }); - }); - }); + createToken({ variables: { email, password } }).then(({ data: { token } }) => Promise.resolve(token)); - return { onGet, isLoading }; + return { onGet, isLoading: loginLoading }; }; function ResetPasswordServices() { diff --git a/ngui/ui/src/services/VerifyEmailService.ts b/ngui/ui/src/services/VerifyEmailService.ts index 0f48c255..0d1719f4 100644 --- a/ngui/ui/src/services/VerifyEmailService.ts +++ b/ngui/ui/src/services/VerifyEmailService.ts @@ -1,8 +1,9 @@ import { useCallback } from "react"; +import { useMutation } from "@apollo/client"; import { useDispatch } from "react-redux"; -import { getToken, verifyEmail } from "api"; -import { GET_TOKEN } from "api/auth/actionTypes"; +import { verifyEmail } from "api"; import { VERIFY_EMAIL } from "api/restapi/actionTypes"; +import { CREATE_TOKEN } from "graphql/api/auth/queries"; import { useApiState } from "hooks/useApiState"; import { isError } from "utils/api"; @@ -30,28 +31,12 @@ const useSendEmailVerificationCode = () => { }; const useGetEmailVerificationCodeToken = () => { - const dispatch = useDispatch(); - - const { isLoading } = useApiState(GET_TOKEN); + const [createToken, { loading: loginLoading }] = useMutation(CREATE_TOKEN); const onGet = (email: string, code: string) => - new Promise((resolve, reject) => { - dispatch((_, getState) => { - dispatch( - getToken({ - email, - code - }) - ).then(() => { - if (!isError(GET_TOKEN, getState())) { - return resolve(); - } - return reject(); - }); - }); - }); + createToken({ variables: { email, code } }).then(({ data: { token } }) => Promise.resolve(token)); - return { onGet, isLoading }; + return { onGet, isLoading: loginLoading }; }; function VerifyEmailService() { diff --git a/ngui/ui/src/stories/Components/CloudExpensesChart.stories.tsx b/ngui/ui/src/stories/Components/CloudExpensesChart.stories.tsx index c2afaeac..c889c48d 100644 --- a/ngui/ui/src/stories/Components/CloudExpensesChart.stories.tsx +++ b/ngui/ui/src/stories/Components/CloudExpensesChart.stories.tsx @@ -13,7 +13,7 @@ export default { const cloudAccounts = [ { - details: { tracked: 768, last_month_cost: 559.7434027942, cost: 141.0194095445, forecast: 546.45 }, + details: { resources: 768, last_month_cost: 559.7434027942, cost: 141.0194095445, forecast: 546.45 }, type: "aws_cnr", name: "AWS", organization_id: "9eb8d5fe-b5a8-4c6f-899a-bf761109d11f", @@ -37,7 +37,7 @@ const cloudAccounts = [ id: "3d4236be-e167-4801-86ff-944943a9ae6f" }, { - details: { tracked: 22, last_month_cost: 85.1889819768111, cost: 5.958978755733334, forecast: 23.09 }, + details: { resources: 22, last_month_cost: 85.1889819768111, cost: 5.958978755733334, forecast: 23.09 }, type: "azure_cnr", name: "EK_azure_connection", organization_id: "9eb8d5fe-b5a8-4c6f-899a-bf761109d11f", diff --git a/ngui/ui/src/stories/Components/ClusterTypesTable.stories.tsx b/ngui/ui/src/stories/Components/ClusterTypesTable.stories.tsx index ae00aebf..ec3a1c64 100644 --- a/ngui/ui/src/stories/Components/ClusterTypesTable.stories.tsx +++ b/ngui/ui/src/stories/Components/ClusterTypesTable.stories.tsx @@ -1,7 +1,5 @@ import { Provider } from "react-redux"; import configureMockStore from "redux-mock-store"; -import { GET_ORGANIZATION_ALLOWED_ACTIONS } from "api/auth/actionTypes"; -import { GET_ORGANIZATIONS } from "api/restapi/actionTypes"; import ClusterTypesTable from "components/ClusterTypesTable"; import { MOCKED_ORGANIZATION_ID } from "stories"; @@ -25,23 +23,7 @@ export const withoutManageResourcePermission = (args) => ( export const withManageResourcePermission = (args) => { const store = mockStore({ - organizationId: MOCKED_ORGANIZATION_ID, - restapi: { - [GET_ORGANIZATIONS]: { - organizations: [ - { - id: MOCKED_ORGANIZATION_ID - } - ] - } - }, - auth: { - [GET_ORGANIZATION_ALLOWED_ACTIONS]: { - allowedActions: { - my_organization_id: ["MANAGE_RESOURCES"] - } - } - } + organizationId: MOCKED_ORGANIZATION_ID }); return ( diff --git a/ngui/ui/src/stories/Pages/Dashboard.stories.tsx b/ngui/ui/src/stories/Pages/Dashboard.stories.tsx index f99c4224..26669e09 100644 --- a/ngui/ui/src/stories/Pages/Dashboard.stories.tsx +++ b/ngui/ui/src/stories/Pages/Dashboard.stories.tsx @@ -1,6 +1,6 @@ import { useContext } from "react"; import { Provider } from "react-redux"; -import { GET_DATA_SOURCES, GET_ENVIRONMENTS } from "api/restapi/actionTypes"; +import { GET_ENVIRONMENTS } from "api/restapi/actionTypes"; import Dashboard from "components/Dashboard"; import { MockPermissionsStateContext } from "stories"; @@ -318,77 +318,11 @@ const environments = [ } ]; -const onlyEnvironmentDataSources = [ - { - deleted_at: 0, - id: "e2bc1b1f-deeb-4eb7-892a-d3714fde97c6", - created_at: 1630193075, - name: "Environment", - type: "environment", - config: {}, - organization_id: "a42c727b-52dd-4b48-8785-dfaed6b75404", - auto_import: false, - import_period: 1, - last_import_at: 1631992150, - last_import_modified_at: 0, - account_id: "4e84a988-a4ab-418a-a576-9f0e9611e5fb", - process_recommendations: false, - last_import_attempt_at: 1631516521, - last_import_attempt_error: null, - details: {} - } -]; - -const allDataSources = [ - { - deleted_at: 0, - id: "e2bc1b1f-deeb-4eb7-892a-d3714fde97c6", - created_at: 1630193075, - name: "Environment", - type: "environment", - config: {}, - organization_id: "a42c727b-52dd-4b48-8785-dfaed6b75404", - auto_import: false, - import_period: 1, - last_import_at: 1631992150, - last_import_modified_at: 0, - account_id: "4e84a988-a4ab-418a-a576-9f0e9611e5fb", - process_recommendations: false, - last_import_attempt_at: 1631516521, - last_import_attempt_error: null, - details: {} - }, - { - deleted_at: 0, - id: "12bc1b1f-deeb-4eb7-892a-d3714fde97c6", - created_at: 1630193075, - name: "AWS", - type: "aws_cnr", - config: {}, - organization_id: "a42c727b-52dd-4b48-8785-dfaed6b75404", - auto_import: false, - import_period: 1, - last_import_at: 1631992150, - last_import_modified_at: 0, - account_id: "4e84a988-a4ab-418a-a576-9f0e9611e5fb", - process_recommendations: false, - last_import_attempt_at: 1631516521, - last_import_attempt_error: null, - details: {} - } -]; - const MockPermissionsContextWrapper = ({ children }) => children(useContext(MockPermissionsStateContext)); export const NoDataSources = () => ( {({ mockStore, mockState }) => { - mockState.mockRestapi({ - [GET_DATA_SOURCES]: { - cloudAccounts: [] - } - }); - const store = mockStore(mockState); return ( @@ -404,9 +338,6 @@ export const OnlyEnvironmentDataSources = (args) => ( {({ mockStore, mockState }) => { mockState.mockRestapi({ - [GET_DATA_SOURCES]: { - cloudAccounts: onlyEnvironmentDataSources - }, [GET_ENVIRONMENTS]: args.withEnvironments ? [] : environments }); @@ -425,9 +356,6 @@ export const AllDataSources = (args) => ( {({ mockStore, mockState }) => { mockState.mockRestapi({ - [GET_DATA_SOURCES]: { - cloudAccounts: allDataSources - }, [GET_ENVIRONMENTS]: args.withEnvironments ? [] : environments }); diff --git a/ngui/ui/src/tests/utils/mockStore/MockState.test.ts b/ngui/ui/src/tests/utils/mockStore/MockState.test.ts index 1faeb6ff..b47a876d 100644 --- a/ngui/ui/src/tests/utils/mockStore/MockState.test.ts +++ b/ngui/ui/src/tests/utils/mockStore/MockState.test.ts @@ -1,5 +1,4 @@ -import { GET_POOL_ALLOWED_ACTIONS, GET_ORGANIZATION_ALLOWED_ACTIONS } from "api/auth/actionTypes"; -import { GET_ORGANIZATIONS } from "api/restapi/actionTypes"; +import { GET_POOL_ALLOWED_ACTIONS } from "api/auth/actionTypes"; import { MockState } from "utils/MockState"; const POOL_ID = "pool_uuid"; @@ -81,23 +80,7 @@ describe("mockOrganizationPermissions method testing", () => { const mockState = MockState(); mockState.mockOrganizationPermissions(ORGANIZATION_ID, ["manage_something"]); expect(mockState.state).toEqual({ - organizationId: ORGANIZATION_ID, - restapi: { - [GET_ORGANIZATIONS]: { - organizations: [ - { - id: ORGANIZATION_ID - } - ] - } - }, - auth: { - [GET_ORGANIZATION_ALLOWED_ACTIONS]: { - allowedActions: { - [ORGANIZATION_ID]: ["manage_something"] - } - } - } + organizationId: ORGANIZATION_ID }); }); describe("extracting existed state", () => { @@ -108,19 +91,7 @@ describe("mockOrganizationPermissions method testing", () => { data: {} }, restapi: { - someRestApiKey: "randomString", - [GET_ORGANIZATIONS]: { - organizations: [{ id: "org1" }, { id: "org2" }] - } - }, - auth: { - someAuthKey: "randomString", - [GET_ORGANIZATION_ALLOWED_ACTIONS]: { - allowedActions: { - org1: ["p1", "p2"], - org2: [] - } - } + someRestApiKey: "randomString" } }); mockState.mockOrganizationPermissions(ORGANIZATION_ID, ["manage_something"]); @@ -130,26 +101,7 @@ describe("mockOrganizationPermissions method testing", () => { data: {} }, restapi: { - someRestApiKey: "randomString", - [GET_ORGANIZATIONS]: { - organizations: [ - { id: "org1" }, - { id: "org2" }, - { - id: ORGANIZATION_ID - } - ] - } - }, - auth: { - someAuthKey: "randomString", - [GET_ORGANIZATION_ALLOWED_ACTIONS]: { - allowedActions: { - org1: ["p1", "p2"], - org2: [], - [ORGANIZATION_ID]: ["manage_something"] - } - } + someRestApiKey: "randomString" } }); }); @@ -159,55 +111,16 @@ describe("mockOrganizationPermissions method testing", () => { }); mockState.mockOrganizationPermissions(ORGANIZATION_ID, ["manage_something"]); expect(mockState.state).toEqual({ - organizationId: "existedOrganizationId", - restapi: { - [GET_ORGANIZATIONS]: { - organizations: [ - { - id: ORGANIZATION_ID - } - ] - } - }, - auth: { - [GET_ORGANIZATION_ALLOWED_ACTIONS]: { - allowedActions: { - [ORGANIZATION_ID]: ["manage_something"] - } - } - } + organizationId: "existedOrganizationId" }); }); test("extend permissions", () => { const mockState = MockState({ - organizationId: "existedOrganizationId", - auth: { - [GET_ORGANIZATION_ALLOWED_ACTIONS]: { - allowedActions: { - [ORGANIZATION_ID]: ["existed_permission"] - } - } - } + organizationId: "existedOrganizationId" }); mockState.mockOrganizationPermissions(ORGANIZATION_ID, ["manage_something"]); expect(mockState.state).toEqual({ - organizationId: "existedOrganizationId", - restapi: { - [GET_ORGANIZATIONS]: { - organizations: [ - { - id: ORGANIZATION_ID - } - ] - } - }, - auth: { - [GET_ORGANIZATION_ALLOWED_ACTIONS]: { - allowedActions: { - [ORGANIZATION_ID]: ["existed_permission", "manage_something"] - } - } - } + organizationId: "existedOrganizationId" }); }); }); diff --git a/ngui/ui/src/translations/en-US/app.json b/ngui/ui/src/translations/en-US/app.json index 60e5ce9d..697e8947 100644 --- a/ngui/ui/src/translations/en-US/app.json +++ b/ngui/ui/src/translations/en-US/app.json @@ -910,6 +910,7 @@ "infrastructureConstraints": "Infrastructure constraints", "inherit": "Inherit", "initialization": "Initialization", + "initializingOptscale": "OptScale is initializing. Please stand by.", "initiationDate": "Initiation date", "input": "Input", "inputMustContainOnlyUppercaseLatinLettersNumberOrUnderscore": "{inputName} must contain only uppercase latin letters, numbers, or underscore", diff --git a/ngui/ui/src/urls.ts b/ngui/ui/src/urls.ts index 080be4c0..f3539faa 100644 --- a/ngui/ui/src/urls.ts +++ b/ngui/ui/src/urls.ts @@ -30,10 +30,11 @@ export const HOME = "/"; export const getHomeUrl = (organizationId) => organizationId ? concatenateUrl([HOME, `?organizationId=${organizationId}`], "", "") : HOME; export const SHOW_POLICY_QUERY_PARAM = "showPolicy"; -export const HOME_FIRST_TIME = `/?${SHOW_POLICY_QUERY_PARAM}=true`; export const LOGIN = "/login"; export const REGISTER = "/register"; export const INVITED = "/invited"; +export const INITIALIZE = "/initialize"; +export const RESET_PASSWORD = "/reset-password"; export const ACCEPT_INVITATION = "/accept-invitation"; export const ACCEPT_INVITATIONS = "/accept-invitations"; export const PASSWORD_RECOVERY = "/password-recovery"; @@ -592,3 +593,5 @@ export const isProduction = () => window.location.origin === PRODUCTION; export const isDemo = () => window.location.origin === DEMO; export const USER_EMAIL_QUERY_PARAMETER_NAME = "userEmail"; + +export const NEXT_QUERY_PARAMETER_NAME = "next"; diff --git a/ngui/ui/src/utils/MockState.ts b/ngui/ui/src/utils/MockState.ts index 64b618df..fd97f7ce 100644 --- a/ngui/ui/src/utils/MockState.ts +++ b/ngui/ui/src/utils/MockState.ts @@ -1,5 +1,4 @@ -import { GET_ORGANIZATION_ALLOWED_ACTIONS, GET_POOL_ALLOWED_ACTIONS, GET_RESOURCE_ALLOWED_ACTIONS } from "api/auth/actionTypes"; -import { GET_ORGANIZATIONS } from "api/restapi/actionTypes"; +import { GET_POOL_ALLOWED_ACTIONS, GET_RESOURCE_ALLOWED_ACTIONS } from "api/auth/actionTypes"; export function MockState(defaultState = {}) { let state = defaultState; @@ -44,46 +43,11 @@ export function MockState(defaultState = {}) { }; }; - const mockOrganizationPermissions = (organizationId, allowedActions) => { + const mockOrganizationPermissions = (organizationId) => { state = { ...state, // get organization id from state or set a new value - organizationId: state.organizationId || organizationId, - restapi: { - // copy entire state - ...state.restapi, - [GET_ORGANIZATIONS]: { - organizations: [ - // copy organizations list if exists - ...(state.restapi?.[GET_ORGANIZATIONS]?.organizations ?? []), - // if organization with "organizationId" already exists in the state - do nothing, otherwise add new organization - ...(state.restapi?.[GET_ORGANIZATIONS]?.organizations.some((organization) => organization.id === organizationId) - ? [] - : [ - { - id: organizationId - } - ]) - ] - } - }, - auth: { - // copy entire auth state - ...state.auth, - [GET_ORGANIZATION_ALLOWED_ACTIONS]: { - allowedActions: { - // copy allowedActions object if exists - ...(state.auth?.[GET_ORGANIZATION_ALLOWED_ACTIONS]?.allowedActions ?? {}), - // merge existed permissions (if they exist) with new permissions - [organizationId]: Array.from( - new Set([ - ...(state.auth?.[GET_ORGANIZATION_ALLOWED_ACTIONS]?.allowedActions?.[organizationId] ?? []), - ...allowedActions - ]) - ) - } - } - } + organizationId: state.organizationId || organizationId }; }; diff --git a/ngui/ui/src/utils/columns/resource.tsx b/ngui/ui/src/utils/columns/resource.tsx index e011d500..3708d550 100644 --- a/ngui/ui/src/utils/columns/resource.tsx +++ b/ngui/ui/src/utils/columns/resource.tsx @@ -1,18 +1,15 @@ import { FormattedMessage } from "react-intl"; -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; import CaptionedCell from "components/CaptionedCell"; import CloudResourceId from "components/CloudResourceId"; import TextWithDataTestId from "components/TextWithDataTestId"; -import { useApiData } from "hooks/useApiData"; +import { useAllDataSources } from "hooks/coreData"; import { getCloudResourceIdentifier } from "utils/resources"; import { RESOURCE_ID_COLUMN_CELL_STYLE } from "utils/tables"; const CellContent = ({ rowData }) => { const { resource_name: name, resource_id: resourceId, cloud_account_id: dataSourceId } = rowData; - const { - apiData: { cloudAccounts: dataSources = [] } - } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); return ( diff --git a/ngui/ui/src/utils/columns/userLocation.tsx b/ngui/ui/src/utils/columns/userLocation.tsx index d4fbddc8..56c4ff6c 100644 --- a/ngui/ui/src/utils/columns/userLocation.tsx +++ b/ngui/ui/src/utils/columns/userLocation.tsx @@ -1,18 +1,13 @@ import { FormattedMessage } from "react-intl"; -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; import CaptionedCell from "components/CaptionedCell"; import CloudLabel from "components/CloudLabel"; import TextWithDataTestId from "components/TextWithDataTestId"; -import { useApiData } from "hooks/useApiData"; +import { useAllDataSources } from "hooks/coreData"; -const Cell = ({ - row: { - original: { cloud_account_id: dataSourceId, cloud_account_name: dataSourceName, cloud_type: dataSourceType, region } - } -}) => { - const { - apiData: { cloudAccounts: dataSources = [] } - } = useApiData(GET_DATA_SOURCES); +const Cell = ({ row: { original } }) => { + const { cloud_account_id: dataSourceId, cloud_account_name: dataSourceName, cloud_type: dataSourceType, region } = original; + + const dataSources = useAllDataSources(); return ( diff --git a/ngui/ui/src/utils/constants.ts b/ngui/ui/src/utils/constants.ts index 0851c784..a0c3d4c7 100644 --- a/ngui/ui/src/utils/constants.ts +++ b/ngui/ui/src/utils/constants.ts @@ -961,3 +961,8 @@ export const OPTSCALE_MODE = Object.freeze({ export const DATASET_NAME_LENGTH_LIMIT = 70; export const DATASET_PATH_LENGTH_LIMIT = 70; + +export const AUTH_PROVIDERS = Object.freeze({ + GOOGLE: "google", + MICROSOFT: "microsoft" +}); diff --git a/ngui/ui/src/utils/dataSources/summarizeTenantChildren.ts b/ngui/ui/src/utils/dataSources/summarizeTenantChildren.ts index f458c9c5..f0655401 100644 --- a/ngui/ui/src/utils/dataSources/summarizeTenantChildren.ts +++ b/ngui/ui/src/utils/dataSources/summarizeTenantChildren.ts @@ -4,14 +4,14 @@ export const summarizeChildrenDetails = (children) => isEmpty(children) ? {} : children.reduce( - (acc, { details: { tracked = 0, cost = 0, forecast = 0, last_month_cost: lastMonthCost = 0 } = {} }) => ({ - tracked: acc.tracked + tracked, + (acc, { details: { resources = 0, cost = 0, forecast = 0, last_month_cost: lastMonthCost = 0 } = {} }) => ({ + resources: acc.resources + resources, cost: acc.cost + cost, forecast: acc.forecast + forecast, last_month_cost: acc.last_month_cost + lastMonthCost }), { - tracked: 0, + resources: 0, cost: 0, forecast: 0, last_month_cost: 0 diff --git a/ngui/ui/src/utils/network.ts b/ngui/ui/src/utils/network.ts index 102823f5..e6c92468 100644 --- a/ngui/ui/src/utils/network.ts +++ b/ngui/ui/src/utils/network.ts @@ -60,16 +60,3 @@ export const removeQueryParam = (key) => { * */ export const formQueryString = (params: Record) => queryString.stringify(params); - -export const getMenuRootUrl = (menu) => { - const currentPath = getPathname(); - const currentQueryParams = getQueryParams(); - const menuItems = menu.reduce((result, value) => [...result, ...value.items], []); - const activeElement = menuItems.find( - (el) => typeof el.isActive === "function" && el.isActive(currentPath, currentQueryParams) - ); - if (activeElement) { - return activeElement.route.link; - } - return currentPath; -}; diff --git a/ngui/ui/src/utils/routes/acceptInvitationsRoute.ts b/ngui/ui/src/utils/routes/acceptInvitationsRoute.ts deleted file mode 100644 index 5484918a..00000000 --- a/ngui/ui/src/utils/routes/acceptInvitationsRoute.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ACCEPT_INVITATIONS } from "urls"; -import BaseRoute from "./baseRoute"; - -class AcceptInvitationsRoute extends BaseRoute { - page = "AcceptInvitations"; - - link = ACCEPT_INVITATIONS; - - layout = null; -} - -export default new AcceptInvitationsRoute(); diff --git a/ngui/ui/src/utils/routes/index.ts b/ngui/ui/src/utils/routes/index.ts index 2ad92cdf..e8b02cad 100644 --- a/ngui/ui/src/utils/routes/index.ts +++ b/ngui/ui/src/utils/routes/index.ts @@ -1,5 +1,4 @@ import acceptInvitationRoute from "./acceptInvitationRoute"; -import acceptInvitationsRoute from "./acceptInvitationsRoute"; import anomaliesRoute from "./anomaliesRoute"; import anomalyRoute from "./anomalyRoute"; import archivedRecommendationsRoute from "./archivedRecommendationsRoute"; @@ -37,6 +36,7 @@ import expensesMapRoute from "./expensesMapRoute"; import expensesRoute from "./expensesRoute"; import finOpsPortalRoute from "./finOpsPortalRoute"; import homeRoute from "./homeRoute"; +import initializeRoute from "./initializeRoute"; import integrationsRoute from "./integrationsRoute"; import invitedRoute from "./invitedRoute"; import inviteEmployeesRoute from "./inviteEmployeesRoute"; @@ -99,7 +99,6 @@ import usersRoute from "./usersRoute"; export const routes = [ acceptInvitationRoute, - acceptInvitationsRoute, anomaliesRoute, anomalyRoute, assignmentRulesRoute, @@ -194,6 +193,7 @@ export const routes = [ mlEditArtifactRoute, emailVerificationRoute, publicMlRun, + initializeRoute, // React router 6.x does not require the not found route (*) to be at the end, // but the matchPath hook that is used in the DocsPanel component seems to honor the order. // Moving it to the bottom for "safety" reasons. diff --git a/ngui/ui/src/utils/routes/initializeRoute.ts b/ngui/ui/src/utils/routes/initializeRoute.ts new file mode 100644 index 00000000..9d8ee214 --- /dev/null +++ b/ngui/ui/src/utils/routes/initializeRoute.ts @@ -0,0 +1,12 @@ +import { INITIALIZE } from "urls"; +import BaseRoute from "./baseRoute"; + +class InitializeRoute extends BaseRoute { + page = "Initialize"; + + link = INITIALIZE; + + layout = null; +} + +export default new InitializeRoute(); diff --git a/rest_api/rest_api_server/controllers/cloud_account.py b/rest_api/rest_api_server/controllers/cloud_account.py index 87bb68fd..ee33958d 100644 --- a/rest_api/rest_api_server/controllers/cloud_account.py +++ b/rest_api/rest_api_server/controllers/cloud_account.py @@ -726,7 +726,7 @@ def list(self, details=False, secure=True, only_linked=None, type=None, 'forecast': expense_ctrl.get_monthly_forecast( last_stats['cost'] + current_stats['cost'], current_stats['cost'], first_expenses.get(acc.id)), - 'tracked': current_stats['count'], + 'resources': current_stats['count'], 'last_month_cost': last_stats['cost'], 'discovery_infos': discovery_infos.get(acc.id, {}) } diff --git a/rest_api/rest_api_server/handlers/v2/cloud_account.py b/rest_api/rest_api_server/handlers/v2/cloud_account.py index b77e380d..fb3b78c5 100644 --- a/rest_api/rest_api_server/handlers/v2/cloud_account.py +++ b/rest_api/rest_api_server/handlers/v2/cloud_account.py @@ -216,7 +216,7 @@ async def get(self, organization_id): description: forecast for this month} last_month_cost: {type: integer, description: total cost in last month} - tracked: {type: integer, + resources: {type: integer, description: number of tracked resources} discovery_infos: type: object diff --git a/rest_api/rest_api_server/tests/unittests/test_cloud_accounts.py b/rest_api/rest_api_server/tests/unittests/test_cloud_accounts.py index 64982bd3..4b3255d8 100644 --- a/rest_api/rest_api_server/tests/unittests/test_cloud_accounts.py +++ b/rest_api/rest_api_server/tests/unittests/test_cloud_accounts.py @@ -595,7 +595,7 @@ def test_list_details(self): } if cloud_acc['id'] == cloud_acc2['id']: self.assertEqual(cloud_acc['details']['cost'], 90) - self.assertEqual(cloud_acc['details']['tracked'], 1) + self.assertEqual(cloud_acc['details']['resources'], 1) self.assertEqual(cloud_acc['details']['forecast'], 277) self.assertEqual( cloud_acc['details']['last_month_cost'], 240) @@ -603,7 +603,7 @@ def test_list_details(self): res_discovery_info_2) else: self.assertEqual(cloud_acc['details']['cost'], 450) - self.assertEqual(cloud_acc['details']['tracked'], 2) + self.assertEqual(cloud_acc['details']['resources'], 2) self.assertEqual(cloud_acc['details']['forecast'], 807) self.assertEqual( cloud_acc['details']['last_month_cost'], 180) From 627c2c8f3c1a9c7fed3adc08e250ce2ea9247522 Mon Sep 17 00:00:00 2001 From: ek-hystax <33006768+ek-hystax@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:25:57 +0400 Subject: [PATCH 48/65] OS-8080. Remove logs --- auth/auth_server/controllers/signin.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/auth/auth_server/controllers/signin.py b/auth/auth_server/controllers/signin.py index 82e0fc43..9627f247 100644 --- a/auth/auth_server/controllers/signin.py +++ b/auth/auth_server/controllers/signin.py @@ -62,7 +62,6 @@ def exchange_token(self, code, redirect_uri): "code": code, 'redirect_uri': redirect_uri, } - LOG.error(f"request_body: {request_body}") request = google_requests.Request() response = request( url=self.DEFAULT_TOKEN_URI, @@ -70,14 +69,11 @@ def exchange_token(self, code, redirect_uri): headers={"Content-Type": "application/x-www-form-urlencoded"}, body=urlencode(request_body).encode("utf-8"), ) - LOG.error(f"response: {response}") - LOG.error(f"response.data: {response.data}") response_body = ( response.data.decode("utf-8") if hasattr(response.data, "decode") else response.data ) - LOG.error(f"response_body: {response_body}") if response.status != 200: raise ValueError(response_body) response_data = json.loads(response_body) @@ -85,13 +81,10 @@ def exchange_token(self, code, redirect_uri): def verify(self, code, **kwargs): try: - LOG.error(f"code: {code}") redirect_uri = kwargs.pop('redirect_uri', None) token = self.exchange_token(code, redirect_uri) - LOG.info(f"token: {token}") token_info = id_token.verify_oauth2_token( token, google_requests.Request(), self.client_id()) - LOG.warning(f"token_info: {token_info}") if not token_info.get('email_verified', False): raise ForbiddenException(Err.OA0012, []) email = token_info['email'] From c6c6442257211cb15f280ad1378d628230944bdb Mon Sep 17 00:00:00 2001 From: sd-hystax <110374605+sd-hystax@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:36:40 +0300 Subject: [PATCH 49/65] OS-8019. GCP optimization & gcp tenant cloud accounts ## Description GCP optimization & gcp tenant cloud accounts ## Related issue number OS-8019 ## Checklist * [ ] The pull request title is a good summary of the changes * [ ] Unit tests for the changes exist * [ ] New and existing unit tests pass locally --- diworker/diworker/importers/gcp.py | 2 +- .../versions/a8dfe40f34a8_gcp_tenant.py | 37 +++++++ .../controllers/cloud_account.py | 10 +- .../controllers/resource_observer.py | 4 +- .../handlers/v2/cloud_account.py | 7 +- rest_api/rest_api_server/models/enums.py | 1 + .../tests/unittests/test_api_base.py | 1 + .../tests/unittests/test_cloud_accounts.py | 54 ++++++++++ tools/cloud_adapter/cloud.py | 2 + tools/cloud_adapter/clouds/gcp.py | 46 +++++--- tools/cloud_adapter/clouds/gcp_tenant.py | 102 ++++++++++++++++++ tools/cloud_adapter/enums.py | 1 + 12 files changed, 244 insertions(+), 23 deletions(-) create mode 100644 rest_api/rest_api_server/alembic/versions/a8dfe40f34a8_gcp_tenant.py create mode 100644 tools/cloud_adapter/clouds/gcp_tenant.py diff --git a/diworker/diworker/importers/gcp.py b/diworker/diworker/importers/gcp.py index 1f5ea9ae..9e4b36c8 100644 --- a/diworker/diworker/importers/gcp.py +++ b/diworker/diworker/importers/gcp.py @@ -27,7 +27,7 @@ def detect_period_start(self): if last_exp_date: self.period_start = last_exp_date.replace( hour=0, minute=0, second=0, microsecond=0) - timedelta( - days=1) + days=3) if not self.period_start: super().detect_period_start() diff --git a/rest_api/rest_api_server/alembic/versions/a8dfe40f34a8_gcp_tenant.py b/rest_api/rest_api_server/alembic/versions/a8dfe40f34a8_gcp_tenant.py new file mode 100644 index 00000000..35792efc --- /dev/null +++ b/rest_api/rest_api_server/alembic/versions/a8dfe40f34a8_gcp_tenant.py @@ -0,0 +1,37 @@ +"""gcp_tenant + +Revision ID: a8dfe40f34a8 +Revises: a1d0494e9815 +Create Date: 2024-12-05 06:58:44.080681 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = 'a8dfe40f34a8' +down_revision = 'a1d0494e9815' +branch_labels = None +depends_on = None + +old_cloud_types = sa.Enum('AWS_CNR', 'ALIBABA_CNR', 'AZURE_CNR', 'AZURE_TENANT', + 'KUBERNETES_CNR', 'ENVIRONMENT', 'GCP_CNR', 'NEBIUS', + 'DATABRICKS') +new_cloud_types = sa.Enum('AWS_CNR', 'ALIBABA_CNR', 'AZURE_CNR', 'AZURE_TENANT', + 'KUBERNETES_CNR', 'ENVIRONMENT', 'GCP_CNR', + 'GCP_TENANT', 'NEBIUS', 'DATABRICKS') + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('cloudaccount', 'type', existing_type=old_cloud_types, + type_=new_cloud_types, nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('cloudaccount', 'type', existing_type=new_cloud_types, + type_=old_cloud_types, nullable=False) + # ### end Alembic commands ### diff --git a/rest_api/rest_api_server/controllers/cloud_account.py b/rest_api/rest_api_server/controllers/cloud_account.py index ee33958d..f6f85b29 100644 --- a/rest_api/rest_api_server/controllers/cloud_account.py +++ b/rest_api/rest_api_server/controllers/cloud_account.py @@ -285,7 +285,6 @@ def _get_non_linked_org_aws_accounts(self, org_id): return result def create(self, **kwargs): - LOG.info('Creating cloud account. Input data: %s', kwargs) org_id = kwargs.get('organization_id') self._check_organization(org_id) root_config = kwargs.pop('root_config', None) @@ -311,9 +310,10 @@ def create(self, **kwargs): adapter_cls, config) if last_import_modified_at: kwargs['last_import_modified_at'] = last_import_modified_at + LOG.info('Creating cloud account. Input data: %s', kwargs) ca_obj = CloudAccount(**kwargs) self._validate(ca_obj, True, **kwargs) - if ca_obj.type == CloudTypes.AZURE_TENANT: + if ca_obj.type in [CloudTypes.AZURE_TENANT, CloudTypes.GCP_TENANT]: ca_obj.auto_import = False configuration_res = self._configure_report( adapter_cls, config, organization) @@ -462,7 +462,6 @@ def _need_notification(kwargs): return bool(set(kwargs.keys()).intersection(set(NOTIFY_FIELDS))) def edit(self, item_id, **kwargs): - LOG.info('Editing cloud account %s. Input: %s', item_id, kwargs) self.check_update_restrictions(**kwargs) cloud_acc_obj = self.get(item_id) self._validate(cloud_acc_obj, False, **kwargs) @@ -480,6 +479,8 @@ def edit(self, item_id, **kwargs): self.session, self._config) old_config = cloud_acc_obj.decoded_config config = kwargs.pop('config', {}) + LOG.info('Editing cloud account %s. Input: %s. Config: %s', item_id, + kwargs, bool(config)) if cloud_acc_obj.parent_id and config: raise WrongArgumentsException(Err.OE0211, ['config']) organization = OrganizationController( @@ -762,6 +763,9 @@ def create_children_accounts(self, root_account): **c_config) if c_name in skipped_subscriptions: skipped_subscriptions.pop(c_name, None) + except ConflictException: + if c_name in skipped_subscriptions: + skipped_subscriptions.pop(c_name, None) except Exception as ex: if c_name not in skipped_subscriptions: # Add error reason to root config diff --git a/rest_api/rest_api_server/controllers/resource_observer.py b/rest_api/rest_api_server/controllers/resource_observer.py index 7f257049..5340f9ce 100644 --- a/rest_api/rest_api_server/controllers/resource_observer.py +++ b/rest_api/rest_api_server/controllers/resource_observer.py @@ -84,7 +84,9 @@ def observe(self, organization_id): Err.OE0002, [Organization.__name__, organization_id]) cloud_accounts_map = {} for cloud_account in self._get_cloud_accounts(organization_id): - if cloud_account.type == CloudTypes.AZURE_TENANT: + if cloud_account.type in [ + CloudTypes.AZURE_TENANT, CloudTypes.GCP_TENANT + ]: try: CloudAccountController( self.session, self._config, self.token diff --git a/rest_api/rest_api_server/handlers/v2/cloud_account.py b/rest_api/rest_api_server/handlers/v2/cloud_account.py index fb3b78c5..b9fbde4f 100644 --- a/rest_api/rest_api_server/handlers/v2/cloud_account.py +++ b/rest_api/rest_api_server/handlers/v2/cloud_account.py @@ -59,7 +59,8 @@ async def post(self, **url_params): type: type: string enum: [aws_cnr, azure_cnr, kubernetes_cnr, alibaba_cnr, - azure_tenant, gcp_cnr, nebius, databricks] + azure_tenant, gcp_cnr, nebius, databricks, + gcp_tenant] description: Cloud account type example: aws_cnr config: @@ -200,7 +201,7 @@ async def get(self, organization_id): description: "cloud account type: ('aws_cnr','azure_cnr', 'kubernetes_cnr', 'azure_tenant', 'alibaba_cnr', 'gcp_cnr', - 'nebius', 'databricks')"} + 'nebius', 'databricks', 'gcp_tenant')"} config: type: object description: | @@ -352,7 +353,7 @@ async def get(self, id, **kwargs): description: "cloud account type: ('aws_cnr','azure_cnr', 'alibaba_cnr', 'azure_tenant', 'kubernetes_cnr', 'gcp_cnr', - 'nebius', 'databricks')"} + 'nebius', 'databricks', 'gcp_tenant')"} config: {type: object, description: "Object with credentials to access cloud"} diff --git a/rest_api/rest_api_server/models/enums.py b/rest_api/rest_api_server/models/enums.py index ad7d9162..a08b1cf8 100644 --- a/rest_api/rest_api_server/models/enums.py +++ b/rest_api/rest_api_server/models/enums.py @@ -9,6 +9,7 @@ class CloudTypes(enum.Enum): KUBERNETES_CNR = 'kubernetes_cnr' ENVIRONMENT = 'environment' GCP_CNR = 'gcp_cnr' + GCP_TENANT = 'gcp_tenant' NEBIUS = 'nebius' DATABRICKS = 'databricks' diff --git a/rest_api/rest_api_server/tests/unittests/test_api_base.py b/rest_api/rest_api_server/tests/unittests/test_api_base.py index 7f6a747e..d0e387d2 100644 --- a/rest_api/rest_api_server/tests/unittests/test_api_base.py +++ b/rest_api/rest_api_server/tests/unittests/test_api_base.py @@ -270,6 +270,7 @@ def create_cloud_account(self, organization_id, config, account_id=None, 'azure_cnr': 'azure.Azure', 'gcp_cnr': 'gcp.Gcp', 'nebius': 'nebius.Nebius', + 'gcp_tenant': 'gcp_tenant.GcpTenant' } if not account_id: account_id = self.gen_id() diff --git a/rest_api/rest_api_server/tests/unittests/test_cloud_accounts.py b/rest_api/rest_api_server/tests/unittests/test_cloud_accounts.py index 4b3255d8..28d0237e 100644 --- a/rest_api/rest_api_server/tests/unittests/test_cloud_accounts.py +++ b/rest_api/rest_api_server/tests/unittests/test_cloud_accounts.py @@ -2035,3 +2035,57 @@ def test_databricks_patch_config(self): def test_adapter_implemented(self): for t in list(CloudTypes): CloudAdapter.get_adapter({'type': t.value}) + + def test_gcp_tenant_workflow(self): + body = { + 'name': 'gcp cloud_acc', + 'type': 'gcp_tenant', + 'config': { + 'credentials': { + "project_id": "hystax", + "type": "service_account", + "private_key_id": "redacted", + "private_key": "redacted", + }, + 'billing_data': { + 'dataset_name': 'billing_data', + 'table_name': 'gcp_billing_export_v1', + }, + } + } + code, parent_ca = self.create_cloud_account(self.org_id, body) + self.assertEqual(code, 201) + self.assertEqual(parent_ca['type'], body['type']) + self.assertDictEqual(parent_ca['config']['billing_data'], { + 'dataset_name': 'billing_data', + 'project_id': 'hystax', + 'table_name': 'gcp_billing_export_v1' + }) + patch('tools.cloud_adapter.clouds.gcp_tenant.GcpTenant' + '.get_children_configs', + return_value=[{ + 'name': 'child 1', + 'config': {'project_id': 'project_1'}, + 'type': 'gcp_cnr' + }]).start() + patch('tools.cloud_adapter.clouds.gcp.Gcp.validate_credentials', + return_value={'account_id': 'project_1', 'warnings': []} + ).start() + code, _ = self.client.observe_resources(self.org_id) + self.assertEqual(code, 204) + code, resp = self.client.cloud_account_list(self.org_id) + self.assertEqual(code, 200) + self.assertEqual(len(resp['cloud_accounts']), 2) + + for c in resp['cloud_accounts']: + if c['id'] != parent_ca['id']: + child_ca_id = c['id'] + self.assertEqual(c['type'], 'gcp_cnr') + self.assertDictEqual(c['config']['credentials'], { + "type": "service_account", + "private_key_id": "redacted", + "private_key": "redacted", + }) + ca_obj = self.get_cloud_account_object(child_ca_id) + conf = decode_config(ca_obj.config) + self.assertEqual(conf, {'project_id': 'project_1'}) diff --git a/tools/cloud_adapter/cloud.py b/tools/cloud_adapter/cloud.py index 84e0c0c9..2967f787 100644 --- a/tools/cloud_adapter/cloud.py +++ b/tools/cloud_adapter/cloud.py @@ -7,6 +7,7 @@ from tools.cloud_adapter.clouds.kubernetes import Kubernetes from tools.cloud_adapter.clouds.environment import Environment from tools.cloud_adapter.clouds.gcp import Gcp +from tools.cloud_adapter.clouds.gcp_tenant import GcpTenant from tools.cloud_adapter.clouds.nebius import Nebius from tools.cloud_adapter.clouds.databricks import Databricks @@ -19,6 +20,7 @@ 'alibaba_cnr': Alibaba, 'environment': Environment, 'gcp_cnr': Gcp, + 'gcp_tenant': GcpTenant, 'nebius': Nebius, 'databricks': Databricks } diff --git a/tools/cloud_adapter/clouds/gcp.py b/tools/cloud_adapter/clouds/gcp.py index 3f78746a..71ece615 100644 --- a/tools/cloud_adapter/clouds/gcp.py +++ b/tools/cloud_adapter/clouds/gcp.py @@ -1,7 +1,7 @@ from collections import defaultdict from functools import cached_property from concurrent.futures import ThreadPoolExecutor -import datetime +from datetime import datetime, timezone, timedelta import hashlib import logging @@ -27,6 +27,7 @@ # can be from 0 to 500 for gcp API MAX_RESULTS = 500 +BILLING_THRESHOLD = 3 # Retries logic composed of code from # google/cloud/bigquery/retry.py and @@ -189,7 +190,7 @@ def _extract_tags(self): @staticmethod def _gcp_date_to_timestamp(date): return int( - datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%f%z").timestamp() + datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%f%z").timestamp() ) @staticmethod @@ -769,8 +770,21 @@ def metrics_client(self): def _billing_table_full_name(self): return f"{self.billing_project_id}.{self.billing_dataset}.{self.billing_table}" + @staticmethod + def _get_billing_threshold_date(): + # billing threshold means datasets should be updated at least 3 days ago + return datetime.now(tz=timezone.utc).replace( + hour=0, minute=0, second=0, microsecond=0 + ) - timedelta(days=BILLING_THRESHOLD) + def _test_bigquery_connection(self): - query = f"select currency from `{self._billing_table_full_name()}` limit 1" + dt = self._get_billing_threshold_date() + query = f""" + SELECT currency + FROM `{self._billing_table_full_name()}` + WHERE TIMESTAMP_TRUNC(_PARTITIONTIME, DAY) >= TIMESTAMP("{dt}") + LIMIT 1 + """ query_job = self.bigquery_client.query(query, **DEFAULT_KWARGS) result = list(query_job.result())[0] if not result or dict(result).get("currency") != self._currency: @@ -844,8 +858,7 @@ def get_usage(self, start_date, end_date): credits, adjustment_info FROM `{table_name}` WHERE - usage_start_time >= TIMESTAMP("{start_date}") AND - usage_end_time <= TIMESTAMP("{end_date}") AND + TIMESTAMP_TRUNC(_PARTITIONTIME, DAY) = TIMESTAMP("{start_date}") AND project.id = "{self.project_id}" """ return self.bigquery_client.query( @@ -1182,18 +1195,21 @@ def _build_pricing_query(self, sku_desription_pattern: str) -> str: # GROUP BY sku.id) inr # ON prices.sku.id = inr.sku_id # AND prices.export_time = inr.export_time + dt = self._get_billing_threshold_date() inner_query = f""" - SELECT sku.id as sku_id, max(export_time) as export_time - FROM `{self._pricing_table_full_name()}` - WHERE service.id = '{COMPUTE_SERVICE_ID}' - AND sku.description LIKE '{sku_desription_pattern}' - GROUP BY sku.id""" + SELECT sku.id as sku_id, max(export_time) as export_time + FROM `{self._pricing_table_full_name()}` + WHERE service.id = '{COMPUTE_SERVICE_ID}' + AND sku.description LIKE '{sku_desription_pattern}' + AND TIMESTAMP_TRUNC(_PARTITIONTIME, DAY) >= TIMESTAMP("{dt}") + GROUP BY sku.id""" query = f""" - SELECT prices.list_price, prices.sku - FROM `{self._pricing_table_full_name()}` prices - INNER JOIN ({inner_query}) inr - ON prices.sku.id = inr.sku_id - AND prices.export_time = inr.export_time + SELECT prices.list_price, prices.sku + FROM `{self._pricing_table_full_name()}` prices + INNER JOIN ({inner_query}) inr + ON prices.sku.id = inr.sku_id + AND prices.export_time = inr.export_time + WHERE TIMESTAMP_TRUNC(_PARTITIONTIME, DAY) >= TIMESTAMP("{dt}") """ return query diff --git a/tools/cloud_adapter/clouds/gcp_tenant.py b/tools/cloud_adapter/clouds/gcp_tenant.py new file mode 100644 index 00000000..73befba4 --- /dev/null +++ b/tools/cloud_adapter/clouds/gcp_tenant.py @@ -0,0 +1,102 @@ +from functools import cached_property +from tools.cloud_adapter.clouds.gcp import Gcp, DEFAULT_KWARGS +from tools.cloud_adapter.enums import CloudTypes +from tools.cloud_adapter.utils import CloudParameter +from tools.cloud_adapter.exceptions import ( + InvalidParameterException, CloudConnectionError) +from google.cloud import bigquery +from google.api_core import exceptions as api_exceptions + + +class GcpTenant(Gcp): + BILLING_CREDS = [ + CloudParameter( + name="billing_data", + type=dict, + required=True, + dependencies=[ + CloudParameter(name="project_id", type=str, required=False), + CloudParameter(name="dataset_name", type=str, required=True), + CloudParameter(name="table_name", type=str, required=True), + ], + ), + CloudParameter( + name="pricing_data", + type=dict, + required=False, + dependencies=[ + CloudParameter(name="project_id", type=str, required=False), + CloudParameter(name="dataset_name", type=str, required=True), + CloudParameter(name="table_name", type=str, required=True), + ], + ), + CloudParameter(name="credentials", type=dict, required=True, protected=True), + + # Service parameters + CloudParameter(name='skipped_subscriptions', type=dict, required=False) + ] + + @classmethod + def configure_credentials(cls, config): + project_id = config['credentials'].pop('project_id', None) + if project_id: + for k in ['billing_data', 'pricing_data']: + dataset = config.get(k) + if dataset and not dataset.get('project_id'): + config[k]['project_id'] = project_id + return config + + @cached_property + def bigquery_client(self): + return bigquery.Client.from_service_account_info( + self.credentials, + project=self.billing_project_id, + ) + + def _test_bigquery_connection(self): + self._list_projects() + + def discovery_calls_map(self): + return {} + + def validate_credentials(self, org_id=None): + try: + self._validate_billing_config() + self._validate_billing_type() + self._test_bigquery_connection() + except api_exceptions.Forbidden as ex: + # remove new-lines, otherwise tornado will fail to write response + raise InvalidParameterException(str(ex).replace("\n", " ")) + except Exception as ex: + raise CloudConnectionError(str(ex)) + return {"account_id": self.config.get("client_id"), "warnings": []} + + def _list_projects(self): + dt = self._get_billing_threshold_date() + # find actual project name from latest dataset update + query = f""" + SELECT project.id, project.name, max(export_time) + FROM `{self._billing_table_full_name()}` + WHERE TIMESTAMP_TRUNC(_PARTITIONTIME, DAY) >= TIMESTAMP("{dt}") + GROUP BY project.id, project.name + """ + query_job = self.bigquery_client.query(query, **DEFAULT_KWARGS) + names_map = {} + for r in list(query_job.result()): + project_id, name, dt = r + if not project_id or ( + project_id in names_map and names_map[project_id][1] > dt + ): + continue + names_map[project_id] = (name, dt) + return {k: v[0] for k, v in names_map.items()} + + def get_children_configs(self): + projects = self._list_projects() + return [{ + 'name': project_name, + 'config': { + 'project_id': project_id + }, + 'type': CloudTypes.GCP_CNR.value + } for project_id, project_name in projects.items()] diff --git a/tools/cloud_adapter/enums.py b/tools/cloud_adapter/enums.py index 426171b0..34980292 100644 --- a/tools/cloud_adapter/enums.py +++ b/tools/cloud_adapter/enums.py @@ -8,6 +8,7 @@ class CloudTypes(Enum): AZURE_TENANT = 'azure_tenant' KUBERNETES_CNR = 'kubernetes_cnr' GCP_CNR = 'gcp_cnr' + GCP_TENANT = 'gcp_tenant' NEBIUS = 'nebius' ENVIRONMENT = 'environment' DATABRICKS = 'databricks' From c84413c154d41550ca950115b480ad035476f168 Mon Sep 17 00:00:00 2001 From: ek-hystax <33006768+ek-hystax@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:57:25 +0400 Subject: [PATCH 50/65] OS-8054. [UI] Add support for gcp_tenant --- ngui/server/api/restapi/client.ts | 1 + .../graphql/resolvers/restapi.generated.ts | 112 +++++++++++++++--- ngui/server/graphql/resolvers/restapi.ts | 3 + ngui/server/graphql/schemas/restapi.graphql | 50 ++++++-- .../GcpTenantCredentials.tsx | 64 ++++++++++ .../GcpTenantCredentials /index.ts | 4 + .../DataSourceCredentialFields/index.ts | 3 + .../DataSourceDetails/DataSourceDetails.tsx | 16 ++- .../ConnectCloudAccountForm.tsx | 55 ++++++++- .../FormElements/ConnectionFields.tsx | 8 +- .../DisconnectCloudAccountForm.tsx | 15 ++- .../FormElements/CredentialInputs.tsx | 25 +++- .../UpdateDataSourceCredentialsForm.tsx | 29 ++++- .../UpdateDataSourceCredentialsContainer.tsx | 13 +- .../api/restapi/queries/restapi.queries.ts | 14 +++ ngui/ui/src/hooks/useDataSources.ts | 11 +- ngui/ui/src/translations/en-US/app.json | 4 + ngui/ui/src/utils/constants.ts | 3 + 18 files changed, 384 insertions(+), 46 deletions(-) create mode 100644 ngui/ui/src/components/DataSourceCredentialFields/GcpTenantCredentials /GcpTenantCredentials.tsx create mode 100644 ngui/ui/src/components/DataSourceCredentialFields/GcpTenantCredentials /index.ts diff --git a/ngui/server/api/restapi/client.ts b/ngui/server/api/restapi/client.ts index 90695923..8be9ca08 100644 --- a/ngui/server/api/restapi/client.ts +++ b/ngui/server/api/restapi/client.ts @@ -95,6 +95,7 @@ class RestApiClient extends BaseClient { ...params.azureSubscriptionConfig, ...params.azureTenantConfig, ...params.gcpConfig, + ...params.gcpTenantConfig, ...params.alibabaConfig, ...params.nebiusConfig, ...params.databricksConfig, diff --git a/ngui/server/graphql/resolvers/restapi.generated.ts b/ngui/server/graphql/resolvers/restapi.generated.ts index ee8e569f..a09c107d 100644 --- a/ngui/server/graphql/resolvers/restapi.generated.ts +++ b/ngui/server/graphql/resolvers/restapi.generated.ts @@ -190,18 +190,18 @@ export type DataSourceDiscoveryInfos = { }; export type DataSourceInterface = { - account_id: Scalars['String']['output']; + account_id?: Maybe; details?: Maybe; - id: Scalars['String']['output']; - last_getting_metric_attempt_at: Scalars['Int']['output']; + id?: Maybe; + last_getting_metric_attempt_at?: Maybe; last_getting_metric_attempt_error?: Maybe; - last_getting_metrics_at: Scalars['Int']['output']; - last_import_at: Scalars['Int']['output']; - last_import_attempt_at: Scalars['Int']['output']; + last_getting_metrics_at?: Maybe; + last_import_at?: Maybe; + last_import_attempt_at?: Maybe; last_import_attempt_error?: Maybe; - name: Scalars['String']['output']; + name?: Maybe; parent_id?: Maybe; - type: DataSourceType; + type?: Maybe; }; export type DataSourceRequestParams = { @@ -216,6 +216,7 @@ export enum DataSourceType { Databricks = 'databricks', Environment = 'environment', GcpCnr = 'gcp_cnr', + GcpTenant = 'gcp_tenant', KubernetesCnr = 'kubernetes_cnr', Nebius = 'nebius' } @@ -320,6 +321,40 @@ export type GcpDataSource = DataSourceInterface & { type: DataSourceType; }; +export type GcpTenantBillingDataConfig = { + __typename?: 'GcpTenantBillingDataConfig'; + dataset_name?: Maybe; + project_id?: Maybe; + table_name?: Maybe; +}; + +export type GcpTenantConfig = { + __typename?: 'GcpTenantConfig'; + billing_data?: Maybe; +}; + +export type GcpTenantConfigInput = { + billing_data: GcpBillingDataConfigInput; + credentials: Scalars['JSONObject']['input']; +}; + +export type GcpTenantDataSource = DataSourceInterface & { + __typename?: 'GcpTenantDataSource'; + account_id?: Maybe; + config?: Maybe; + details?: Maybe; + id: Scalars['String']['output']; + last_getting_metric_attempt_at: Scalars['Int']['output']; + last_getting_metric_attempt_error?: Maybe; + last_getting_metrics_at: Scalars['Int']['output']; + last_import_at: Scalars['Int']['output']; + last_import_attempt_at: Scalars['Int']['output']; + last_import_attempt_error?: Maybe; + name: Scalars['String']['output']; + parent_id?: Maybe; + type: DataSourceType; +}; + export type Invitation = { __typename?: 'Invitation'; id: Scalars['String']['output']; @@ -577,6 +612,7 @@ export type UpdateDataSourceInput = { azureTenantConfig?: InputMaybe; databricksConfig?: InputMaybe; gcpConfig?: InputMaybe; + gcpTenantConfig?: InputMaybe; k8sConfig?: InputMaybe; lastImportAt?: InputMaybe; lastImportModifiedAt?: InputMaybe; @@ -674,7 +710,7 @@ export type DirectiveResolverFn> = { - DataSourceInterface: ( AlibabaDataSource ) | ( AwsDataSource ) | ( AzureSubscriptionDataSource ) | ( AzureTenantDataSource ) | ( DatabricksDataSource ) | ( EnvironmentDataSource ) | ( GcpDataSource ) | ( K8sDataSource ) | ( NebiusDataSource ); + DataSourceInterface: ( AlibabaDataSource ) | ( AwsDataSource ) | ( AzureSubscriptionDataSource ) | ( AzureTenantDataSource ) | ( DatabricksDataSource ) | ( EnvironmentDataSource ) | ( GcpDataSource ) | ( GcpTenantDataSource ) | ( K8sDataSource ) | ( NebiusDataSource ); }; /** Mapping between all available schema types and the resolvers types */ @@ -711,6 +747,10 @@ export type ResolversTypes = { GcpConfig: ResolverTypeWrapper; GcpConfigInput: GcpConfigInput; GcpDataSource: ResolverTypeWrapper; + GcpTenantBillingDataConfig: ResolverTypeWrapper; + GcpTenantConfig: ResolverTypeWrapper; + GcpTenantConfigInput: GcpTenantConfigInput; + GcpTenantDataSource: ResolverTypeWrapper; ID: ResolverTypeWrapper; Int: ResolverTypeWrapper; Invitation: ResolverTypeWrapper; @@ -769,6 +809,10 @@ export type ResolversParentTypes = { GcpConfig: GcpConfig; GcpConfigInput: GcpConfigInput; GcpDataSource: GcpDataSource; + GcpTenantBillingDataConfig: GcpTenantBillingDataConfig; + GcpTenantConfig: GcpTenantConfig; + GcpTenantConfigInput: GcpTenantConfigInput; + GcpTenantDataSource: GcpTenantDataSource; ID: Scalars['ID']['output']; Int: Scalars['Int']['output']; Invitation: Invitation; @@ -917,19 +961,19 @@ export type DataSourceDiscoveryInfosResolvers = { - __resolveType: TypeResolveFn<'AlibabaDataSource' | 'AwsDataSource' | 'AzureSubscriptionDataSource' | 'AzureTenantDataSource' | 'DatabricksDataSource' | 'EnvironmentDataSource' | 'GcpDataSource' | 'K8sDataSource' | 'NebiusDataSource', ParentType, ContextType>; - account_id?: Resolver; + __resolveType: TypeResolveFn<'AlibabaDataSource' | 'AwsDataSource' | 'AzureSubscriptionDataSource' | 'AzureTenantDataSource' | 'DatabricksDataSource' | 'EnvironmentDataSource' | 'GcpDataSource' | 'GcpTenantDataSource' | 'K8sDataSource' | 'NebiusDataSource', ParentType, ContextType>; + account_id?: Resolver, ParentType, ContextType>; details?: Resolver, ParentType, ContextType>; - id?: Resolver; - last_getting_metric_attempt_at?: Resolver; + id?: Resolver, ParentType, ContextType>; + last_getting_metric_attempt_at?: Resolver, ParentType, ContextType>; last_getting_metric_attempt_error?: Resolver, ParentType, ContextType>; - last_getting_metrics_at?: Resolver; - last_import_at?: Resolver; - last_import_attempt_at?: Resolver; + last_getting_metrics_at?: Resolver, ParentType, ContextType>; + last_import_at?: Resolver, ParentType, ContextType>; + last_import_attempt_at?: Resolver, ParentType, ContextType>; last_import_attempt_error?: Resolver, ParentType, ContextType>; - name?: Resolver; + name?: Resolver, ParentType, ContextType>; parent_id?: Resolver, ParentType, ContextType>; - type?: Resolver; + type?: Resolver, ParentType, ContextType>; }; export type DatabricksConfigResolvers = { @@ -1016,6 +1060,35 @@ export type GcpDataSourceResolvers; }; +export type GcpTenantBillingDataConfigResolvers = { + dataset_name?: Resolver, ParentType, ContextType>; + project_id?: Resolver, ParentType, ContextType>; + table_name?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type GcpTenantConfigResolvers = { + billing_data?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type GcpTenantDataSourceResolvers = { + account_id?: Resolver, ParentType, ContextType>; + config?: Resolver, ParentType, ContextType>; + details?: Resolver, ParentType, ContextType>; + id?: Resolver; + last_getting_metric_attempt_at?: Resolver; + last_getting_metric_attempt_error?: Resolver, ParentType, ContextType>; + last_getting_metrics_at?: Resolver; + last_import_at?: Resolver; + last_import_attempt_at?: Resolver; + last_import_attempt_error?: Resolver, ParentType, ContextType>; + name?: Resolver; + parent_id?: Resolver, ParentType, ContextType>; + type?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type InvitationResolvers = { id?: Resolver; invite_assignments?: Resolver>, ParentType, ContextType>; @@ -1156,6 +1229,9 @@ export type Resolvers = { GcpBillingDataConfig?: GcpBillingDataConfigResolvers; GcpConfig?: GcpConfigResolvers; GcpDataSource?: GcpDataSourceResolvers; + GcpTenantBillingDataConfig?: GcpTenantBillingDataConfigResolvers; + GcpTenantConfig?: GcpTenantConfigResolvers; + GcpTenantDataSource?: GcpTenantDataSourceResolvers; Invitation?: InvitationResolvers; InvitationAssignment?: InvitationAssignmentResolvers; JSONObject?: GraphQLScalarType; diff --git a/ngui/server/graphql/resolvers/restapi.ts b/ngui/server/graphql/resolvers/restapi.ts index 19855538..0e0423a1 100644 --- a/ngui/server/graphql/resolvers/restapi.ts +++ b/ngui/server/graphql/resolvers/restapi.ts @@ -19,6 +19,9 @@ const resolvers: Resolvers = { case "gcp_cnr": { return "GcpDataSource"; } + case "gcp_tenant": { + return "GcpTenantDataSource"; + } case "alibaba_cnr": { return "AlibabaDataSource"; } diff --git a/ngui/server/graphql/schemas/restapi.graphql b/ngui/server/graphql/schemas/restapi.graphql index c5f73f93..4ebd4d7a 100644 --- a/ngui/server/graphql/schemas/restapi.graphql +++ b/ngui/server/graphql/schemas/restapi.graphql @@ -6,6 +6,7 @@ enum DataSourceType { azure_tenant azure_cnr gcp_cnr + gcp_tenant alibaba_cnr nebius databricks @@ -35,16 +36,16 @@ type DataSourceDetails { } interface DataSourceInterface { - id: String! - name: String! - type: DataSourceType! + id: String + name: String + type: DataSourceType parent_id: String - account_id: String! - last_import_at: Int! - last_import_attempt_at: Int! + account_id: String + last_import_at: Int + last_import_attempt_at: Int last_import_attempt_error: String - last_getting_metrics_at: Int! - last_getting_metric_attempt_at: Int! + last_getting_metrics_at: Int + last_getting_metric_attempt_at: Int last_getting_metric_attempt_error: String details: DataSourceDetails } @@ -151,6 +152,33 @@ type GcpDataSource implements DataSourceInterface { config: GcpConfig } +# GCP tenant data source +type GcpTenantBillingDataConfig { + dataset_name: String + table_name: String + project_id: String +} + +type GcpTenantConfig { + billing_data: GcpTenantBillingDataConfig +} + +type GcpTenantDataSource implements DataSourceInterface { + id: String! + name: String! + type: DataSourceType! + parent_id: String + account_id: String + last_import_at: Int! + last_import_attempt_at: Int! + last_import_attempt_error: String + last_getting_metrics_at: Int! + last_getting_metric_attempt_at: Int! + last_getting_metric_attempt_error: String + details: DataSourceDetails + config: GcpTenantConfig +} + # Alibaba data source type AlibabaConfig { access_key_id: String @@ -309,6 +337,11 @@ input GcpConfigInput { credentials: JSONObject! } +input GcpTenantConfigInput { + billing_data: GcpBillingDataConfigInput! + credentials: JSONObject! +} + input AlibabaConfigInput { access_key_id: String! secret_access_key: String! @@ -359,6 +392,7 @@ input UpdateDataSourceInput { azureSubscriptionConfig: AzureSubscriptionConfigInput azureTenantConfig: AzureTenantConfigInput gcpConfig: GcpConfigInput + gcpTenantConfig: GcpTenantConfigInput alibabaConfig: AlibabaConfigInput nebiusConfig: NebiusConfigInput databricksConfig: DatabricksConfigInput diff --git a/ngui/ui/src/components/DataSourceCredentialFields/GcpTenantCredentials /GcpTenantCredentials.tsx b/ngui/ui/src/components/DataSourceCredentialFields/GcpTenantCredentials /GcpTenantCredentials.tsx new file mode 100644 index 00000000..03a4b023 --- /dev/null +++ b/ngui/ui/src/components/DataSourceCredentialFields/GcpTenantCredentials /GcpTenantCredentials.tsx @@ -0,0 +1,64 @@ +import { FormControl } from "@mui/material"; +import { FormattedMessage } from "react-intl"; +import { DropzoneArea } from "components/Dropzone"; +import { TextInput } from "components/forms/common/fields"; +import QuestionMark from "components/QuestionMark"; +import { ObjectValues } from "utils/types"; + +export const FIELD_NAMES = Object.freeze({ + CREDENTIALS: "credentials", + BILLING_DATA_DATASET: "billingDataDatasetName", + BILLING_DATA_TABLE: "billingDataTableName" +}); + +type FIELD_NAME = ObjectValues; + +type GcpTenantCredentialsProps = { + hidden?: FIELD_NAME[]; +}; + +const GcpTenantCredentials = ({ hidden = [] }: GcpTenantCredentialsProps) => { + const isHidden = (fieldName: FIELD_NAME) => hidden.includes(fieldName); + + return ( + <> + + + + {!isHidden(FIELD_NAMES.BILLING_DATA_DATASET) && ( + {chunks} + }} + dataTestId="qmark_billing_data_dataset_name" + /> + ) + }} + label={} + autoComplete="off" + /> + )} + {!isHidden(FIELD_NAMES.BILLING_DATA_DATASET) && ( + + }} + label={} + autoComplete="off" + /> + )} + + ); +}; + +export default GcpTenantCredentials; diff --git a/ngui/ui/src/components/DataSourceCredentialFields/GcpTenantCredentials /index.ts b/ngui/ui/src/components/DataSourceCredentialFields/GcpTenantCredentials /index.ts new file mode 100644 index 00000000..70cbe42b --- /dev/null +++ b/ngui/ui/src/components/DataSourceCredentialFields/GcpTenantCredentials /index.ts @@ -0,0 +1,4 @@ +import GcpTenantCredentials, { FIELD_NAMES } from "./GcpTenantCredentials"; + +export { FIELD_NAMES }; +export default GcpTenantCredentials; diff --git a/ngui/ui/src/components/DataSourceCredentialFields/index.ts b/ngui/ui/src/components/DataSourceCredentialFields/index.ts index b73e8796..ae90d4fa 100644 --- a/ngui/ui/src/components/DataSourceCredentialFields/index.ts +++ b/ngui/ui/src/components/DataSourceCredentialFields/index.ts @@ -10,6 +10,7 @@ import AzureSubscriptionCredentials, { import AzureTenantCredentials, { FIELD_NAMES as AZURE_TENANT_CREDENTIALS_FIELD_NAMES } from "./AzureTenantCredentials"; import DatabricksCredentials, { FIELD_NAMES as DATABRICKS_CREDENTIALS_FIELD_NAMES } from "./DatabricksCredentials"; import GcpCredentials, { FIELD_NAMES as GCP_CREDENTIALS_FIELD_NAMES } from "./GcpCredentials"; +import GcpTenantCredentials, { FIELD_NAMES as GCP_TENANT_CREDENTIALS_FIELD_NAMES } from "./GcpTenantCredentials "; import KubernetesCredentials, { FIELD_NAMES as KUBERNETES_CREDENTIALS_FIELD_NAMES } from "./KubernetesCredentials"; import NebiusCredentials from "./NebiusCredentials"; @@ -32,6 +33,8 @@ export { KUBERNETES_CREDENTIALS_FIELD_NAMES, GcpCredentials, GCP_CREDENTIALS_FIELD_NAMES, + GcpTenantCredentials, + GCP_TENANT_CREDENTIALS_FIELD_NAMES, AlibabaCredentials, ALIBABA_CREDENTIALS_FIELD_NAMES, NebiusCredentials, diff --git a/ngui/ui/src/components/DataSourceDetails/DataSourceDetails.tsx b/ngui/ui/src/components/DataSourceDetails/DataSourceDetails.tsx index e4de0837..04beeb2f 100644 --- a/ngui/ui/src/components/DataSourceDetails/DataSourceDetails.tsx +++ b/ngui/ui/src/components/DataSourceDetails/DataSourceDetails.tsx @@ -1,7 +1,17 @@ import { Stack } from "@mui/material"; import { FormattedMessage } from "react-intl"; import SummaryList from "components/SummaryList"; -import { ALIBABA_CNR, AWS_CNR, AZURE_CNR, AZURE_TENANT, GCP_CNR, KUBERNETES_CNR, NEBIUS, DATABRICKS } from "utils/constants"; +import { + ALIBABA_CNR, + AWS_CNR, + AZURE_CNR, + AZURE_TENANT, + GCP_CNR, + KUBERNETES_CNR, + NEBIUS, + DATABRICKS, + GCP_TENANT +} from "utils/constants"; import { SPACING_2 } from "utils/layouts"; import { ChildrenList } from "./ChildrenList"; import { K8sHelp } from "./Help"; @@ -21,6 +31,7 @@ const DataSourceDetails = ({ id, accountId, parentId, type, config = {} }) => { [AZURE_CNR]: AzureProperties, [AZURE_TENANT]: AzureProperties, [GCP_CNR]: GcpProperties, + [GCP_TENANT]: GcpProperties, [ALIBABA_CNR]: AlibabaProperties, [KUBERNETES_CNR]: K8sProperties, [NEBIUS]: NebiusProperties, @@ -32,7 +43,8 @@ const DataSourceDetails = ({ id, accountId, parentId, type, config = {} }) => { }[type]; const childrenList = { - [AZURE_TENANT]: ChildrenList + [AZURE_TENANT]: ChildrenList, + [GCP_TENANT]: ChildrenList }[type]; return ( diff --git a/ngui/ui/src/components/forms/ConnectCloudAccountForm/ConnectCloudAccountForm.tsx b/ngui/ui/src/components/forms/ConnectCloudAccountForm/ConnectCloudAccountForm.tsx index a361ee85..8935697f 100644 --- a/ngui/ui/src/components/forms/ConnectCloudAccountForm/ConnectCloudAccountForm.tsx +++ b/ngui/ui/src/components/forms/ConnectCloudAccountForm/ConnectCloudAccountForm.tsx @@ -13,6 +13,7 @@ import { AZURE_TENANT_CREDENTIALS_FIELD_NAMES, AZURE_SUBSCRIPTION_CREDENTIALS_FIELD_NAMES, GCP_CREDENTIALS_FIELD_NAMES, + GCP_TENANT_CREDENTIALS_FIELD_NAMES, KUBERNETES_CREDENTIALS_FIELD_NAMES, DATABRICKS_CREDENTIALS_FIELD_NAMES, AWS_ROOT_CREDENTIALS_FIELD_NAMES, @@ -65,7 +66,9 @@ import { NEBIUS, DATABRICKS, DATABRICKS_ACCOUNT, - OPTSCALE_MODE + OPTSCALE_MODE, + GCP_TENANT_ACCOUNT, + GCP_TENANT } from "utils/constants"; import { readFileAsText } from "utils/files"; import { SPACING_2 } from "utils/layouts"; @@ -85,6 +88,7 @@ const getCloudType = (connectionType) => [AZURE_TENANT_ACCOUNT]: AZURE_TENANT, [ALIBABA_ACCOUNT]: ALIBABA_CNR, [GCP_ACCOUNT]: GCP_CNR, + [GCP_TENANT_ACCOUNT]: GCP_TENANT, [NEBIUS_ACCOUNT]: NEBIUS, [DATABRICKS_ACCOUNT]: DATABRICKS, [KUBERNETES]: KUBERNETES_CNR @@ -189,6 +193,22 @@ const getGoogleParameters = async (formData) => { }; }; +const getGoogleTenantParameters = async (formData) => { + const credentials = await readFileAsText(formData[GCP_TENANT_CREDENTIALS_FIELD_NAMES.CREDENTIALS]); + + return { + name: formData[DATA_SOURCE_NAME_FIELD_NAME], + type: GCP_TENANT, + config: { + credentials: JSON.parse(credentials), + billing_data: { + dataset_name: formData[GCP_TENANT_CREDENTIALS_FIELD_NAMES.BILLING_DATA_DATASET], + table_name: formData[GCP_TENANT_CREDENTIALS_FIELD_NAMES.BILLING_DATA_TABLE] + } + } + }; +}; + const getNebiusParameters = (formData) => ({ name: formData[DATA_SOURCE_NAME_FIELD_NAME], type: NEBIUS, @@ -227,7 +247,7 @@ const renderConnectionTypeDescription = (settings) => )); -const renderConnectionTypeInfoMessage = ({ connectionType }) => +const renderConnectionTypeInfoMessage = (connectionType) => ({ [AWS_ROOT_ACCOUNT]: renderConnectionTypeDescription([ { @@ -387,10 +407,29 @@ const renderConnectionTypeInfoMessage = ({ connectionType }) => p: (chunks) =>

{chunks}

} } + ]), + [GCP_TENANT_ACCOUNT]: renderConnectionTypeDescription([ + { + key: "createGCPTenantDocumentationReference1", + messageId: "createGCPTenantDocumentationReference1" + }, + { + key: "createGCPTenantDocumentationReference2", + messageId: "createGCPTenantDocumentationReference2", + values: { + link: (chunks) => ( + + {chunks} + + ), + strong: (chunks) => {chunks}, + p: (chunks) =>

{chunks}

+ } + } ]) })[connectionType]; -const ConnectCloudAccountForm = ({ onSubmit, onCancel, isLoading, showCancel = true }) => { +const ConnectCloudAccountForm = ({ onSubmit, onCancel, isLoading = false, showCancel = true }) => { const methods = useForm(); const ref = useRef(); @@ -452,6 +491,13 @@ const ConnectCloudAccountForm = ({ onSubmit, onCancel, isLoading, showCancel = t dataTestId: "btn_gcp_account", action: () => defaultTileAction(GCP_ACCOUNT, GCP_CNR) }, + { + id: GCP_TENANT_ACCOUNT, + icon: GcpLogoIcon, + messageId: GCP_TENANT_ACCOUNT, + dataTestId: "btn_gcp_tenant_account", + action: () => defaultTileAction(GCP_TENANT_ACCOUNT, GCP_TENANT) + }, { id: ALIBABA_ACCOUNT, icon: AlibabaLogoIcon, @@ -518,7 +564,7 @@ const ConnectCloudAccountForm = ({ onSubmit, onCancel, isLoading, showCancel = t ))}
- {renderConnectionTypeInfoMessage({ connectionType })} + {renderConnectionTypeInfoMessage(connectionType)} { return ; case GCP_ACCOUNT: return ; + case GCP_TENANT_ACCOUNT: + return ; case NEBIUS_ACCOUNT: return ; case DATABRICKS_ACCOUNT: diff --git a/ngui/ui/src/components/forms/DisconnectCloudAccountForm/DisconnectCloudAccountForm.tsx b/ngui/ui/src/components/forms/DisconnectCloudAccountForm/DisconnectCloudAccountForm.tsx index 4348883b..a0d7536c 100644 --- a/ngui/ui/src/components/forms/DisconnectCloudAccountForm/DisconnectCloudAccountForm.tsx +++ b/ngui/ui/src/components/forms/DisconnectCloudAccountForm/DisconnectCloudAccountForm.tsx @@ -1,8 +1,10 @@ +import { Box } from "@mui/material"; import { FormProvider, useForm } from "react-hook-form"; import DeleteEntity from "components/DeleteEntity"; import PageContentDescription from "components/PageContentDescription"; import { useDataSources } from "hooks/useDataSources"; -import { AZURE_TENANT } from "utils/constants"; +import { AZURE_TENANT, GCP_TENANT } from "utils/constants"; +import { SPACING_1 } from "utils/layouts"; import Survey from "./FormElements/Survey"; import { DisconnectCloudAccountFormProps, FormValues } from "./types"; import { getDefaultValues } from "./utils"; @@ -17,6 +19,7 @@ const DisconnectCloudAccountForm = ({ }: DisconnectCloudAccountFormProps) => { const { disconnectQuestionId } = useDataSources(type); const isAzureTenant = type === AZURE_TENANT; + const isGcpTenant = type === GCP_TENANT; const methods = useForm({ defaultValues: getDefaultValues() }); const { handleSubmit } = methods; @@ -24,8 +27,8 @@ const DisconnectCloudAccountForm = ({ return ( - {(parentId || isAzureTenant) && ( - <> + {(parentId || isAzureTenant || isGcpTenant) && ( + {parentId && ( )} - {isAzureTenant && ( + {isAzureTenant || isGcpTenant ? ( - )} - + ) : null} + )} { ]} /> ); + case GCP_TENANT: + return ( +