diff --git a/README.md b/README.md index 10eac9f39..49ec53865 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ _The current installation process does not work on Ubuntu 22.04_ Run the following commands: ``` -sudo apt update; sudo apt install python3-pip sshpass git python3-virtualenv python3.9 +sudo apt update; sudo apt install python3-pip sshpass git python3.9-venv python3.9-dev python3.9 -y ``` #### Pulling optscale-deploy scripts @@ -125,13 +125,11 @@ ansible-playbook -e "ansible_ssh_user=" -k -K -i "," ansible/k ``` where `` - actual username; `` - host ip address, -ip address should be private address of the machine, you can check it with +ip address should be private address of the machine, you can check it with the command `ip a`. -``` -ip a -``` +**Note:** do not use `127.0.0.1` or `localhost` as the hostname. Instead, prefer providing the server's hostname (check with the command `hostname`) and make sure it is resolveable from host that the Ansible Playbooks ran from (if needed, add to the ``/etc/hosts`` files). -If your deployment server is the service-host server, add `"ansible_connection=local"` to the ansible command. +If your deployment server is the service-host server, add `-e "ansible_connection=local"` to the ansible command. #### Creating user overlay diff --git a/auth/auth_server/handlers/v2/signin.py b/auth/auth_server/handlers/v2/signin.py index 2b4bf943b..bd8fb0513 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/auth/auth_server/handlers/v2/users.py b/auth/auth_server/handlers/v2/users.py index 898df5337..304f4b125 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 diff --git a/bumiworker/bumiworker/modules/archive/inactive_users.py b/bumiworker/bumiworker/modules/archive/inactive_users.py index 81cbe4951..ad5fde9d8 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' ] diff --git a/bumiworker/bumiworker/modules/recommendations/abandoned_images.py b/bumiworker/bumiworker/modules/recommendations/abandoned_images.py index a0f0eb25d..fdf27108a 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/bumiworker/bumiworker/modules/recommendations/inactive_users.py b/bumiworker/bumiworker/modules/recommendations/inactive_users.py index ab93231c2..cb10d2481 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/bumiworker/bumiworker/modules/recommendations/insecure_security_groups.py b/bumiworker/bumiworker/modules/recommendations/insecure_security_groups.py index 330d111dd..b1a367b1a 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/bumiworker/bumiworker/modules/recommendations/s3_public_buckets.py b/bumiworker/bumiworker/modules/recommendations/s3_public_buckets.py index 24e59b022..41a133ab2 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/bumiworker/bumiworker/modules/service/saving_spike_notification.py b/bumiworker/bumiworker/modules/service/saving_spike_notification.py index 9fe4a33ef..5e82770ab 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, diff --git a/diworker/diworker/importers/gcp.py b/diworker/diworker/importers/gcp.py index a65271682..7c7b0f3cc 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/docker_images/cleanmongodb/clean-mongo-db.py b/docker_images/cleanmongodb/clean-mongo-db.py index ee3bc053f..9a0056277 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,14 +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): - if not token: - self.update_cleaned_at(organization_id=org_id) - return + 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, @@ -342,6 +350,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) diff --git a/docker_images/herald_executor/worker.py b/docker_images/herald_executor/worker.py index 4dbc49bfd..f94f378ae 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,16 +243,14 @@ 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') 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']) @@ -272,7 +286,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 +424,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 +440,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 +561,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 +595,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 +604,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 +634,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 +654,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 +684,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 +706,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 +716,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 b1634bdff..ebe4ef7e7 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/jira_ui/server/package-lock.json b/jira_ui/server/package-lock.json index c893df20d..9f201b04d 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 9eabe5d97..2895c3d1a 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 08da59925..5199b788b 100644 --- a/jira_ui/ui/package-lock.json +++ b/jira_ui/ui/package-lock.json @@ -20,15 +20,15 @@ "@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", "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", @@ -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": { @@ -7989,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", @@ -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", @@ -9895,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", @@ -9953,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", @@ -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", @@ -15552,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", @@ -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", @@ -16858,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", @@ -16869,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 544fcaca1..2e15ffde1 100644 --- a/jira_ui/ui/package.json +++ b/jira_ui/ui/package.json @@ -16,15 +16,15 @@ "@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", "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/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 000000000..4c7959eae --- /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 c094e134f..77256f1ef 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 206c266df..b42b728c0 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 46749202b..7fceabc5e 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 889109102..329d228ea 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 3763d168c..e8d4026b5 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 a8071a1d5..1f6f6550c 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 fe824abb5..8648e0c2d 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 51f57ddde..100b2697f 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 06fafe6ab..86f001eaf 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 030e15b05..efe5b685a 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 e6a0f46d1..c5529465f 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/.env.sample b/ngui/server/.env.sample index 4673cc406..aef38854d 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 000000000..75c52a678 --- /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 7ba68b386..c5397e53f 100644 --- a/ngui/server/api/restapi/client.ts +++ b/ngui/server/api/restapi/client.ts @@ -1,14 +1,45 @@ import BaseClient from "../baseClient.js"; import { DataSourceRequestParams, - UpdateDataSourceInput, + MutationUpdateEmployeeEmailsArgs, + 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 @@ -20,7 +51,38 @@ 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.gcpTenantConfig, + ...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, { @@ -34,6 +96,7 @@ class RestClient extends BaseClient { ...params.azureSubscriptionConfig, ...params.azureTenantConfig, ...params.gcpConfig, + ...params.gcpTenantConfig, ...params.alibabaConfig, ...params.nebiusConfig, ...params.databricksConfig, @@ -44,6 +107,188 @@ 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; + } + + 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 96d538527..ca560a414 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 31f2a9096..38b8f81b6 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 000000000..61f3f2ce4 --- /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 000000000..dd289df75 --- /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 652934c23..1f8da94a3 100644 --- a/ngui/server/graphql/resolvers/restapi.generated.ts +++ b/ngui/server/graphql/resolvers/restapi.generated.ts @@ -152,12 +152,27 @@ export type AzureTenantDataSource = DataSourceInterface & { type: DataSourceType; }; +export type CreateDataSourceInput = { + alibabaConfig?: InputMaybe; + awsLinkedConfig?: InputMaybe; + awsRootConfig?: InputMaybe; + azureSubscriptionConfig?: InputMaybe; + azureTenantConfig?: InputMaybe; + databricksConfig?: InputMaybe; + gcpConfig?: InputMaybe; + gcpTenantConfig?: 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']; }; @@ -176,18 +191,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 = { @@ -202,6 +217,7 @@ export enum DataSourceType { Databricks = 'databricks', Environment = 'environment', GcpCnr = 'gcp_cnr', + GcpTenant = 'gcp_tenant', KubernetesCnr = 'kubernetes_cnr', Nebius = 'nebius' } @@ -235,6 +251,22 @@ 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']; + 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']; @@ -260,6 +292,7 @@ export type GcpBillingDataConfig = { export type GcpBillingDataConfigInput = { dataset_name: Scalars['String']['input']; + project_id?: InputMaybe; table_name: Scalars['String']['input']; }; @@ -290,6 +323,58 @@ 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']; + 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_name: Scalars['String']['output']; + scope_type: Scalars['String']['output']; +}; + export type K8CostModelConfig = { __typename?: 'K8CostModelConfig'; cpu_hourly_cost: Scalars['Float']['output']; @@ -303,6 +388,7 @@ export type K8sConfig = { }; export type K8sConfigInput = { + cost_model?: InputMaybe; password: Scalars['String']['input']; user: Scalars['String']['input']; }; @@ -326,7 +412,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']; }; @@ -335,6 +453,48 @@ 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 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; @@ -373,9 +533,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']; }; @@ -384,6 +578,36 @@ export type QueryDataSourceArgs = { requestParams?: InputMaybe; }; + +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; @@ -392,6 +616,7 @@ export type UpdateDataSourceInput = { azureTenantConfig?: InputMaybe; databricksConfig?: InputMaybe; gcpConfig?: InputMaybe; + gcpTenantConfig?: InputMaybe; k8sConfig?: InputMaybe; lastImportAt?: InputMaybe; lastImportModifiedAt?: InputMaybe; @@ -399,6 +624,26 @@ 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 UpdateOrganizationInput = { + currency?: InputMaybe; + name?: InputMaybe; +}; + export type ResolverTypeWrapper = Promise | T; @@ -469,7 +714,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 */ @@ -488,6 +733,7 @@ export type ResolversTypes = { AzureTenantConfigInput: AzureTenantConfigInput; AzureTenantDataSource: ResolverTypeWrapper; Boolean: ResolverTypeWrapper; + CreateDataSourceInput: CreateDataSourceInput; DataSourceDetails: ResolverTypeWrapper; DataSourceDiscoveryInfos: ResolverTypeWrapper; DataSourceInterface: ResolverTypeWrapper['DataSourceInterface']>; @@ -496,6 +742,8 @@ export type ResolversTypes = { DatabricksConfig: ResolverTypeWrapper; DatabricksConfigInput: DatabricksConfigInput; DatabricksDataSource: ResolverTypeWrapper; + Employee: ResolverTypeWrapper; + EmployeeEmail: ResolverTypeWrapper; EnvironmentDataSource: ResolverTypeWrapper; Float: ResolverTypeWrapper; GcpBillingDataConfig: ResolverTypeWrapper; @@ -503,8 +751,14 @@ export type ResolversTypes = { GcpConfig: ResolverTypeWrapper; GcpConfigInput: GcpConfigInput; GcpDataSource: ResolverTypeWrapper; + GcpTenantBillingDataConfig: ResolverTypeWrapper; + GcpTenantConfig: ResolverTypeWrapper; + GcpTenantConfigInput: GcpTenantConfigInput; + GcpTenantDataSource: ResolverTypeWrapper; ID: ResolverTypeWrapper; Int: ResolverTypeWrapper; + Invitation: ResolverTypeWrapper; + InvitationAssignment: ResolverTypeWrapper; JSONObject: ResolverTypeWrapper; K8CostModelConfig: ResolverTypeWrapper; K8sConfig: ResolverTypeWrapper; @@ -514,9 +768,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 */ @@ -535,6 +796,7 @@ export type ResolversParentTypes = { AzureTenantConfigInput: AzureTenantConfigInput; AzureTenantDataSource: AzureTenantDataSource; Boolean: Scalars['Boolean']['output']; + CreateDataSourceInput: CreateDataSourceInput; DataSourceDetails: DataSourceDetails; DataSourceDiscoveryInfos: DataSourceDiscoveryInfos; DataSourceInterface: ResolversInterfaceTypes['DataSourceInterface']; @@ -542,6 +804,8 @@ export type ResolversParentTypes = { DatabricksConfig: DatabricksConfig; DatabricksConfigInput: DatabricksConfigInput; DatabricksDataSource: DatabricksDataSource; + Employee: Employee; + EmployeeEmail: EmployeeEmail; EnvironmentDataSource: EnvironmentDataSource; Float: Scalars['Float']['output']; GcpBillingDataConfig: GcpBillingDataConfig; @@ -549,8 +813,14 @@ 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; + InvitationAssignment: InvitationAssignment; JSONObject: Scalars['JSONObject']['output']; K8CostModelConfig: K8CostModelConfig; K8sConfig: K8sConfig; @@ -560,9 +830,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 = { @@ -669,7 +945,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; }; @@ -689,19 +965,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 = { @@ -727,6 +1003,22 @@ 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; + employee_id?: Resolver; + enabled?: Resolver; + id?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type EnvironmentDataSourceResolvers = { account_id?: Resolver; details?: Resolver, ParentType, ContextType>; @@ -772,6 +1064,53 @@ 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>; + organization?: Resolver; + owner_email?: Resolver; + owner_name?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type InvitationAssignmentResolvers = { + id?: Resolver; + purpose?: Resolver; + scope_id?: Resolver; + scope_name?: Resolver; + scope_type?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export interface JsonObjectScalarConfig extends GraphQLScalarTypeConfig { name: 'JSONObject'; } @@ -806,7 +1145,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 = { @@ -836,8 +1186,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 = { @@ -854,10 +1228,17 @@ export type Resolvers = { DataSourceInterface?: DataSourceInterfaceResolvers; DatabricksConfig?: DatabricksConfigResolvers; DatabricksDataSource?: DatabricksDataSourceResolvers; + Employee?: EmployeeResolvers; + EmployeeEmail?: EmployeeEmailResolvers; EnvironmentDataSource?: EnvironmentDataSourceResolvers; GcpBillingDataConfig?: GcpBillingDataConfigResolvers; GcpConfig?: GcpConfigResolvers; GcpDataSource?: GcpDataSourceResolvers; + GcpTenantBillingDataConfig?: GcpTenantBillingDataConfigResolvers; + GcpTenantConfig?: GcpTenantConfigResolvers; + GcpTenantDataSource?: GcpTenantDataSourceResolvers; + Invitation?: InvitationResolvers; + InvitationAssignment?: InvitationAssignmentResolvers; JSONObject?: GraphQLScalarType; K8CostModelConfig?: K8CostModelConfigResolvers; K8sConfig?: K8sConfigResolvers; @@ -865,6 +1246,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 3cdcd89d4..0e0423a13 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"; } @@ -44,11 +47,109 @@ const resolvers: Resolvers = { dataSource: async (_, { dataSourceId, requestParams }, { dataSources }) => { return dataSources.restapi.getDataSource(dataSourceId, requestParams); }, + 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); }, + updateEmployeeEmails: async ( + _, + { employeeId, params }, + { dataSources } + ) => { + return dataSources.restapi.updateEmployeeEmails(employeeId, params); + }, + 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 000000000..1aa8dcfc6 --- /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 9b11d9898..6eab27669 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 @@ -30,21 +31,21 @@ type DataSourceDetails { cost: Float! discovery_infos: [DataSourceDiscoveryInfos] forecast: Float! - last_month_cost: Float! + last_month_cost: Float resources: Int! } 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 @@ -302,6 +330,7 @@ input AzureTenantConfigInput { input GcpBillingDataConfigInput { dataset_name: String! table_name: String! + project_id: String } input GcpConfigInput { @@ -309,6 +338,11 @@ input GcpConfigInput { credentials: JSONObject! } +input GcpTenantConfigInput { + billing_data: GcpBillingDataConfigInput! + credentials: JSONObject! +} + input AlibabaConfigInput { access_key_id: String! secret_access_key: String! @@ -328,6 +362,7 @@ input NebiusConfigInput { input K8sConfigInput { password: String! user: String! + cost_model: JSONObject } input DatabricksConfigInput { @@ -336,6 +371,21 @@ input DatabricksConfigInput { client_secret: String! } +input CreateDataSourceInput { + name: String + type: String + awsRootConfig: AwsRootConfigInput + awsLinkedConfig: AwsLinkedConfigInput + azureSubscriptionConfig: AzureSubscriptionConfigInput + azureTenantConfig: AzureTenantConfigInput + gcpConfig: GcpConfigInput + gcpTenantConfig: GcpTenantConfigInput + alibabaConfig: AlibabaConfigInput + nebiusConfig: NebiusConfigInput + databricksConfig: DatabricksConfigInput + k8sConfig: K8sConfigInput +} + input UpdateDataSourceInput { name: String lastImportAt: Int @@ -345,22 +395,132 @@ input UpdateDataSourceInput { azureSubscriptionConfig: AzureSubscriptionConfigInput azureTenantConfig: AzureTenantConfigInput gcpConfig: GcpConfigInput + gcpTenantConfig: GcpTenantConfigInput alibabaConfig: AlibabaConfigInput nebiusConfig: NebiusConfigInput databricksConfig: DatabricksConfigInput 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 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_name: 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! ): DataSourceInterface + updateEmployeeEmails( + employeeId: ID! + params: UpdateEmployeeEmailsInput! + ): [EmployeeEmail] + updateEmployeeEmail( + 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/package.json b/ngui/server/package.json index 494fca696..227dcbd80 100644 --- a/ngui/server/package.json +++ b/ngui/server/package.json @@ -32,7 +32,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 59d8ece3a..26bf02346 100644 --- a/ngui/server/pnpm-lock.yaml +++ b/ngui/server/pnpm-lock.yaml @@ -37,8 +37,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/server/server.ts b/ngui/server/server.ts index bec821db7..d61595e9d 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/package.json b/ngui/ui/package.json index 7ce4054d2..6d263c7ab 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", @@ -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", @@ -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 383315233..2043df4bc 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 @@ -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 @@ -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: @@ -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 @@ -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': '*' @@ -4676,13 +4676,13 @@ 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 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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==} @@ -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: diff --git a/ngui/ui/public/docs/assignment-rules.md b/ngui/ui/public/docs/assignment-rules.md new file mode 100644 index 000000000..ceb92c774 --- /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 diff --git a/ngui/ui/src/api/auth/actionCreators.ts b/ngui/ui/src/api/auth/actionCreators.ts index 0113af409..7d389a4be 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 f56ca204f..3fa15fedb 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 8ef321414..82a283c96 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 31365acd7..b6a237d1f 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 cb1d494ea..a536fd920 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 09e4a5f4c..93772fab3 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 07f9211b2..7ed39862f 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`, @@ -634,7 +518,8 @@ export const createPool = (organizationId, params) => parent_id: params.parentId, limit: params.limit, purpose: params.type, - auto_extension: params.autoExtension + auto_extension: params.autoExtension, + default_owner_id: params.defaultOwnerId } }); @@ -672,29 +557,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 +641,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 +718,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`, @@ -1460,6 +1294,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 @@ -2240,24 +2075,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 +2261,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 +2291,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 +2683,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 +2707,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/api/restapi/actionTypes.ts b/ngui/ui/src/api/restapi/actionTypes.ts index 8f70dd069..1a1fd8aaf 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 7f6c53813..9d861349c 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 56de3d857..a537b6c35 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 36652540e..0ec6f706a 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 5020e805d..000000000 --- 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 fed00f8a6..000000000 --- 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 79a6a7aaf..000000000 --- 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/Accordion/Accordion.styles.ts b/ngui/ui/src/components/Accordion/Accordion.styles.ts index 477258f79..163c2f322 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 d7b274979..9d6143147 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/ActionBar/ActionBar.tsx b/ngui/ui/src/components/ActionBar/ActionBar.tsx index 37a01de48..bbb73a5b6 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} diff --git a/ngui/ui/src/components/ActivityListener/ActivityListener.ts b/ngui/ui/src/components/ActivityListener/ActivityListener.ts index 0f63d5bba..d5cc57b63 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 9ff842ff3..5fb150281 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 34db5a591..a8a390009 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/ArtifactsTable/ArtifactsTable.tsx b/ngui/ui/src/components/ArtifactsTable/ArtifactsTable.tsx index bdc2f2a79..fe0cc5807 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/AssignmentRules/AssignmentRules.tsx b/ngui/ui/src/components/AssignmentRules/AssignmentRules.tsx index faf9b0df8..110a41fc2 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 c0e0faa41..5e1c6d207 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/CloudAccountDetails/CloudAccountDetails.tsx b/ngui/ui/src/components/CloudAccountDetails/CloudAccountDetails.tsx index f4d9e6ce7..afffdfe7b 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/CloudAccountsOverview.tsx b/ngui/ui/src/components/CloudAccountsOverview/CloudAccountsOverview.tsx index 4448d9842..05b2181c0 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/CloudAccountsOverview/CloudAccountsOverviewMocked.tsx b/ngui/ui/src/components/CloudAccountsOverview/CloudAccountsOverviewMocked.tsx index 53e0b6bdb..ee7dce0ce 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 8f5ce4c67..f7bf1e61a 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/CloudCostComparison/CloudCostComparison.tsx b/ngui/ui/src/components/CloudCostComparison/CloudCostComparison.tsx index dd53d944a..2934db914 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 bd9261891..dc15cc527 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 6e8b313d9..ab797d8ce 100644 --- a/ngui/ui/src/components/ClusterTypes/ClusterTypes.tsx +++ b/ngui/ui/src/components/ClusterTypes/ClusterTypes.tsx @@ -5,7 +5,7 @@ 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 { MPT_SPACING_2 } from "../../utils/layouts"; @@ -22,20 +22,6 @@ const actionBarDefinition = { } }; -const ExplanationMessage = () => ( - ( - - {chunks} - - ) - }} - /> -); - const ClusterTypes = ({ clusterTypes, onUpdatePriority, isLoading = false }) => ( <> @@ -45,7 +31,20 @@ const ClusterTypes = ({ clusterTypes, onUpdatePriority, isLoading = false }) => - + ( + + {chunks} + + ) + } + }} + /> 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 000000000..8ed9c0788 --- /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 000000000..ca485c37c --- /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 8c70b7411..3f1dd05aa 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/Dashboard/Dashboard.tsx b/ngui/ui/src/components/Dashboard/Dashboard.tsx index 02c8524d3..44e480858 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/DataSourceCredentialFields/GcpTenantCredentials/GcpTenantCredentials.tsx b/ngui/ui/src/components/DataSourceCredentialFields/GcpTenantCredentials/GcpTenantCredentials.tsx new file mode 100644 index 000000000..03a4b0231 --- /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 000000000..70cbe42b3 --- /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 b73e8796e..068463188 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/ChildrenList/ChildrenList.tsx b/ngui/ui/src/components/DataSourceDetails/ChildrenList/ChildrenList.tsx index 60e230df6..831b68db9 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/DataSourceDetails.tsx b/ngui/ui/src/components/DataSourceDetails/DataSourceDetails.tsx index e4de0837e..04beeb2fb 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/DataSourceDetails/Properties/AzureProperties.tsx b/ngui/ui/src/components/DataSourceDetails/Properties/AzureProperties.tsx index 3c68a0eb5..afe3ea5c1 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/Environments/Environments.tsx b/ngui/ui/src/components/Environments/Environments.tsx index f83c4c641..cd58ed4c3 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/EnvironmentsTable/EnvironmentsTable.tsx b/ngui/ui/src/components/EnvironmentsTable/EnvironmentsTable.tsx index 23e90cfa8..95634c70b 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) => ({ diff --git a/ngui/ui/src/components/ErrorBoundary/ErrorBoundary.tsx b/ngui/ui/src/components/ErrorBoundary/ErrorBoundary.tsx index 3b638757f..397a27276 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; diff --git a/ngui/ui/src/components/ExecutionBreakdown/DashboardControls/DashboardControls.tsx b/ngui/ui/src/components/ExecutionBreakdown/DashboardControls/DashboardControls.tsx index 3004c7d78..09b76630a 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 2ff70ce14..f84bef0ae 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 3b7575905..3b3309670 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/ExpensesBreakdown/TableWidget/ExpensesBreakdownTableWidget.tsx b/ngui/ui/src/components/ExpensesBreakdown/TableWidget/ExpensesBreakdownTableWidget.tsx index 8a49a2746..fcd2c89ac 100644 --- a/ngui/ui/src/components/ExpensesBreakdown/TableWidget/ExpensesBreakdownTableWidget.tsx +++ b/ngui/ui/src/components/ExpensesBreakdown/TableWidget/ExpensesBreakdownTableWidget.tsx @@ -26,12 +26,12 @@ const getTableWrapperCardTitleName = (filterBy) => const getTableEmptyMessageId = (filterBy) => ({ - [EXPENSES_FILTERBY_TYPES.POOL]: "noPools", - [EXPENSES_FILTERBY_TYPES.CLOUD]: "noDataSources", - [EXPENSES_FILTERBY_TYPES.SERVICE]: "noServices", - [EXPENSES_FILTERBY_TYPES.REGION]: "noRegions", - [EXPENSES_FILTERBY_TYPES.EMPLOYEE]: "noOwners", - [EXPENSES_FILTERBY_TYPES.RESOURCE_TYPE]: "noResourceTypes" + [EXPENSES_FILTERBY_TYPES.POOL]: "noPoolExpenses", + [EXPENSES_FILTERBY_TYPES.CLOUD]: "noDataSourceExpenses", + [EXPENSES_FILTERBY_TYPES.SERVICE]: "noServiceExpenses", + [EXPENSES_FILTERBY_TYPES.REGION]: "noRegionExpenses", + [EXPENSES_FILTERBY_TYPES.EMPLOYEE]: "noOwnerExpenses", + [EXPENSES_FILTERBY_TYPES.RESOURCE_TYPE]: "noResourceTypeExpenses" })[filterBy]; const getExpensesTableData = ({ filteredBreakdown, totalExpenses, urlGetter, colorsMap }) => diff --git a/ngui/ui/src/components/FormContentDescription/FormContentDescription.tsx b/ngui/ui/src/components/FormContentDescription/FormContentDescription.tsx new file mode 100644 index 000000000..6f1c46c81 --- /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 000000000..5bcbe7f3e --- /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/GenerateLiveDemo/GenerateLiveDemo.tsx b/ngui/ui/src/components/GenerateLiveDemo/GenerateLiveDemo.tsx index a987c05c1..54421150a 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 e450bb1c0..d662742d1 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/Icon/Icon.tsx b/ngui/ui/src/components/Icon/Icon.tsx index 30e5b38bd..801cb131f 100644 --- a/ngui/ui/src/components/Icon/Icon.tsx +++ b/ngui/ui/src/components/Icon/Icon.tsx @@ -1,8 +1,29 @@ +import { ComponentType, ReactNode } from "react"; +import { SvgIconProps } from "@mui/material"; import { FormattedMessage } from "react-intl"; import KeyValueLabel from "components/KeyValueLabel/KeyValueLabel"; import Tooltip from "components/Tooltip"; import useStyles from "./Icon.styles"; +type IconProps = { + icon: ComponentType<{ + fontSize?: SvgIconProps["fontSize"]; + className?: string; + }>; + hasRightMargin?: boolean; + hasLeftMargin?: boolean; + fontSize?: SvgIconProps["fontSize"]; + color?: "info" | "success" | "warning" | "error"; + tooltip?: { + show?: boolean; + value?: string; + messageId?: string; + body?: ReactNode; + placement?: "top" | "bottom" | "left" | "right"; + }; + dataTestId?: string; +}; + const Icon = ({ icon: IconComponent, hasRightMargin = false, @@ -11,7 +32,7 @@ const Icon = ({ color = "info", tooltip = {}, dataTestId -}) => { +}: IconProps) => { const { classes, cx } = useStyles(); const { show: showTooltip = false, value = "", messageId = "", body, placement = "top" } = tooltip; diff --git a/ngui/ui/src/components/IconLabel/IconLabel.tsx b/ngui/ui/src/components/IconLabel/IconLabel.tsx index 77d8108e0..d158cc1f6 100644 --- a/ngui/ui/src/components/IconLabel/IconLabel.tsx +++ b/ngui/ui/src/components/IconLabel/IconLabel.tsx @@ -1,4 +1,12 @@ -const IconLabel = ({ icon: startIcon, endIcon, label }) => ( +import { ReactNode } from "react"; + +type IconLabelProps = { + label: ReactNode; + icon?: ReactNode; + endIcon?: ReactNode; +}; + +const IconLabel = ({ icon: startIcon, endIcon, label }: IconLabelProps) => (
{startIcon && <>{startIcon} } {label} diff --git a/ngui/ui/src/components/InlineSeverityAlert/InlineSeverityAlert.tsx b/ngui/ui/src/components/InlineSeverityAlert/InlineSeverityAlert.tsx index 386ba325f..8ff831480 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 e8580eb24..197d01743 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/Input/Input.tsx b/ngui/ui/src/components/Input/Input.tsx index 2d1fdaf43..4e0b6dc95 100644 --- a/ngui/ui/src/components/Input/Input.tsx +++ b/ngui/ui/src/components/Input/Input.tsx @@ -25,15 +25,11 @@ const Input = forwardRef((props, ref) => { const inputClassName = cx(isMasked ? classes.masked : ""); - const { readOnly = false, style } = InputProps; - - // Please note, disableUnderline not supported by outlined variant. - // But now we replacing variant to standard if control is readOnly - const InputPropsMerged = { ...InputProps, style, ...(readOnly ? { disableUnderline: true } : {}) }; + const { readOnly } = InputProps; return ( event.preventDefault() : undefined} @@ -42,12 +38,18 @@ const Input = forwardRef((props, ref) => { "data-test-id": dataTestId, className: cx(inputClassName, inputProps.className) }} - sx={sx} + sx={{ + sx, + fieldset: { + ...sx?.fieldset, + border: readOnly ? "none" : undefined + } + }} InputLabelProps={{ shrink: true, ...InputLabelProps }} - InputProps={InputPropsMerged} + InputProps={InputProps} minRows={minRows} maxRows={maxRows} {...rest} diff --git a/ngui/ui/src/components/K8sRightsizing/K8sRightsizing.tsx b/ngui/ui/src/components/K8sRightsizing/K8sRightsizing.tsx index 907505f09..763a4ea78 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 36286c898..5f7c6d542 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/MainMenu/MainMenu.tsx b/ngui/ui/src/components/MainMenu/MainMenu.tsx index 8d37cacc0..855cada58 100644 --- a/ngui/ui/src/components/MainMenu/MainMenu.tsx +++ b/ngui/ui/src/components/MainMenu/MainMenu.tsx @@ -4,11 +4,11 @@ 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"; import useStyles from "./MainMenu.styles"; const SimpleItem = ({ menuItem }) => { - const optScaleMode = useOptScaleMode(); + const { optscaleMode } = useGetOptscaleMode(); return ( @@ -19,7 +19,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 581a8dda3..2f8a756dc 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/MlArtifacts/MlArtifacts.tsx b/ngui/ui/src/components/MlArtifacts/MlArtifacts.tsx index a98f1e79b..fea17f5da 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 }) => { ( ( - -
- - - -
-
- -
-
+ <> + + + + + ); export default MlEditTaskMetrics; diff --git a/ngui/ui/src/components/MlExecutorsTable/MlExecutorsTable.tsx b/ngui/ui/src/components/MlExecutorsTable/MlExecutorsTable.tsx index d060da7ca..6f7b7f19e 100644 --- a/ngui/ui/src/components/MlExecutorsTable/MlExecutorsTable.tsx +++ b/ngui/ui/src/components/MlExecutorsTable/MlExecutorsTable.tsx @@ -1,34 +1,25 @@ import { useMemo } from "react"; import Table from "components/Table"; import TableLoader from "components/TableLoader"; -import { useIsOptScaleModeEnabled } from "hooks/useIsOptScaleModeEnabled"; -import { lastUsed, firstSeen, mlExecutorLocation, expenses } from "utils/columns"; -import executor from "utils/columns/executor"; -import { OPTSCALE_MODE } from "utils/constants"; +import { getColumns } from "./utils"; -const MlExecutorsTable = ({ executors, isLoading }) => { +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 000000000..0260bffe6 --- /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/MlMetrics/MlMetrics.tsx b/ngui/ui/src/components/MlMetrics/MlMetrics.tsx index 9ef0d6aa5..05c6d3a50 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/MlRunHistoryChart/RecommendationListItem copy/RecommendationListItem.test.tsx b/ngui/ui/src/components/MlRunHistoryChart/RecommendationListItem copy/RecommendationListItem.test.tsx deleted file mode 100644 index cbec58d1d..000000000 --- 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 fea4a20bc..000000000 --- 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 12f224aa8..000000000 --- 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 cbec58d1d..000000000 --- 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 fea4a20bc..000000000 --- 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 12f224aa8..000000000 --- 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 ff901f9b1..000000000 --- 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 bf4c8535e..000000000 --- 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 490eaa93f..000000000 --- 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 ff901f9b1..000000000 --- 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 bf4c8535e..000000000 --- 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 490eaa93f..000000000 --- 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 f892ed44d..000000000 --- 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 42503f771..000000000 --- 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/MlTaskRun/Components/Charts/Charts.tsx b/ngui/ui/src/components/MlTaskRun/Components/Charts/Charts.tsx new file mode 100644 index 000000000..d55bb3705 --- /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 dc269c18a..000000000 --- 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 000000000..92d6228a8 --- /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 885cf0bf3..0939ebdb6 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 cbf5a1e7a..000000000 --- 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 000000000..fde292b08 --- /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 000000000..7df28ec22 --- /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 96ae224f5..34f94c6e8 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 9a17e0916..e604a8d19 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 d47d7e1b4..f5510c69d 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/MlTasks/MlTasks.tsx b/ngui/ui/src/components/MlTasks/MlTasks.tsx index 45bf3ad0f..2a8a898f7 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/Mode/Mode.tsx b/ngui/ui/src/components/Mode/Mode.tsx index 67475b97e..f1c7613de 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 d3228d17e..2443ca489 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 25f619087..646839cc2 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 0072e05d2..81ed8caeb 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 61fbb97ab..26ed615b3 100644 --- a/ngui/ui/src/components/OrganizationSelector/OrganizationSelector.tsx +++ b/ngui/ui/src/components/OrganizationSelector/OrganizationSelector.tsx @@ -44,7 +44,7 @@ const SELECTOR_SX = { }; type OrganizationSelectorProps = { - organizations?: { + organizations: { id: string; name: string; }[]; @@ -54,7 +54,7 @@ type OrganizationSelectorProps = { }; const OrganizationSelector = ({ - organizations = [], + organizations, organizationId = "", onChange, isLoading = false @@ -126,7 +126,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 f851fc7a0..fed7cc6c0 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/PageContentDescription/PageContentDescription.tsx b/ngui/ui/src/components/PageContentDescription/PageContentDescription.tsx new file mode 100644 index 000000000..51bf871e6 --- /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 000000000..a83bfef1d --- /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/PasswordRecovery/PasswordRecovery.tsx b/ngui/ui/src/components/PasswordRecovery/PasswordRecovery.tsx index be80b2a95..bc7803c09 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 47f8d5a60..9ece929a4 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/PoolConstraints/PoolConstraints.tsx b/ngui/ui/src/components/PoolConstraints/PoolConstraints.tsx index ebad9b32b..793753bf8 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/PoolLabel/PoolLabel.tsx b/ngui/ui/src/components/PoolLabel/PoolLabel.tsx index d267968ed..71262b36e 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/PowerSchedules/PowerSchedules.tsx b/ngui/ui/src/components/PowerSchedules/PowerSchedules.tsx index 51c7dd654..08b039073 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} + /> + )} ); diff --git a/ngui/ui/src/components/RecommendationDetails/Details/Details.tsx b/ngui/ui/src/components/RecommendationDetails/Details/Details.tsx index c7a4dc6cc..63c3a42fa 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 } + }} /> )}
{ - 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 bf4c8535e..063a70cb0 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 ( { @@ -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 5d02e26de..869fe6f70 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 09035e0b6..8d5c58076 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/ResourceLocationCell/ResourceLocationCell.tsx b/ngui/ui/src/components/ResourceLocationCell/ResourceLocationCell.tsx index cb17db3f4..dc1482f58 100644 --- a/ngui/ui/src/components/ResourceLocationCell/ResourceLocationCell.tsx +++ b/ngui/ui/src/components/ResourceLocationCell/ResourceLocationCell.tsx @@ -1,12 +1,9 @@ -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; import CaptionedCell from "components/CaptionedCell"; import CloudLabel from "components/CloudLabel"; -import { useApiData } from "hooks/useApiData"; +import { useAllDataSources } from "hooks/coreData"; const ResourceLocationCell = ({ dataSource, caption }) => { - const { - apiData: { cloudAccounts: dataSources = [] } - } = useApiData(GET_DATA_SOURCES); + const dataSources = useAllDataSources(); return ( diff --git a/ngui/ui/src/components/ResourceRecommendations/ResourceRecommendations.tsx b/ngui/ui/src/components/ResourceRecommendations/ResourceRecommendations.tsx index ec9cf1317..957d12b89 100644 --- a/ngui/ui/src/components/ResourceRecommendations/ResourceRecommendations.tsx +++ b/ngui/ui/src/components/ResourceRecommendations/ResourceRecommendations.tsx @@ -26,7 +26,7 @@ const descriptionColumn = (state) => ({ ) }); -const actionsColumn = ({ active = true, isLoading, patchResource, shouldRenderTableActions }) => ({ +const actionsColumn = ({ active = true, isLoading, patchResource, havePermissionsToPerformActions }) => ({ header: ( @@ -43,10 +43,10 @@ const actionsColumn = ({ active = true, isLoading, patchResource, shouldRenderTa onClick={() => patchResource(original.name, active ? RESOURCE_VISIBILITY_ACTIONS.DISMISS : RESOURCE_VISIBILITY_ACTIONS.ACTIVATE) } - disabled={!shouldRenderTableActions} + disabled={!havePermissionsToPerformActions} tooltip={{ show: true, - value: shouldRenderTableActions ? ( + value: havePermissionsToPerformActions ? ( ) : ( @@ -72,16 +72,16 @@ const ResourceRecommendationLayout = ({ title, table }) => ( const DismissedResourceRecommendations = ({ patchResource, dismissedRecommendations = [], - shouldRenderTableActions, + havePermissionsToPerformActions, isLoading }) => { const data = useMemo(() => dismissedRecommendations, [dismissedRecommendations]); const columns = useMemo( () => [ descriptionColumn("dismissed"), - actionsColumn({ active: false, isLoading, patchResource, shouldRenderTableActions }) + actionsColumn({ active: false, isLoading, patchResource, havePermissionsToPerformActions }) ], - [isLoading, patchResource, shouldRenderTableActions] + [isLoading, patchResource, havePermissionsToPerformActions] ); return ( { +const ActiveResourceRecommendations = ({ + patchResource, + activeRecommendations = [], + havePermissionsToPerformActions, + isLoading +}) => { const data = useMemo(() => activeRecommendations, [activeRecommendations]); - const columns = useMemo( - () => [ + const columns = useMemo(() => { + const includesAnyDismissable = activeRecommendations.some((recommendation) => recommendation.dismissable); + + return [ descriptionColumn("active"), { header: ( @@ -120,10 +127,11 @@ const ActiveResourceRecommendations = ({ patchResource, activeRecommendations = defaultSort: "desc", cell: ({ cell }) => }, - actionsColumn({ active: true, isLoading, patchResource, shouldRenderTableActions }) - ], - [isLoading, patchResource, shouldRenderTableActions] - ); + ...(includesAnyDismissable + ? [actionsColumn({ active: true, isLoading, patchResource, havePermissionsToPerformActions })] + : []) + ]; + }, [activeRecommendations, isLoading, patchResource, havePermissionsToPerformActions]); return ( } @@ -160,7 +168,7 @@ const ResourceRecommendations = ({ resourceId, isLoading = false }) => { - const shouldRenderTableActions = useIsAllowed({ + const havePermissionsToPerformActions = useIsAllowed({ entityType: SCOPE_TYPES.RESOURCE, entityId: resourceId, requiredActions: ["MANAGE_RESOURCES", "MANAGE_OWN_RESOURCES"] @@ -181,7 +189,7 @@ const ResourceRecommendations = ({ )} @@ -189,7 +197,7 @@ const ResourceRecommendations = ({ )} diff --git a/ngui/ui/src/components/ResourceTypeLabel/ResourceTypeLabel.tsx b/ngui/ui/src/components/ResourceTypeLabel/ResourceTypeLabel.tsx index 200c83d5f..c0abff429 100644 --- a/ngui/ui/src/components/ResourceTypeLabel/ResourceTypeLabel.tsx +++ b/ngui/ui/src/components/ResourceTypeLabel/ResourceTypeLabel.tsx @@ -1,14 +1,80 @@ +import { ReactNode } from "react"; import DnsOutlinedIcon from "@mui/icons-material/DnsOutlined"; import GroupWorkOutlinedIcon from "@mui/icons-material/GroupWorkOutlined"; import { FormattedMessage } from "react-intl"; import Icon from "components/Icon"; import IconLabel from "components/IconLabel"; -const DefaultLabel = ({ label }) => label || null; +type ClusterIconProps = { + dataTestId: string; + hasRightMargin?: boolean; +}; + +type EnvironmentIconProps = { + dataTestId: string; + hasRightMargin?: boolean; +}; + +type DefaultLabelProps = { + label: ReactNode; +}; + +type ClusterLabelProps = { + label: ReactNode; + iconDataTestId: string; +}; + +type EnvironmentLabelProps = { + label: ReactNode; + iconDataTestId: string; +}; + +type EnvironmentClusterLabelProps = { + label: ReactNode; + iconDataTestId: string; +}; + +type ResourceInfo = { + isEnvironment: boolean; + shareable: boolean; + clusterTypeId: string; + resourceType: string; +}; + +type ResourceTypeLabelProps = { + resourceInfo: ResourceInfo; + iconDataTestId: string; +}; + +const ClusterIcon = ({ dataTestId, hasRightMargin = false }: ClusterIconProps) => ( + +); + +const EnvironmentIcon = ({ dataTestId, hasRightMargin = false }: EnvironmentIconProps) => ( + +); + +const DefaultLabel = ({ label }: DefaultLabelProps) => label || null; -const ClusterLabel = ({ label, iconDataTestId }) => ( +const ClusterLabel = ({ label, iconDataTestId }: ClusterLabelProps) => ( } + icon={} label={ {label} () @@ -17,9 +83,9 @@ const ClusterLabel = ({ label, iconDataTestId }) => ( /> ); -const EnvironmentLabel = ({ label, iconDataTestId }) => ( +const EnvironmentLabel = ({ label, iconDataTestId }: EnvironmentLabelProps) => ( } + icon={} label={ {label} () @@ -28,12 +94,12 @@ const EnvironmentLabel = ({ label, iconDataTestId }) => ( /> ); -const EnvironmentClusterLabel = ({ label, iconDataTestId }) => ( +const EnvironmentClusterLabel = ({ label, iconDataTestId }: EnvironmentClusterLabelProps) => ( - - + + } label={ @@ -44,7 +110,7 @@ const EnvironmentClusterLabel = ({ label, iconDataTestId }) => ( /> ); -const getLabelComponent = (resourceInfo) => { +const getLabelComponent = (resourceInfo: ResourceInfo) => { if (resourceInfo.isEnvironment || resourceInfo.shareable) { return resourceInfo.clusterTypeId ? EnvironmentClusterLabel : EnvironmentLabel; } @@ -54,7 +120,7 @@ const getLabelComponent = (resourceInfo) => { return DefaultLabel; }; -const ResourceTypeLabel = ({ resourceInfo, iconDataTestId }) => { +const ResourceTypeLabel = ({ resourceInfo, iconDataTestId }: ResourceTypeLabelProps) => { const LabelComponent = getLabelComponent(resourceInfo); return ; }; diff --git a/ngui/ui/src/components/ResourcesPerspectives/ResourcesPerspectives.tsx b/ngui/ui/src/components/ResourcesPerspectives/ResourcesPerspectives.tsx index 5d3f0bbb8..3b1c67c03 100644 --- a/ngui/ui/src/components/ResourcesPerspectives/ResourcesPerspectives.tsx +++ b/ngui/ui/src/components/ResourcesPerspectives/ResourcesPerspectives.tsx @@ -1,26 +1,25 @@ 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"; 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"; -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/ShareRunLink/ShareRunLink.tsx b/ngui/ui/src/components/ShareRunLink/ShareRunLink.tsx new file mode 100644 index 000000000..2b7cb4f75 --- /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 000000000..3a3874d50 --- /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 881a6024c..edcd5c09c 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 000000000..74d3a471d --- /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 dde56a107..2fb2948bc 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/components/SideModalManager/SideModals/recommendations/components/InformationWrapper.tsx b/ngui/ui/src/components/SideModalManager/SideModals/recommendations/components/InformationWrapper.tsx index 357f73d1e..dff4ccb7d 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/TopAlertWrapper/TopAlertWrapper.tsx b/ngui/ui/src/components/TopAlertWrapper/TopAlertWrapper.tsx index 1a098dd34..264fe06d7 100644 --- a/ngui/ui/src/components/TopAlertWrapper/TopAlertWrapper.tsx +++ b/ngui/ui/src/components/TopAlertWrapper/TopAlertWrapper.tsx @@ -1,9 +1,12 @@ +// import { useApiData } from "hooks/useApiData"; +// import { useApiState } from "hooks/useApiState"; +// import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; +// import { useGetToken } from "hooks/useGetToken"; +// import { useRootData } from "hooks/useRootData"; import { useCallback, useEffect, useMemo } from "react"; import { FormattedMessage } from "react-intl"; import { useDispatch } from "react-redux"; -import { GET_DATA_SOURCES } from "api/restapi/actionTypes"; -import { useApiData } from "hooks/useApiData"; -import { useApiState } from "hooks/useApiState"; +import { useAllDataSources } from "hooks/coreData"; import { useOrganizationInfo } from "hooks/useOrganizationInfo"; import { AZURE_TENANT, ENVIRONMENT } from "utils/constants"; import { updateOrganizationTopAlert as updateOrganizationTopAlertActionCreator } from "./actionCreators"; @@ -50,22 +53,15 @@ const TopAlertWrapper = ({ blacklistIds = [] }) => { const { organizationId } = useOrganizationInfo(); - // MPT_TODO: disabled openSourceAnnouncement - // const { - // apiData: { userId } - // } = useApiData(GET_TOKEN); - // - // const { rootData: isExistingUser = false } = useRootData(IS_EXISTING_USER); + // const { userId } = useGetToken(); const storedAlerts = useAllAlertsSelector(organizationId); - const { - apiData: { cloudAccounts = [] } - } = useApiData(GET_DATA_SOURCES); + // const { rootData: isExistingUser = false } = useRootData(IS_EXISTING_USER); - const { isDataReady: isDataSourceReady } = useApiState(GET_DATA_SOURCES, organizationId); + const dataSources = useAllDataSources(); - const eligibleDataSources = getEligibleDataSources(cloudAccounts); + const eligibleDataSources = getEligibleDataSources(dataSources); const hasDataSourceInProcessing = eligibleDataSources.some(({ last_import_at: lastImportAt }) => lastImportAt === 0); @@ -82,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( @@ -100,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 }); @@ -113,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), @@ -161,7 +157,8 @@ const TopAlertWrapper = ({ blacklistIds = [] }) => { // dataTestId: "top_alert_open_source_announcement" // } ]; - }, [storedAlerts, isDataSourceReady, hasDataSourceInProcessing, updateOrganizationTopAlert]); + // }, [storedAlerts, hasDataSourceInProcessing, isExistingUser, updateOrganizationTopAlert, userId, organizationId]); + }, [storedAlerts, hasDataSourceInProcessing, updateOrganizationTopAlert]); const currentAlert = useMemo( () => diff --git a/ngui/ui/src/components/TopResourcesExpensesCard/TopResourcesExpensesCard.tsx b/ngui/ui/src/components/TopResourcesExpensesCard/TopResourcesExpensesCard.tsx index 3c3ca5eff..c289e084e 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/TrafficExpensesMap/TrafficExpensesMap.tsx b/ngui/ui/src/components/TrafficExpensesMap/TrafficExpensesMap.tsx index f4e8f5473..581eb71f8 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/UpcomingBooking/BookingTimeMeasure/BookingTimeMeasure.tsx b/ngui/ui/src/components/UpcomingBooking/BookingTimeMeasure/BookingTimeMeasure.tsx deleted file mode 100644 index 25a16c1ba..000000000 --- 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 e4faeda84..000000000 --- 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 000000000..9d2435a31 --- /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 000000000..3a28ffcc7 --- /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 952c9a055..3289c546f 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 b1dd46f2a..1db1d6b19 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/components/UserEmailNotificationSettings/UserEmailNotificationSettings.tsx b/ngui/ui/src/components/UserEmailNotificationSettings/UserEmailNotificationSettings.tsx new file mode 100644 index 000000000..4c70611ca --- /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 000000000..de4b998b4 --- /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 000000000..d75343bd8 --- /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/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 fed80fa77..505273f52 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 74ad9f5d6..55c45cf32 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/AddInstancesToScheduleForm/FormElements/InstancesField.tsx b/ngui/ui/src/components/forms/AddInstancesToScheduleForm/FormElements/InstancesField.tsx index 8b93e8e1d..e365ccf49 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 { useIntl } from "react-intl"; +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) ? intl.formatMessage({ id: "atLeastOneInstanceMustBeSelected" }) : 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 b1ae2a97c..4fd22d7e0 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/ConnectCloudAccountForm/ConnectCloudAccountForm.tsx b/ngui/ui/src/components/forms/ConnectCloudAccountForm/ConnectCloudAccountForm.tsx index a361ee85d..6e236ea45 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 @@ -115,7 +119,6 @@ const getAwsParameters = (formData) => { access_key_id: formData[AWS_ROOT_CREDENTIALS_FIELD_NAMES.ACCESS_KEY_ID], secret_access_key: formData[AWS_ROOT_CREDENTIALS_FIELD_NAMES.SECRET_ACCESS_KEY], use_edp_discount: formData[AWS_ROOT_USE_AWS_EDP_DISCOUNT_FIELD_NAMES.USE_EDP_DISCOUNT], - linked: false, cur_version: Number(formData[AWS_ROOT_EXPORT_TYPE_FIELD_NAMES.CUR_VERSION]), ...getConfigSchemeParameters() } @@ -189,6 +192,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 +246,7 @@ const renderConnectionTypeDescription = (settings) => )); -const renderConnectionTypeInfoMessage = ({ connectionType }) => +const renderConnectionTypeInfoMessage = (connectionType) => ({ [AWS_ROOT_ACCOUNT]: renderConnectionTypeDescription([ { @@ -387,10 +406,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 +490,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 +563,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/CreateClusterTypeForm/CreateClusterTypeForm.tsx b/ngui/ui/src/components/forms/CreateClusterTypeForm/CreateClusterTypeForm.tsx index 46100b9a2..c1f76a290 100644 --- a/ngui/ui/src/components/forms/CreateClusterTypeForm/CreateClusterTypeForm.tsx +++ b/ngui/ui/src/components/forms/CreateClusterTypeForm/CreateClusterTypeForm.tsx @@ -5,7 +5,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 { MPT_SPACING_3 } from "utils/layouts"; @@ -45,21 +45,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 3a8e3a6ed..01544f722 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/CreateS3DuplicateFinderCheckForm/FormElements/BucketsField.tsx b/ngui/ui/src/components/forms/CreateS3DuplicateFinderCheckForm/FormElements/BucketsField.tsx index 0da7620c3..82f0fac7a 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; } } }} diff --git a/ngui/ui/src/components/forms/CreateSshKeyForm/CreateSshKeyForm.tsx b/ngui/ui/src/components/forms/CreateSshKeyForm/CreateSshKeyForm.tsx index a47f286d1..00aa44e0a 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 02a6ac8ef..690423d49 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 93dfd9c1e..a0d7536c1 100644 --- a/ngui/ui/src/components/forms/DisconnectCloudAccountForm/DisconnectCloudAccountForm.tsx +++ b/ngui/ui/src/components/forms/DisconnectCloudAccountForm/DisconnectCloudAccountForm.tsx @@ -1,9 +1,9 @@ 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 { AZURE_TENANT, GCP_TENANT } from "utils/constants"; import { SPACING_1 } from "utils/layouts"; import Survey from "./FormElements/Survey"; import { DisconnectCloudAccountFormProps, FormValues } from "./types"; @@ -19,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; @@ -26,10 +27,24 @@ const DisconnectCloudAccountForm = ({ return (
- {(parentId || isAzureTenant) && ( + {(parentId || isAzureTenant || isGcpTenant) && ( - {parentId && } - {isAzureTenant && } + {parentId && ( + + )} + {isAzureTenant || isGcpTenant ? ( + + ) : null} )} ( - {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/LoginForm/FormElements/FormButtons.tsx b/ngui/ui/src/components/forms/LoginForm/FormElements/FormButtons.tsx index 169ef57bf..a332c9e5f 100644 --- a/ngui/ui/src/components/forms/LoginForm/FormElements/FormButtons.tsx +++ b/ngui/ui/src/components/forms/LoginForm/FormElements/FormButtons.tsx @@ -1,13 +1,14 @@ import ButtonLoader from "components/ButtonLoader"; import FormButtonsWrapper from "components/FormButtonsWrapper"; -const FormButtons = ({ isLoading = false }) => ( +const FormButtons = ({ disabled = false, isLoading = false }) => ( { +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 470e9d5b1..721a35211 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/MlRunsetTemplateForm/MlRunsetTemplateForm.tsx b/ngui/ui/src/components/forms/MlRunsetTemplateForm/MlRunsetTemplateForm.tsx index e228a39bb..b5a43cee5 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/PoolForm/CreatePoolForm.tsx b/ngui/ui/src/components/forms/PoolForm/CreatePoolForm.tsx index 72b401063..53b71d795 100644 --- a/ngui/ui/src/components/forms/PoolForm/CreatePoolForm.tsx +++ b/ngui/ui/src/components/forms/PoolForm/CreatePoolForm.tsx @@ -3,12 +3,15 @@ import { FormProvider, useForm } from "react-hook-form"; import ButtonLoader from "components/ButtonLoader"; import FormButtonsWrapper from "components/FormButtonsWrapper"; import PoolsService from "services/PoolsService"; -import { NameField, LimitField, TypeSelector, AutoExtendCheckbox } from "./FormElements"; -import { CreatePoolFormValues } from "./types"; +import { NameField, LimitField, TypeSelector, AutoExtendCheckbox, OwnerSelector } from "./FormElements"; +import { CreatePoolFormProps, CreatePoolFormValues } from "./types"; import { getCreateFormDefaultValues } from "./utils"; -const CreatePoolForm = ({ parentId, onSuccess, unallocatedLimit }) => { - const { isLoading: isCreatePoolLoading, createPool } = PoolsService().useCreatePool(); +const CreatePoolForm = ({ parentId, onSuccess, unallocatedLimit }: CreatePoolFormProps) => { + const { useCreatePool, useGetPoolOwners } = PoolsService(); + const { isLoading: isCreatePoolLoading, createPool } = useCreatePool(); + + const { poolOwners, isDataReady: isPoolOwnersDataReady } = useGetPoolOwners(parentId); const methods = useForm({ defaultValues: getCreateFormDefaultValues() @@ -22,9 +25,10 @@ const CreatePoolForm = ({ parentId, onSuccess, unallocatedLimit }) => { + + - ( +type AutoExtendCheckboxProps = { + isLoading?: boolean; + isReadOnly?: boolean; +}; + +const AutoExtendCheckbox = ({ isLoading = false, isReadOnly = false }: AutoExtendCheckboxProps) => ( { +type PoolFormLimitInputProps = { + unallocatedLimit: number; + isLoading?: boolean; + isRootPool?: boolean; + isReadOnly?: boolean; +}; + +const PoolFormLimitInput = ({ + unallocatedLimit, + isLoading = false, + isRootPool = false, + isReadOnly = false +}: PoolFormLimitInputProps) => { const { currencySymbol } = useOrganizationInfo(); const intl = useIntl(); diff --git a/ngui/ui/src/components/forms/PoolForm/FormElements/NameField.tsx b/ngui/ui/src/components/forms/PoolForm/FormElements/NameField.tsx index 6044e4d76..05813b11a 100644 --- a/ngui/ui/src/components/forms/PoolForm/FormElements/NameField.tsx +++ b/ngui/ui/src/components/forms/PoolForm/FormElements/NameField.tsx @@ -4,7 +4,12 @@ import { FIELD_NAMES } from "../constants"; const FIELD_NAME = FIELD_NAMES.NAME; -const NameField = ({ isLoading = false, readOnly = false }) => ( +type NameFieldProps = { + isLoading?: boolean; + readOnly?: boolean; +}; + +const NameField = ({ isLoading = false, readOnly = false }: NameFieldProps) => ( } diff --git a/ngui/ui/src/components/forms/PoolForm/FormElements/OwnerSelector.tsx b/ngui/ui/src/components/forms/PoolForm/FormElements/OwnerSelector.tsx index 1387e164e..bd2df7c04 100644 --- a/ngui/ui/src/components/forms/PoolForm/FormElements/OwnerSelector.tsx +++ b/ngui/ui/src/components/forms/PoolForm/FormElements/OwnerSelector.tsx @@ -1,22 +1,49 @@ +import { useIntl } from "react-intl"; import { Selector } from "components/forms/common/fields"; +import QuestionMark from "components/QuestionMark"; import { ItemContent } from "components/Selector"; +import { useCurrentEmployee } from "hooks/coreData"; import { FIELD_NAMES } from "../constants"; +type OwnerSelectorProps = { + owners: { id: string; name: string }[]; + isLoading?: boolean; + isReadOnly?: boolean; + helpMessageId?: string; +}; + const FIELD_NAME = FIELD_NAMES.OWNER; -const OwnerSelector = ({ owners, isLoading = false, isReadOnly = false }) => ( - ({ - value: owner.id, - content: {owner.name} - }))} - /> -); +const OwnerSelector = ({ owners, isLoading = false, isReadOnly = false, helpMessageId }: OwnerSelectorProps) => { + const intl = useIntl(); + + const currentEmployee = useCurrentEmployee(); + + return ( + owner.id === currentEmployee.id), + ...owners + .filter((owner) => owner.id !== currentEmployee.id) + .sort(({ name: nameA }, { name: nameB }) => nameA.localeCompare(nameB)) + ].map((owner) => ({ + value: owner.id, + content: ( + + {owner.id === currentEmployee.id ? `${owner.name} (${intl.formatMessage({ id: "you" })})` : owner.name} + + ) + }))} + endAdornment={helpMessageId && } + /> + ); +}; export default OwnerSelector; diff --git a/ngui/ui/src/components/forms/PoolForm/FormElements/TypeSelector.tsx b/ngui/ui/src/components/forms/PoolForm/FormElements/TypeSelector.tsx index 2546e988f..138326401 100644 --- a/ngui/ui/src/components/forms/PoolForm/FormElements/TypeSelector.tsx +++ b/ngui/ui/src/components/forms/PoolForm/FormElements/TypeSelector.tsx @@ -15,9 +15,14 @@ import { } from "utils/constants"; import { FIELD_NAMES } from "../constants"; +type TypeSelectorProps = { + isLoading?: boolean; + readOnly?: boolean; +}; + const FIELD_NAME = FIELD_NAMES.TYPE; -const TypeSelector = ({ isLoading = false, readOnly = false }) => { +const TypeSelector = ({ isLoading = false, readOnly = false }: TypeSelectorProps) => { const intl = useIntl(); return ( diff --git a/ngui/ui/src/components/forms/PoolForm/types.ts b/ngui/ui/src/components/forms/PoolForm/types.ts index fc3b6cc5c..91626299a 100644 --- a/ngui/ui/src/components/forms/PoolForm/types.ts +++ b/ngui/ui/src/components/forms/PoolForm/types.ts @@ -2,6 +2,7 @@ export type CreatePoolFormValues = { name: string; limit: string; type: string; + defaultOwnerId: string; autoExtension: boolean; }; @@ -12,3 +13,9 @@ export type EditPoolFormValues = { defaultOwnerId: string; autoExtension: boolean; }; + +export type CreatePoolFormProps = { + parentId: string; + onSuccess: () => void; + unallocatedLimit: number; +}; diff --git a/ngui/ui/src/components/forms/PoolForm/utils.ts b/ngui/ui/src/components/forms/PoolForm/utils.ts index d3916a243..ba7529273 100644 --- a/ngui/ui/src/components/forms/PoolForm/utils.ts +++ b/ngui/ui/src/components/forms/PoolForm/utils.ts @@ -24,5 +24,6 @@ export const getCreateFormDefaultValues = (): CreatePoolFormValues => ({ [FIELD_NAMES.NAME]: "", [FIELD_NAMES.LIMIT]: "", [FIELD_NAMES.TYPE]: POOL_TYPE_BUDGET, + [FIELD_NAMES.OWNER]: "", [FIELD_NAMES.AUTO_EXTENSION]: false }); diff --git a/ngui/ui/src/components/forms/RegistrationForm/RegistrationForm.tsx b/ngui/ui/src/components/forms/RegistrationForm/RegistrationForm.tsx index efe692e81..8fff93f05 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 }; @@ -43,6 +43,7 @@ const RegistrationForm = ({ onSubmit, isLoading = false, isInvited = false }: Re color="primary" 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 ddbe402ff..e05336ea4 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/components/forms/RenameDataSourceForm/RenameDataSourceForm.tsx b/ngui/ui/src/components/forms/RenameDataSourceForm/RenameDataSourceForm.tsx index aebcadf03..b149188f2 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/FormElements/CredentialInputs.tsx b/ngui/ui/src/components/forms/UpdateDataSourceCredentialsForm/FormElements/CredentialInputs.tsx index def4dfe44..a3d5cdce6 100644 --- a/ngui/ui/src/components/forms/UpdateDataSourceCredentialsForm/FormElements/CredentialInputs.tsx +++ b/ngui/ui/src/components/forms/UpdateDataSourceCredentialsForm/FormElements/CredentialInputs.tsx @@ -17,7 +17,9 @@ import { AwsRootCredentials, AwsRootBillingBucket, AwsRootExportType, - AwsRootUseAwsEdpDiscount + AwsRootUseAwsEdpDiscount, + GcpTenantCredentials, + GCP_TENANT_CREDENTIALS_FIELD_NAMES } from "components/DataSourceCredentialFields"; import { Switch } from "components/forms/common/fields"; import { @@ -27,7 +29,17 @@ import { ReportBucketPathPrefix } from "components/NebiusConfigFormElements"; import UpdateServiceAccountCredentialsDescription from "components/NebiusConfigFormElements/UpdateServiceAccountCredentialsDescription"; -import { ALIBABA_CNR, AZURE_TENANT, AWS_CNR, AZURE_CNR, NEBIUS, GCP_CNR, DATABRICKS, KUBERNETES_CNR } from "utils/constants"; +import { + ALIBABA_CNR, + AZURE_TENANT, + AWS_CNR, + AZURE_CNR, + NEBIUS, + GCP_CNR, + DATABRICKS, + KUBERNETES_CNR, + GCP_TENANT +} from "utils/constants"; export const AWS_POOL_UPDATE_DATA_EXPORT_PARAMETERS = "updateDataExportParameters"; @@ -93,6 +105,15 @@ const CredentialInputs = ({ type, config }) => { ]} /> ); + case GCP_TENANT: + return ( +