diff --git a/.gitignore b/.gitignore index 6bcb9b6e7..8b8a8989c 100644 --- a/.gitignore +++ b/.gitignore @@ -214,3 +214,8 @@ oauth2.cfg scripts/keep_slack_bot.py keepnew.db providers_cache.json + +tests/provision/* +grafana/* +!grafana/provisioning/ +!grafana/dashboards/ \ No newline at end of file diff --git a/STRESS.md b/STRESS.md deleted file mode 100644 index dd248cc22..000000000 --- a/STRESS.md +++ /dev/null @@ -1,58 +0,0 @@ - -# UNDER CONSTRUCTION - -# First, create a Kubernetes cluster - - -# Install Keep -gcloud config set project keep-dev-429814 -gcloud container clusters get-credentials keep-stress --zone us-central1-c --project keep-dev-429814 -helm repo add keephq https://keephq.github.io/helm-charts -helm pull keephq/keep -# create the namespace -kubectl create namespace keep -# install keep -helm install keep keephq/keep --namespace keep -# from local -helm install keep ./charts/keep --namespace keep - -kubectl -n keep describe pod keep-backend-697f6b946f-v2jxp -kubectl -n keep logs keep-frontend-577fdf5497-r8ht9 -# Import alerts - -# uninstall -helm uninstall keep --namespace keep - -kubectl -n keep exec -it keep-backend-64c4d7ddb7-7p5q5 /bin/bash -# copy the db -kubectl -n keep exec -it keep-database-86dd6b6775-92sz4 /bin/bash -kubectl -n keep cp ./keep.sql keep-database-659c69689-vxhkz:/tmp/keep.sql -kubectl -n keep exec -it keep-database-659c69689-vxhkz -- bash -c "mysql -u root keep < /tmp/keep.sql" -# exec into the pod -kubectl -n keep exec -it keep-database-86dd6b6775-92sz4 -- /bin/bash -# import -kubectl -n keep exec -it keep-database-659c69689-vxhkz -- bash -c "mysql -u root keep < /tmp/keep.sql" - -# No Load -## 500k alerts - 1Gi/250m cpu: get_last_alerts 2 minutes and 30 seconds -Keep Backend Workers get a timeout after one minute (status code 500 for preset and alert endpoints) -## 500k alerts - 2Gi/500m cpu: -- default mysql: get_last_alerts 1 minutes and 30 seconds -- innodb_buffer_pool_size = 4294967296: 25 seconds, 3 seconds after cache -## 500k alerts - 4Gi/1 cpu: get_last_alerts 2 minutes and 30 seconds -- -## 500k alerts - 8Gi/1 cpu: get_last_alerts 2 minutes and 30 seconds - -# Load 10 alerts per minute - -# Load 100 alerts per minute - -# Load 1000 alerts per minute - - -## 1M alerts -# Load 10 alerts per minute - -# Load 100 alerts per minute - -# Load 1000 alerts per minute diff --git a/docker-compose.yml b/docker-compose.yml index 68291e6b6..14b6001ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,3 +26,33 @@ services: extends: file: docker-compose.common.yml service: keep-websocket-server-common + + grafana: + image: grafana/grafana:latest + profiles: + - grafana + ports: + - "3001:3000" + volumes: + - ./grafana:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning + - ./grafana/dashboards:/etc/grafana/dashboards + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false + depends_on: + - prometheus + + prometheus: + image: prom/prometheus:latest + profiles: + - grafana + ports: + - "9090:9090" + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + command: + - "--config.file=/etc/prometheus/prometheus.yml" + depends_on: + - keep-backend diff --git a/docker/Dockerfile.dev.api b/docker/Dockerfile.dev.api index d4f4e3a7b..def660dfa 100644 --- a/docker/Dockerfile.dev.api +++ b/docker/Dockerfile.dev.api @@ -22,6 +22,7 @@ ENV PYTHONPATH="/app:${PYTHONPATH}" ENV PATH="/venv/bin:${PATH}" ENV VIRTUAL_ENV="/venv" ENV POSTHOG_DISABLED="true" +ENV FRIGADE_DISABLED="true" ENTRYPOINT ["/app/keep/entrypoint.sh"] diff --git a/docs/api-ref/root.mdx b/docs/api-ref/root.mdx deleted file mode 100644 index 8c64891f9..000000000 --- a/docs/api-ref/root.mdx +++ /dev/null @@ -1,3 +0,0 @@ ---- -openapi: get / ---- \ No newline at end of file diff --git a/docs/deployment/configuration.mdx b/docs/deployment/configuration.mdx index ddf404912..30b0584a7 100644 --- a/docs/deployment/configuration.mdx +++ b/docs/deployment/configuration.mdx @@ -157,6 +157,18 @@ Sentry configuration controls Keep's integration with Sentry for error monitorin |:-------------------:|:-------:|:----------:|:-------------:|:-------------:| | **SENTRY_DISABLED** | Disables Sentry integration | No | "false" | "true" or "false" | + +### Frigade + + +Frigade configuration controls Keep's integration with the Frigade onboarding platform. + + +| Env var | Purpose | Required | Default Value | Valid options | +|:-------------------:|:-------:|:----------:|:-------------:|:-------------:| +| **FRIGADE_DISABLED** | Disables Frigade integration | No | "false" | "true" or "false" | + + ### Ngrok Ngrok configuration enables secure tunneling to your Keep instance. These settings are particularly useful for development or when you need to expose your local Keep instance to the internet securely. @@ -208,6 +220,24 @@ ARQ (Asynchronous Task Queue) configuration controls Keep's background task proc | **ARQ_EXPIRES** | Default job expiration time (in seconds) | No | 3600 | Positive integer | | **ARQ_EXPIRES_AI** | AI job expiration time (in seconds) | No | 3600000 | Positive integer | +### Rate Limiting + +Rate limiting configuration controls how many requests can be made to Keep's API endpoints within a specified time period. This helps prevent abuse and ensures system stability. + + +| Env var | Purpose | Required | Default Value | Valid options | +|:-------------------:|:-------:|:----------:|:-------------:|:-------------:| +| **KEEP_USE_LIMITER** | Enables or disables rate limiting | No | "false" | "true" or "false" | +| **KEEP_LIMIT_CONCURRENCY** | Sets the rate limit for API endpoints | No | "100/minute" | Format: "{number}/{interval}" where interval can be "second", "minute", "hour", "day" | + + +Currently, rate limiting is applied to the following endpoints: +- POST `/alerts/event` - Generic event ingestion endpoint +- POST `/alerts/{provider_type}` - Provider-specific event ingestion endpoints + +These endpoints are rate-limited according to the `KEEP_LIMIT_CONCURRENCY` setting when `KEEP_USE_LIMITER` is enabled. + + ## Frontend Environment Variables Frontend configuration variables control the behavior and features of Keep's user interface. These settings are crucial for customizing the frontend's appearance, functionality, and integration with the backend services. diff --git a/docs/deployment/kubernetes/overview.mdx b/docs/deployment/kubernetes/overview.mdx index a3d889f07..9b891c3d7 100644 --- a/docs/deployment/kubernetes/overview.mdx +++ b/docs/deployment/kubernetes/overview.mdx @@ -14,6 +14,6 @@ We maintain an opinionated, batteries-included Helm chart, but you can customize ## Next steps - Install Keep on [Kubernetes](/deployment/kubernetes/installation). - Keep's [Helm Chart](https://github.com/keephq/helm-charts). -- Keep with [Kubernetes Secret Manager](/deployment/secret-manager#kubernetes-secret-manager) +- Keep with [Kubernetes Secret Manager](/deployment/secret-store#kubernetes-secret-manager) - Deep dive to Keep's kubernetes [Architecture](/deployment/kubernetes/architecture). - Install Keep on [OpenShift](/deployment/kubernetes/openshift). diff --git a/docs/deployment/provision/overview.mdx b/docs/deployment/provision/overview.mdx index 7d6226f6a..290030d95 100644 --- a/docs/deployment/provision/overview.mdx +++ b/docs/deployment/provision/overview.mdx @@ -8,10 +8,11 @@ Keep supports various deployment and provisioning strategies to accommodate diff Keep offers three main provisioning options: -1. [**Provider Provisioning**](/deployment/provision/provider) - Set up and manage data providers for Keep. +1. [**Provider Provisioning**](/deployment/provision/provider) - Set up and manage data providers with their deduplication rules for Keep. 2. [**Workflow Provisioning**](/deployment/provision/workflow) - Configure and manage workflows within Keep. 3. [**Dashboard Provisioning**](/deployment/provision/dashboard) - Configure and manage dashboards within Keep. + Choosing the right provisioning strategy depends on your specific use case, deployment environment, and scalability requirements. You can read more about each provisioning option in their respective sections. ### How To Configure Provisioning @@ -23,10 +24,11 @@ Choosing the right provisioning strategy depends on your specific use case, depl Provisioning in Keep is controlled through environment variables and configuration files. The main environment variables for provisioning are: -| Provisioning Type | Environment Variable | Purpose | -| ----------------- | -------------------------- | ------------------------------------------------------ | -| **Provider** | `KEEP_PROVIDERS` | JSON string containing provider configurations | -| **Workflow** | `KEEP_WORKFLOWS_DIRECTORY` | Directory path containing workflow configuration files | -| **Dashboard** | `KEEP_DASHBOARDS` | JSON string containing dashboard configurations | +| Provisioning Type | Environment Variable | Purpose | +| ---------------------- | ------------------------------ | ----------------------------------------------------------------------- | +| **Provider** | `KEEP_PROVIDERS` | JSON string containing provider configurations with deduplication rules | +| **Workflow** | `KEEP_WORKFLOWS_DIRECTORY` | Directory path containing workflow configuration files | +| **Dashboard** | `KEEP_DASHBOARDS` | JSON string containing dashboard configurations | + For more details on each provisioning strategy, including setup instructions and implications, refer to the respective sections. diff --git a/docs/deployment/provision/provider.mdx b/docs/deployment/provision/provider.mdx index f6993aabf..937b02e7e 100644 --- a/docs/deployment/provision/provider.mdx +++ b/docs/deployment/provision/provider.mdx @@ -8,15 +8,33 @@ Provider provisioning in Keep allows you to set up and manage data providers dyn ### Configuring Providers -To provision providers, set the `KEEP_PROVIDERS` environment variable with a JSON string containing the provider configurations. Here's an example: +To provision providers and deduplication rules for them, set the `KEEP_PROVIDERS` environment variable. This can be done in two ways: +1. Directly with a JSON string containing the providers configurations. +2. With the path to a JSON file that contains the providers configurations. +Please note: Deduplication rules are not mandatory for provider distribution. See the Clickhouse example. + +Providers provisioning JSON example: ```json { "keepVictoriaMetrics": { "type": "victoriametrics", "authentication": { "VMAlertHost": "http://localhost", - "VMAlertPort": 1234 + "VMAlertPort": 1234, + "deduplication_rules": { + "deduplication rule name example 1": { + "description": "deduplication rule name example 1", + "fingerprint_fields": ["fingerprint", "source", "service"], + "full_deduplication": true, + "ignore_fields": ["name", "lastReceived"] + }, + "deduplication rule name example 2": { + "description": "deduplication rule name example 2", + "fingerprint_fields": ["fingerprint", "source", "service"], + "full_deduplication": false, + } + } } }, "keepClickhouse1": { diff --git a/docs/incidents/overview.mdx b/docs/incidents/overview.mdx index 0c9c9a38f..8d4687e38 100644 --- a/docs/incidents/overview.mdx +++ b/docs/incidents/overview.mdx @@ -21,10 +21,6 @@ A brief overview of the incident, optionally enhanced with AI-generated summarie ### (4) Link Similar Incidents Connects related incidents for better visibility into recurring or interconnected issues. - - - - ### (5) Involved Services Lists the services affected by the incident, allowing teams to understand the scope of the impact. diff --git a/docs/mint.json b/docs/mint.json index 6c2e0ff9e..59101220e 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -210,6 +210,7 @@ { "group": "Deployment", "pages": [ + "deployment/getting-started", "deployment/configuration", "deployment/monitoring", { diff --git a/docs/overview/examples.mdx b/docs/overview/examples.mdx deleted file mode 100644 index fc81bf413..000000000 --- a/docs/overview/examples.mdx +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: "Examples" ---- - -Got an interesting example of how would you use Keep? Feel free to submit a new example issue and we'll credit you when we add it! - - -## Create an incident only if the customer is on Enterprise tier -In this example we will utilize: - -1. Datadog for monitoring -2. OpsGenie for incident management -3. A postgres database that stores the customer tier. - -This example consists of two steps: -1. Connect your tools - Datadog, OpsGenie and Postgres. -2. Create a workflow that is triggered by the alert, runs an SQL query, and decides whether to create an incident. Once the workflow is created, you can upload it via the [Workflows](https://docs.keephq.dev/workflows/overview) page. -```yaml -alert: - id: enterprise-tier-alerts - description: Create an incident only if the customer is enterprise. - triggers: - - type: alert - filters: - - key: source - value: datadog - - key: name - value: YourAlertName - steps: - - name: check-if-customer-is-enterprise - provider: - type: postgres - config: "{{ providers.postgres-prod }}" - with: - # Keep will replace {{ alert.customer_id }} with the customer id - query: "SELECT customer_tier, customer_name FROM customers_table WHERE customer_id = {{ alert.customer_id }} LIMIT 1" - actions: - - name: opsgenie-incident - # trigger only if the customer is enterprise - condition: - - name: verify-true - type: assert - assert: "{{ steps.check-if-customer-is-enterprise.results[0] }} == 'enterprise'" - provider: - type: opsgenie - config: " {{ providers.opsgenie-prod }} " - with: - message: "A new alert on enterprise customer ( {{ steps.check-if-customer-is-enterprise.results[1] }} )" -``` - -## Send a slack message for every Cloudwatch alarm -1. Connect your Cloudwatch(/es) and Slack to Keep. -2. Create a simple Workflow that filters for CloudWatch events and sends a Slack message: -```yaml -workflow: - id: cloudwatch-slack - description: Send a slack message when a cloudwatch alarm is triggered - triggers: - - type: alert - filters: - - key: source - value: cloudwatch - actions: - - name: trigger-slack - provider: - type: slack - config: " {{ providers.slack-prod }} " - with: - message: "Got alarm from aws cloudwatch! {{ alert.name }}" - -``` - - -## Monitor a HTTP service -Suppose you want to monitor an HTTP service. -All you have to do is upload the following workflow: - -```yaml -workflow: - id: monitor-http-service - description: Monitor a HTTP service each 10 seconds - triggers: - - type: interval - value: 10 - steps: - - name: simple-http-request - provider: - type: http - with: - method: GET - url: 'https://YOUR_SERVICE_URL/' - timeout: 2 - verify: true - actions: - - name: trigger-slack - condition: - - name: assert-condition - type: assert - assert: '{{ steps.simple-http-request.results.status_code }} == 200' - provider: - type: slack - config: ' {{ providers.slack-prod }} ' - with: - message: "HTTP Request Status: {{ steps.simple-http-request.results.status_code }}\nHTTP Request Body: {{ steps.simple-http-request.results.body }}" - on-failure: - # Just need a provider we can use to send the failure reason - provider: - type: slack - config: ' {{ providers.slack-prod }} ' - -``` diff --git a/docs/platform/alerts.mdx b/docs/platform/alerts.mdx deleted file mode 100644 index 0b729e9dd..000000000 --- a/docs/platform/alerts.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: "Alerts" -sidebarTitle: Alerts ---- - -## Overview -You can manage Alerts programmatically using the Alerts API. -The alerts page let you manage your alerts in a single pane of glass. - - -## View your alerts - -By connecting Providers, you get a single pane of glass for your alerts: - - - - -## Pushed alerts - - - - -See all of the alerts that were pushed into Keep. - -## Pulled alerts - - - - -See all of the alerts that were pulled by Keep. - - -## Alert history -To see an alert history, just click on the history button: - - - - - -## Go to the original alert -You can see your alert in the origin tool by clicking on "Open Alert": - - - diff --git a/docs/platform/overview.mdx b/docs/platform/overview.mdx deleted file mode 100644 index bb3f3b809..000000000 --- a/docs/platform/overview.mdx +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: "Overview" -sidebarTitle: Overview ---- -Keep is fully open source. If you want to start Keep on your local environment, see the deployment section. -Keep is API first. Everything you do on the UI can be done via API. - -The platform is accessible on https://platform.keephq.dev and let you start the journey of improving your alerts. - -The platform is currently built on top of: - -1. [Providers](/providers/overview) - connect your stack to Keep. -2. [Alerts](/platform/alerts) - single pane of glass for your alerts. -3. [Workflows](/workflows/overview) - create automations on top of your alerts (or regardless). -4. [Settings](/platform/settings) - the settings page (add users, etc). diff --git a/docs/platform/settings.mdx b/docs/platform/settings.mdx deleted file mode 100644 index 6f472a977..000000000 --- a/docs/platform/settings.mdx +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: "Settings" -sidebarTitle: Settings ---- - -# Overview -Setup and configure Keep. - -## Users -Add or remove users from your tenant. - - - - - -## Webhook -View your tenant webhook settings. - - - - - -## SMTP -Configure your SMTP server to send emails. - - - - - -### Get an API Key - - - diff --git a/docs/platform/support.mdx b/docs/platform/support.mdx deleted file mode 100644 index b220d117f..000000000 --- a/docs/platform/support.mdx +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: "Support" -sidebarTitle: Support ---- - -## Overview -You can use the following methods to ask for support/help with anything related with Keep: - - - - You can use the [Keep Slack community](https://slack.keephq.dev) to get support. - - - You can use support@keephq.dev to send inquiries. - - diff --git a/docs/providers/documentation/grafana-provider.mdx b/docs/providers/documentation/grafana-provider.mdx index e559d005b..500d6b15c 100644 --- a/docs/providers/documentation/grafana-provider.mdx +++ b/docs/providers/documentation/grafana-provider.mdx @@ -4,6 +4,28 @@ description: "Grafana Provider allows either pull/push alerts from Grafana to Ke --- Grafana currently supports pulling/pushing alerts. We will add querying and notifying soon. +## Legacy vs Unified Alerting + +Keep supports both Grafana's legacy alerting system and the newer Unified Alerting system. Here are the key differences: + +### Legacy Alerting +- Uses notification channels for alert delivery +- Configured at the dashboard level +- Uses a different API endpoint (`/api/alerts` and `/api/alert-notifications`) +- Simpler setup but fewer features +- Alerts are tightly coupled with dashboard panels + +### Unified Alerting (Default from Grafana 9.0) +- Uses alert rules and contact points +- Configured centrally in the Alerting section +- Uses the newer `/api/v1/alerts` endpoint +- More powerful features including label-based routing +- Supports multiple data sources in a single alert rule + + +If you're using Grafana 8.x or earlier, or have explicitly enabled legacy alerting in newer versions, make sure to configure Keep accordingly using the legacy alerting configuration. + + ## Inputs Grafana Provider does not currently support the `notify` function. diff --git a/docs/providers/documentation/pagerduty-provider.mdx b/docs/providers/documentation/pagerduty-provider.mdx index 9e1fa5872..a786f8dc1 100644 --- a/docs/providers/documentation/pagerduty-provider.mdx +++ b/docs/providers/documentation/pagerduty-provider.mdx @@ -29,7 +29,7 @@ PagerDuty supports two authentication methods: To connect Keep to PagerDuty: -- **Routing Key**: Use for event posting via the PagerDuty Events API. +- **Routing Key**: Use for event posting via the PagerDuty Events API. In the PagerDuty UI, this is displayed as the integration key. - **API Key**: Use for incident creation and management through the PagerDuty Incidents API. - **Service Id** (Optional): If provided, keep operates within the service's scope. - **OAuth2**: Token management handled automatically by Keep. @@ -39,7 +39,7 @@ To connect Keep to PagerDuty: -You can find your integration key or routing key in the PagerDuty web app under **Configuration** > **Integrations**, and select the integration you want to use. +You can find your routing key in the PagerDuty (integration key in PagerDuty UI) web app under **Services** > **Service Directory** > **Your service** > **Integrations** > **Expand Events API**, and select the integration you want to use. You can find your API key in the PagerDuty web app under **Configuration** > **API Access**. The routing_key is used to post events to PagerDuty using the events API. diff --git a/docs/providers/getting-started.mdx b/docs/providers/getting-started.mdx deleted file mode 100644 index fb10c800d..000000000 --- a/docs/providers/getting-started.mdx +++ /dev/null @@ -1,27 +0,0 @@ ---- -Title: "Providers" -sidebarTitle: "Getting Started" -description: "We tried our best to cover all common providers." ---- - -Click [here](https://github.com/keephq/keep/issues/new?assignees=&labels=feature,provider&template=feature_request.md&title=Missing%20PROVIDER_NAME) if you feel like we're missing some and we'll do our best to add them ASAP. - -Common providers include: - - - AWS, GCP, Azure, etc. - - - Sentry, New Relic, Datadog, etc. - - - PagerDuty, OpsGenie, etc. - - - Email, Slack, Discord, Microsoft Teams, etc. - - - MySQL, Postgresql etc - - - diff --git a/docs/providers/what-is-a-provider.mdx b/docs/providers/what-is-a-provider.mdx deleted file mode 100644 index 69a404580..000000000 --- a/docs/providers/what-is-a-provider.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: "❓ What is a Provider" -sidebarTitle: "What is a Provider?" -description: "A Provider is a component of Keep that enables it to interact with third-party products. It is implemented as extensible Python code, making it easy to enhance and customize." ---- - -Providers are core components of Keep that allow Keep to either query data or send notifications to products such as Datadog, Cloudwatch, and Sentry for data querying, and Slack, Email, and PagerDuty for sending notifications about alerts. - -By leveraging Keep Providers, developers are able to integrate Keep with the tools they use and trust, providing them with a flexible and powerful way to manage their alerts. - -![](/images/providers.png) diff --git a/docs/workflows/syntax/state.mdx b/docs/workflows/syntax/state.mdx deleted file mode 100644 index d089c612c..000000000 --- a/docs/workflows/syntax/state.mdx +++ /dev/null @@ -1,3 +0,0 @@ ---- -title: "State" ---- diff --git a/ee/identitymanager/identity_managers/keycloak/keycloak_authverifier.py b/ee/identitymanager/identity_managers/keycloak/keycloak_authverifier.py index d38af7f50..3b974d521 100644 --- a/ee/identitymanager/identity_managers/keycloak/keycloak_authverifier.py +++ b/ee/identitymanager/identity_managers/keycloak/keycloak_authverifier.py @@ -21,6 +21,9 @@ def __init__(self, scopes: list[str] = []) -> None: self.keycloak_realm = os.environ.get("KEYCLOAK_REALM") self.keycloak_client_id = os.environ.get("KEYCLOAK_CLIENT_ID") self.keycloak_audience = os.environ.get("KEYCLOAK_AUDIENCE") + self.keycloak_verify_cert = ( + os.environ.get("KEYCLOAK_VERIFY_CERT", "true").lower() == "true" + ) if ( not self.keycloak_url or not self.keycloak_realm @@ -35,12 +38,14 @@ def __init__(self, scopes: list[str] = []) -> None: realm_name=self.keycloak_realm, client_id=self.keycloak_client_id, client_secret_key=os.environ.get("KEYCLOAK_CLIENT_SECRET"), + verify=self.keycloak_verify_cert, ) self.keycloak_openid_connection = KeycloakOpenIDConnection( server_url=self.keycloak_url, realm_name=self.keycloak_realm, client_id=self.keycloak_client_id, client_secret_key=os.environ.get("KEYCLOAK_CLIENT_SECRET"), + verify=self.keycloak_verify_cert, ) self.keycloak_uma = KeycloakUMA(connection=self.keycloak_openid_connection) # will be populated in on_start of the identity manager diff --git a/ee/identitymanager/identity_managers/keycloak/keycloak_identitymanager.py b/ee/identitymanager/identity_managers/keycloak/keycloak_identitymanager.py index 0fa6c9b14..e3f774762 100644 --- a/ee/identitymanager/identity_managers/keycloak/keycloak_identitymanager.py +++ b/ee/identitymanager/identity_managers/keycloak/keycloak_identitymanager.py @@ -45,13 +45,16 @@ class KeycloakIdentityManager(BaseIdentityManager): def __init__(self, tenant_id, context_manager: ContextManager, **kwargs): super().__init__(tenant_id, context_manager, **kwargs) self.server_url = os.environ.get("KEYCLOAK_URL") + self.keycloak_verify_cert = ( + os.environ.get("KEYCLOAK_VERIFY_CERT", "true").lower() == "true" + ) try: self.keycloak_admin = KeycloakAdmin( server_url=os.environ["KEYCLOAK_URL"] + "/admin", username=os.environ.get("KEYCLOAK_ADMIN_USER"), password=os.environ.get("KEYCLOAK_ADMIN_PASSWORD"), realm_name=os.environ["KEYCLOAK_REALM"], - verify=True, + verify=self.keycloak_verify_cert, ) self.client_id = self.keycloak_admin.get_client_id( os.environ["KEYCLOAK_CLIENT_ID"] @@ -61,6 +64,7 @@ def __init__(self, tenant_id, context_manager: ContextManager, **kwargs): client_id=os.environ["KEYCLOAK_CLIENT_ID"], realm_name=os.environ["KEYCLOAK_REALM"], client_secret_key=os.environ["KEYCLOAK_CLIENT_SECRET"], + verify=self.keycloak_verify_cert, ) self.admin_url = f'{os.environ["KEYCLOAK_URL"]}/admin/realms/{os.environ["KEYCLOAK_REALM"]}/clients/{self.client_id}' diff --git a/examples/workflows/bigquery.yml b/examples/workflows/bigquery.yml index 10ea244c8..cae9e2b4b 100644 --- a/examples/workflows/bigquery.yml +++ b/examples/workflows/bigquery.yml @@ -1,5 +1,7 @@ -alert: +workflow: id: bq-sql-query + triggers: + - type: manual description: Monitor that time difference is no more than 1 hour steps: - name: get-max-datetime @@ -12,63 +14,7 @@ alert: - name: runbook-step1-bigquery-sql provider: type: bigquery - config: "{{ providers.bigquery-prod }}" + config: "{{ providers.bigquery }}" with: # Get max(datetime) from the random table query: "SELECT * FROM `bigquery-public-data.austin_bikeshare.bikeshare_stations` LIMIT 10" - actions: - - name: opsgenie-alert - condition: - - name: threshold-condition - type: threshold - # datetime_compare(t1, t2) compares t1-t2 and returns the diff in hours - # utcnow() returns the local machine datetime in UTC - # to_utc() converts a datetime to UTC - value: keep.datetime_compare(keep.utcnow(), keep.to_utc("{{ steps.get-max-datetime.results[0][date] }}")) - compare_to: 1 # hours - compare_type: gt # greater than - # Give it an alias so we can use it in the slack action - alias: A - provider: - type: opsgenie - config: " {{ providers.opsgenie-prod }} " - with: - message: "DB datetime value ({{ actions.opsgenie-alert.conditions.threshold-condition.0.compare_value }}) is greater than 1! 🚨" - - name: trigger-slack - if: "{{ A }}" - provider: - type: slack - config: " {{ providers.slack-prod }} " - with: - message: "DB datetime value ({{ actions.opsgenie-alert.conditions.threshold-condition.0.compare_value }}) is greater than 1! 🚨" - - name: trigger-slack-2 - if: "{{ A }}" - provider: - type: slack - config: " {{ providers.slack-prod }} " - with: - blocks: - - type: header - text: - type: plain_text - text: "Adding some context to the alert:" - emoji: true - - type: section - text: - type: mrkdwn - text: |- - {{#steps.runbook-step1-bigquery-sql.results}} - - Station id: {{station_id}} | Status: {{status}} - {{/steps.runbook-step1-bigquery-sql.results}} - - -providers: - bigquery-prod: - description: BigQuery Prod - authentication: - opsgenie-prod: - authentication: - api_key: "{{ env.OPSGENIE_API_KEY }}" - slack-prod: - authentication: - webhook_url: "{{ env.SLACKDEMO_WEBHOOK }}" diff --git a/examples/workflows/ifelse.yml b/examples/workflows/ifelse.yml new file mode 100644 index 000000000..461d2df8c --- /dev/null +++ b/examples/workflows/ifelse.yml @@ -0,0 +1,53 @@ +workflow: + id: alert-routing-policy + description: Route alerts based on team and environment conditions + triggers: + - type: alert + actions: + - name: business-hours-check + if: "keep.is_business_hours(timezone='America/New_York')" + # stop the workflow if it's business hours + continue: false + provider: + type: mock + with: + message: "Alert during business hours, exiting" + + - name: infra-prod-slack + if: "'{{ alert.team }}' == 'infra' and '{{ alert.env }}' == 'prod'" + provider: + type: console + with: + channel: prod-infra-alerts + message: | + "Infrastructure Production Alert + Team: {{ alert.team }} + Environment: {{ alert.env }} + Description: {{ alert.description }}" + + - name: http-api-errors-slack + if: "'{{ alert.monitor_name }}' == 'Http API Errors'" + provider: + type: console + with: + channel: backend-team-alerts + message: | + "HTTP API Error Alert + Monitor: {{ alert.monitor_name }} + Description: {{ alert.description }}" + # exit after sending http api error alert + continue: false + + - name: backend-staging-pagerduty + if: "'{{ alert.team }}'== 'backend' and '{{ alert.env }}' == 'staging'" + provider: + type: console + with: + severity: low + message: | + "Backend Staging Alert + Team: {{ alert.team }} + Environment: {{ alert.env }} + Description: {{ alert.description }}" + # Exit after sending staging alert + continue: false diff --git a/grafana/dashboards/keep.json b/grafana/dashboards/keep.json new file mode 100644 index 000000000..a94725b1f --- /dev/null +++ b/grafana/dashboards/keep.json @@ -0,0 +1,737 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": ["mean", "max"], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "Request Duration by Endpoint", + "type": "timeseries", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "rate(keep_http_request_duration_seconds_sum{handler!=\"none\"}[5m]) / rate(keep_http_request_duration_seconds_count{handler!=\"none\"}[5m])", + "legendFormat": "{{handler}}" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 2, + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "10.0.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "sum(keep_running_tasks_current)", + "refId": "A" + } + ], + "title": "Running Tasks", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "sum(rate(keep_http_requests_total{status=~\"2..\"}[5m])) by (handler)", + "legendFormat": "{{handler}}", + "refId": "A" + } + ], + "title": "Request Rate by Endpoint (2xx)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "rate(keep_events_in_total[5m])", + "legendFormat": "Events In", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "rate(keep_events_processed_total[5m])", + "legendFormat": "Events Processed", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "rate(keep_events_error_total[5m])", + "legendFormat": "Events Error", + "refId": "C" + } + ], + "title": "Events Processing Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 5, + "title": "Workflow Execution Duration", + "type": "timeseries", + "targets": [ + { + "expr": "rate(keep_workflows_execution_duration_seconds_sum[5m]) / rate(keep_workflows_execution_duration_seconds_count[5m])", + "legendFormat": "{{workflow_id}}" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 5 + }, + { + "color": "red", + "value": 10 + } + ] + } + } + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "id": 6, + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "title": "Workflow Queue Size", + "type": "gauge", + "targets": [ + { + "expr": "keep_workflows_queue_size", + "legendFormat": "{{tenant_id}}" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + } + } + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 32 + }, + "id": 7, + "title": "Workflow Executions", + "type": "timeseries", + "targets": [ + { + "expr": "rate(keep_workflows_executions_total[5m])", + "legendFormat": "{{workflow_id}} ({{trigger_type}})" + } + ] + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 8, + "options": { + "legend": { + "calcs": ["lastNotNull"], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "keep_events_in_total", + "legendFormat": "Total Events In", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "keep_events_processed_total", + "legendFormat": "Total Events Processed", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "keep_events_error_total", + "legendFormat": "Total Events Error", + "refId": "C" + } + ], + "title": "Total Events", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "id": 9, + "options": { + "legend": { + "calcs": ["lastNotNull"], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "keep_workflows_executions_total", + "legendFormat": "{{workflow_id}} ({{trigger_type}})", + "refId": "A" + } + ], + "title": "Total Workflow Executions", + "type": "timeseries" + } + ], + "refresh": "5s", + "schemaVersion": 38, + "style": "dark", + "tags": ["keep"], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Keep Dashboard", + "uid": "keep", + "version": 1, + "weekStart": "" +} diff --git a/grafana/provisioning/dashboards/keep.yml b/grafana/provisioning/dashboards/keep.yml new file mode 100644 index 000000000..6213d6185 --- /dev/null +++ b/grafana/provisioning/dashboards/keep.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: "Keep" + orgId: 1 + folder: "" + type: file + disableDeletion: false + editable: true + options: + path: /etc/grafana/dashboards diff --git a/grafana/provisioning/datasources/prometheus.yml b/grafana/provisioning/datasources/prometheus.yml new file mode 100644 index 000000000..a221c3c37 --- /dev/null +++ b/grafana/provisioning/datasources/prometheus.yml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: + - name: prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: true diff --git a/keep-ui/app/(keep)/alerts/ColumnSelection.tsx b/keep-ui/app/(keep)/alerts/ColumnSelection.tsx index a65088e77..3e70e461b 100644 --- a/keep-ui/app/(keep)/alerts/ColumnSelection.tsx +++ b/keep-ui/app/(keep)/alerts/ColumnSelection.tsx @@ -7,7 +7,7 @@ import { FloatingArrow, arrow, offset, useFloating } from "@floating-ui/react"; import { Popover } from "@headlessui/react"; import { FiSettings, FiSearch } from "react-icons/fi"; import { DEFAULT_COLS, DEFAULT_COLS_VISIBILITY } from "./alert-table-utils"; -import { AlertDto } from "./models"; +import { AlertDto } from "@/entities/alerts/model"; interface AlertColumnsSelectProps { table: Table; diff --git a/keep-ui/app/(keep)/alerts/EnrichAlertSidePanel.tsx b/keep-ui/app/(keep)/alerts/EnrichAlertSidePanel.tsx index 21cb7749c..0f8d401a3 100644 --- a/keep-ui/app/(keep)/alerts/EnrichAlertSidePanel.tsx +++ b/keep-ui/app/(keep)/alerts/EnrichAlertSidePanel.tsx @@ -1,10 +1,9 @@ -import { AlertDto } from "./models"; -import { Dialog, Transition } from "@headlessui/react"; -import React, { Fragment, useEffect, useState } from "react"; +import { AlertDto } from "@/entities/alerts/model"; +import React, { useEffect, useState } from "react"; import { Button, TextInput } from "@tremor/react"; import { toast } from "react-toastify"; import SidePanel from "@/components/SidePanel"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; import { useApi } from "@/shared/lib/hooks/useApi"; interface EnrichAlertModalProps { diff --git a/keep-ui/app/(keep)/alerts/ThemeSelection.tsx b/keep-ui/app/(keep)/alerts/ThemeSelection.tsx index f9058dddd..a15b02375 100644 --- a/keep-ui/app/(keep)/alerts/ThemeSelection.tsx +++ b/keep-ui/app/(keep)/alerts/ThemeSelection.tsx @@ -1,4 +1,4 @@ -import React, { useState, Fragment, useRef, FormEvent } from "react"; +import React, { useState, Fragment, useRef } from "react"; import { Popover } from "@headlessui/react"; import { Button, diff --git a/keep-ui/app/(keep)/alerts/TitleAndFilters.tsx b/keep-ui/app/(keep)/alerts/TitleAndFilters.tsx index d1a8849a0..3cccdb035 100644 --- a/keep-ui/app/(keep)/alerts/TitleAndFilters.tsx +++ b/keep-ui/app/(keep)/alerts/TitleAndFilters.tsx @@ -1,6 +1,6 @@ import { Table } from "@tanstack/react-table"; -import { DateRangePicker, DateRangePickerValue, Title } from "@tremor/react"; -import { AlertDto } from "./models"; +import { Title } from "@tremor/react"; +import { AlertDto } from "@/entities/alerts/model"; import ColumnSelection from "./ColumnSelection"; import { ThemeSelection } from "./ThemeSelection"; import EnhancedDateRangePicker from "@/components/ui/DateRangePicker"; diff --git a/keep-ui/app/(keep)/alerts/ViewAlertModal.tsx b/keep-ui/app/(keep)/alerts/ViewAlertModal.tsx index 0f5bca82c..9bda67c80 100644 --- a/keep-ui/app/(keep)/alerts/ViewAlertModal.tsx +++ b/keep-ui/app/(keep)/alerts/ViewAlertModal.tsx @@ -1,4 +1,4 @@ -import { AlertDto } from "./models"; // Adjust the import path as needed +import { AlertDto } from "@/entities/alerts/model"; // Adjust the import path as needed import Modal from "@/components/ui/Modal"; // Ensure this path matches your project structure import { Button, Icon, Switch, Text } from "@tremor/react"; import { toast } from "react-toastify"; @@ -6,7 +6,7 @@ import { XMarkIcon } from "@heroicons/react/24/outline"; import "./ViewAlertModal.css"; import React, { useState } from "react"; import { useApi } from "@/shared/lib/hooks/useApi"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; interface ViewAlertModalProps { alert: AlertDto | null | undefined; diff --git a/keep-ui/app/(keep)/alerts/alert-actions.tsx b/keep-ui/app/(keep)/alerts/alert-actions.tsx index ccbba1398..551278362 100644 --- a/keep-ui/app/(keep)/alerts/alert-actions.tsx +++ b/keep-ui/app/(keep)/alerts/alert-actions.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { Button } from "@tremor/react"; -import { AlertDto } from "./models"; +import { AlertDto } from "@/entities/alerts/model"; import { PlusIcon, RocketIcon } from "@radix-ui/react-icons"; import { toast } from "react-toastify"; import { useRouter } from "next/navigation"; diff --git a/keep-ui/app/(keep)/alerts/alert-assign-ticket-modal.tsx b/keep-ui/app/(keep)/alerts/alert-assign-ticket-modal.tsx index 0a2bda534..f9f6711ea 100644 --- a/keep-ui/app/(keep)/alerts/alert-assign-ticket-modal.tsx +++ b/keep-ui/app/(keep)/alerts/alert-assign-ticket-modal.tsx @@ -1,10 +1,10 @@ -import React, { useState } from "react"; +import React from "react"; import Select, { components } from "react-select"; import { Button, TextInput, Text, Icon } from "@tremor/react"; import { PlusIcon } from "@heroicons/react/20/solid"; import { useForm, Controller, SubmitHandler } from "react-hook-form"; import { Providers } from "../providers/providers"; -import { AlertDto } from "./models"; +import { AlertDto } from "@/entities/alerts/model"; import Modal from "@/components/ui/Modal"; import { useApi } from "@/shared/lib/hooks/useApi"; import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline"; diff --git a/keep-ui/app/(keep)/alerts/alert-associate-incident-modal.tsx b/keep-ui/app/(keep)/alerts/alert-associate-incident-modal.tsx index fb1d8a8bc..c98142ffb 100644 --- a/keep-ui/app/(keep)/alerts/alert-associate-incident-modal.tsx +++ b/keep-ui/app/(keep)/alerts/alert-associate-incident-modal.tsx @@ -1,6 +1,5 @@ import Modal from "@/components/ui/Modal"; import { Button, Divider, Title } from "@tremor/react"; -import Select from "@/components/ui/Select"; import { CreateOrUpdateIncidentForm } from "@/features/create-or-update-incident"; import { FormEvent, useCallback, useEffect, useState } from "react"; import { toast } from "react-toastify"; @@ -9,10 +8,10 @@ import { usePollIncidents, } from "../../../utils/hooks/useIncidents"; import Loading from "@/app/(keep)/loading"; -import { AlertDto } from "./models"; +import { AlertDto } from "@/entities/alerts/model"; import { getIncidentName } from "@/entities/incidents/lib/utils"; import { useApi } from "@/shared/lib/hooks/useApi"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { Select, showErrorToast } from "@/shared/ui"; interface AlertAssociateIncidentModalProps { isOpen: boolean; diff --git a/keep-ui/app/(keep)/alerts/alert-change-status-modal.tsx b/keep-ui/app/(keep)/alerts/alert-change-status-modal.tsx index 7218af97e..6ce00212d 100644 --- a/keep-ui/app/(keep)/alerts/alert-change-status-modal.tsx +++ b/keep-ui/app/(keep)/alerts/alert-change-status-modal.tsx @@ -1,13 +1,7 @@ import { Button, Title, Subtitle } from "@tremor/react"; import Modal from "@/components/ui/Modal"; -import Select, { - CSSObjectWithLabel, - ControlProps, - OptionProps, - GroupBase, -} from "react-select"; import { useState } from "react"; -import { AlertDto, Status } from "./models"; +import { AlertDto, Status } from "@/entities/alerts/model"; import { toast } from "react-toastify"; import { CheckCircleIcon, @@ -18,7 +12,7 @@ import { } from "@heroicons/react/24/outline"; import { useAlerts } from "utils/hooks/useAlerts"; import { useApi } from "@/shared/lib/hooks/useApi"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { Select, showErrorToast } from "@/shared/ui"; import { useRevalidateMultiple } from "@/shared/lib/state-utils"; @@ -30,40 +24,6 @@ const statusIcons = { [Status.Pending]: , }; -const customSelectStyles = { - control: ( - base: CSSObjectWithLabel, - state: ControlProps< - { value: Status; label: JSX.Element }, - false, - GroupBase<{ value: Status; label: JSX.Element }> - > - ) => ({ - ...base, - borderColor: state.isFocused ? "orange" : base.borderColor, - boxShadow: state.isFocused ? "0 0 0 1px orange" : base.boxShadow, - "&:hover": { - borderColor: "orange", - }, - }), - option: ( - base: CSSObjectWithLabel, - { - isFocused, - }: OptionProps< - { value: Status; label: JSX.Element }, - false, - GroupBase<{ value: Status; label: JSX.Element }> - > - ) => ({ - ...base, - backgroundColor: isFocused ? "rgba(255,165,0,0.1)" : base.backgroundColor, - "&:hover": { - backgroundColor: "rgba(255,165,0,0.2)", - }, - }), -}; - interface Props { alert: AlertDto | null | undefined; handleClose: () => void; @@ -147,7 +107,6 @@ export default function AlertChangeStatusModal({ onChange={(option) => setSelectedStatus(option?.value || null)} placeholder="Select new status" className="ml-2" - styles={customSelectStyles} /> diff --git a/keep-ui/app/(keep)/alerts/alert-create-incident-ai-card.tsx b/keep-ui/app/(keep)/alerts/alert-create-incident-ai-card.tsx index 32aea9b75..eadd378f7 100644 --- a/keep-ui/app/(keep)/alerts/alert-create-incident-ai-card.tsx +++ b/keep-ui/app/(keep)/alerts/alert-create-incident-ai-card.tsx @@ -16,7 +16,7 @@ import { Textarea, } from "@tremor/react"; import { Droppable, Draggable } from "react-beautiful-dnd"; -import { AlertDto } from "./models"; +import { AlertDto } from "@/entities/alerts/model"; import { IncidentCandidateDto } from "@/entities/incidents/model"; interface IncidentCardProps { diff --git a/keep-ui/app/(keep)/alerts/alert-create-incident-ai-modal.tsx b/keep-ui/app/(keep)/alerts/alert-create-incident-ai-modal.tsx index 6b4444354..5f55ab300 100644 --- a/keep-ui/app/(keep)/alerts/alert-create-incident-ai-modal.tsx +++ b/keep-ui/app/(keep)/alerts/alert-create-incident-ai-modal.tsx @@ -3,7 +3,7 @@ import Modal from "@/components/ui/Modal"; import { Callout, Button, Title, Card } from "@tremor/react"; import { toast } from "react-toastify"; import Loading from "@/app/(keep)/loading"; -import { AlertDto } from "./models"; +import { AlertDto } from "@/entities/alerts/model"; import { IncidentCandidateDto } from "@/entities/incidents/model"; import { DragDropContext } from "react-beautiful-dnd"; import IncidentCard from "./alert-create-incident-ai-card"; diff --git a/keep-ui/app/(keep)/alerts/alert-dismiss-modal.tsx b/keep-ui/app/(keep)/alerts/alert-dismiss-modal.tsx index 3b161f628..912c40d43 100644 --- a/keep-ui/app/(keep)/alerts/alert-dismiss-modal.tsx +++ b/keep-ui/app/(keep)/alerts/alert-dismiss-modal.tsx @@ -14,7 +14,7 @@ import Modal from "@/components/ui/Modal"; import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; import "react-quill/dist/quill.snow.css"; -import { AlertDto } from "./models"; +import { AlertDto } from "@/entities/alerts/model"; import { set, isSameDay, isAfter } from "date-fns"; import { useAlerts } from "utils/hooks/useAlerts"; import { toast } from "react-toastify"; @@ -22,7 +22,7 @@ const ReactQuill = typeof window === "object" ? require("react-quill") : () => false; import "./alert-dismiss-modal.css"; import { useApi } from "@/shared/lib/hooks/useApi"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; import { useRevalidateMultiple } from "@/shared/lib/state-utils"; diff --git a/keep-ui/app/(keep)/alerts/alert-extra-payload.tsx b/keep-ui/app/(keep)/alerts/alert-extra-payload.tsx index 78b68ea1c..6d0d3cc2f 100644 --- a/keep-ui/app/(keep)/alerts/alert-extra-payload.tsx +++ b/keep-ui/app/(keep)/alerts/alert-extra-payload.tsx @@ -1,6 +1,5 @@ import { Accordion, AccordionBody, AccordionHeader } from "@tremor/react"; -import { AlertDto, AlertKnownKeys } from "./models"; -import { useEffect, useRef, useState } from "react"; +import { AlertDto, AlertKnownKeys } from "@/entities/alerts/model"; export const getExtraPayloadNoKnownKeys = (alert: AlertDto) => { const extraPayload = Object.entries(alert).filter( diff --git a/keep-ui/app/(keep)/alerts/alert-fatigue-meter.tsx b/keep-ui/app/(keep)/alerts/alert-fatigue-meter.tsx index b528e365d..3dde1b92a 100644 --- a/keep-ui/app/(keep)/alerts/alert-fatigue-meter.tsx +++ b/keep-ui/app/(keep)/alerts/alert-fatigue-meter.tsx @@ -1,7 +1,7 @@ import { CategoryBar } from "@tremor/react"; -import { AlertDto } from "./models"; +import { AlertDto } from "@/entities/alerts/model"; import { calculateFatigue } from "utils/fatigue"; -import { useEffect, useMemo, useState } from "react"; +import { useMemo } from "react"; const oneHourAgo = new Date().getTime() - 60 * 60 * 1000; // Current time - 1 hour diff --git a/keep-ui/app/(keep)/alerts/alert-history-charts.tsx b/keep-ui/app/(keep)/alerts/alert-history-charts.tsx index b79e37e65..798996039 100644 --- a/keep-ui/app/(keep)/alerts/alert-history-charts.tsx +++ b/keep-ui/app/(keep)/alerts/alert-history-charts.tsx @@ -1,6 +1,6 @@ import { AreaChart } from "@tremor/react"; import Loading from "@/app/(keep)/loading"; -import { AlertDto } from "./models"; +import { AlertDto } from "@/entities/alerts/model"; import { calculateFatigue } from "utils/fatigue"; interface Props { diff --git a/keep-ui/app/(keep)/alerts/alert-history.tsx b/keep-ui/app/(keep)/alerts/alert-history.tsx index 7869df473..948d24306 100644 --- a/keep-ui/app/(keep)/alerts/alert-history.tsx +++ b/keep-ui/app/(keep)/alerts/alert-history.tsx @@ -1,5 +1,5 @@ import { Fragment } from "react"; -import { AlertDto } from "./models"; +import { AlertDto } from "@/entities/alerts/model"; import { AlertTable } from "./alert-table"; import { useAlertTableCols } from "./alert-table-utils"; import { Button, Flex, Subtitle, Title, Divider } from "@tremor/react"; diff --git a/keep-ui/app/(keep)/alerts/alert-menu.tsx b/keep-ui/app/(keep)/alerts/alert-menu.tsx index 7273f38f1..a7bf35fe6 100644 --- a/keep-ui/app/(keep)/alerts/alert-menu.tsx +++ b/keep-ui/app/(keep)/alerts/alert-menu.tsx @@ -1,30 +1,27 @@ -import { Menu, Portal, Transition } from "@headlessui/react"; -import { Fragment, useEffect } from "react"; -import { Icon } from "@tremor/react"; +import { Menu } from "@headlessui/react"; +import { useCallback, useMemo } from "react"; import { ChevronDoubleRightIcon, ArchiveBoxIcon, - EllipsisHorizontalIcon, PlusIcon, UserPlusIcon, PlayIcon, EyeIcon, AdjustmentsHorizontalIcon, } from "@heroicons/react/24/outline"; +import { EllipsisHorizontalIcon } from "@heroicons/react/20/solid"; import { IoNotificationsOffOutline } from "react-icons/io5"; -import Link from "next/link"; import { ProviderMethod } from "@/app/(keep)/providers/providers"; -import { AlertDto } from "./models"; -import { useFloating } from "@floating-ui/react"; +import { AlertDto } from "@/entities/alerts/model"; import { useProviders } from "utils/hooks/useProviders"; import { useAlerts } from "utils/hooks/useAlerts"; import { useRouter } from "next/navigation"; import { useApi } from "@/shared/lib/hooks/useApi"; +import { DropdownMenu } from "@/shared/ui"; +import { ElementType } from "react"; interface Props { alert: AlertDto; - isMenuOpen?: boolean; - setIsMenuOpen?: (key: string) => void; setRunWorkflowModalAlert?: (alert: AlertDto) => void; setDismissModalAlert?: (alert: AlertDto[]) => void; setChangeStatusAlert?: (alert: AlertDto) => void; @@ -33,10 +30,35 @@ interface Props { setIsIncidentSelectorOpen?: (open: boolean) => void; } +interface MenuItem { + icon: ElementType; + label: string; + onClick: () => void; + disabled?: boolean; + show?: boolean; +} + +function DynamicIcon({ providerType }: { providerType: string }) { + return ( + + + + ); +} + export default function AlertMenu({ alert, - isMenuOpen, - setIsMenuOpen, setRunWorkflowModalAlert, setDismissModalAlert, setChangeStatusAlert, @@ -55,347 +77,195 @@ export default function AlertMenu({ const { usePresetAlerts } = useAlerts(); const { mutate } = usePresetAlerts(presetName, { revalidateOnMount: false }); - const { refs, x, y } = useFloating(); - - const alertName = alert.name; const fingerprint = alert.fingerprint; - const alertSource = alert.source![0]; const provider = installedProviders.find((p) => p.type === alert.source[0]); - const DynamicIcon = (props: any) => ( - - {" "} - - - ); - - const onDismiss = async () => { + const onDismiss = useCallback(async () => { setDismissModalAlert?.([alert]); - }; + }, [alert, setDismissModalAlert]); - const callAssignEndpoint = async (unassign: boolean = false) => { - if ( - confirm( - "After assigning this alert to yourself, you won't be able to unassign it until someone else assigns it to himself. Are you sure you want to continue?" - ) - ) { - const res = await api.post( - `/alerts/${fingerprint}/assign/${alert.lastReceived.toISOString()}` - ); - if (res.ok) { - await mutate(); + const callAssignEndpoint = useCallback( + async (unassign: boolean = false) => { + if ( + confirm( + "After assigning this alert to yourself, you won't be able to unassign it until someone else assigns it to himself. Are you sure you want to continue?" + ) + ) { + const res = await api.post( + `/alerts/${fingerprint}/assign/${alert.lastReceived.toISOString()}` + ); + if (res.ok) { + await mutate(); + } } - } - }; + }, + [alert, fingerprint, api, mutate] + ); - const isMethodEnabled = (method: ProviderMethod) => { - if (provider) { - return method.scopes.every( - (scope) => provider.validatedScopes[scope] === true - ); - } + const isMethodEnabled = useCallback( + (method: ProviderMethod) => { + if (provider) { + return method.scopes.every( + (scope) => provider.validatedScopes[scope] === true + ); + } - return false; - }; + return false; + }, + [provider] + ); - const openMethodModal = (method: ProviderMethod) => { - router.replace( - `/alerts/${presetName}?methodName=${method.name}&providerId=${ - provider!.id - }&alertFingerprint=${alert.fingerprint}`, - { - scroll: false, - } - ); - handleCloseMenu(); - }; + const openMethodModal = useCallback( + (method: ProviderMethod) => { + router.replace( + `/alerts/${presetName}?methodName=${method.name}&providerId=${ + provider!.id + }&alertFingerprint=${alert.fingerprint}`, + { + scroll: false, + } + ); + }, + [alert, presetName, provider, router] + ); - const openAlertPayloadModal = () => { + const openAlertPayloadModal = useCallback(() => { router.replace( `/alerts/${presetName}?alertPayloadFingerprint=${alert.fingerprint}`, { scroll: false, } ); - handleCloseMenu(); - }; + }, [alert, presetName, router]); const canAssign = true; // TODO: keep track of assignments for auditing - const handleMenuToggle = () => { - setIsMenuOpen!(alert.fingerprint); - }; - - const handleCloseMenu = () => { - setIsMenuOpen!(""); - }; - - useEffect(() => { - const rowElement = document.getElementById(`alert-row-${fingerprint}`); - if (rowElement) { - if (isMenuOpen) { - rowElement.classList.add("menu-open"); - } else { - rowElement.classList.remove("menu-open"); - } - } - }, [isMenuOpen, fingerprint]); + const menuItems = useMemo( + () => [ + { + icon: PlayIcon, + label: "Run Workflow", + onClick: () => setRunWorkflowModalAlert?.(alert), + }, + { + icon: PlusIcon, + label: "Workflow", + onClick: () => + router.push( + `/workflows/builder?alertName=${encodeURIComponent(alert.name)}&alertSource=${alert.source![0]}` + ), + show: !isInSidebar, + }, + { + icon: ArchiveBoxIcon, + label: "History", + onClick: () => + router.replace( + `/alerts/${presetName}?fingerprint=${alert.fingerprint}`, + { scroll: false } + ), + }, + { + icon: AdjustmentsHorizontalIcon, + label: "Enrich", + onClick: () => + router.replace( + `/alerts/${presetName}?alertPayloadFingerprint=${alert.fingerprint}&enrich=true` + ), + }, + { + icon: UserPlusIcon, + label: "Self-Assign", + onClick: () => callAssignEndpoint(), + show: canAssign, + }, + { + icon: EyeIcon, + label: "View Alert", + onClick: openAlertPayloadModal, + }, + ...(provider?.methods?.map((method) => ({ + icon: DynamicIcon, + label: method.name, + onClick: () => openMethodModal(method), + disabled: !isMethodEnabled(method), + })) ?? []), + { + icon: IoNotificationsOffOutline, + label: alert.dismissed ? "Restore" : "Dismiss", + onClick: onDismiss, + }, + { + icon: ChevronDoubleRightIcon, + label: "Change Status", + onClick: () => setChangeStatusAlert?.(alert), + }, + { + icon: PlusIcon, + label: "Correlate Incident", + onClick: () => setIsIncidentSelectorOpen?.(true), + show: !!setIsIncidentSelectorOpen, + }, + ], + [ + isInSidebar, + canAssign, + openAlertPayloadModal, + provider?.methods, + alert, + onDismiss, + setIsIncidentSelectorOpen, + setRunWorkflowModalAlert, + router, + presetName, + callAssignEndpoint, + isMethodEnabled, + openMethodModal, + setChangeStatusAlert, + ] + ); - const menuItems = ( - <> - - {({ active }) => ( - - )} - - {!isInSidebar && ( - - {({ active }) => ( - - - )} + const visibleMenuItems = useMemo( + () => menuItems.filter((item) => item.show !== false), + [menuItems] + ); - - {({ active }) => ( - - )} - - - {({ active }) => ( - - )} - - {canAssign && ( - - {({ active }) => ( - - )} - - )} - - {({ active }) => ( - - )} - - {provider?.methods && provider.methods.length > 0 && ( -
- {provider.methods.map((method) => { - const methodEnabled = isMethodEnabled(method); + if (isInSidebar) { + // For sidebar we want to show the menu items in a horizontal scrollable menu + return ( + +
+ {visibleMenuItems.map((item, index) => { + const Icon = item.icon; return ( - - {({ active }) => ( - - )} - + ); })}
- )} - - {({ active }) => ( - - )} - - - {({ active }) => ( - - )} - - {setIsIncidentSelectorOpen && ( - - {({ active }) => ( - - )} - - )} - - ); +
+ ); + } return ( - <> - {!isInSidebar ? ( - - - - - {isMenuOpen && ( - - - ) : ( - -
- {menuItems} -
-
- )} - + + {visibleMenuItems.map((item, index) => ( + + ))} + ); } diff --git a/keep-ui/app/(keep)/alerts/alert-method-modal.tsx b/keep-ui/app/(keep)/alerts/alert-method-modal.tsx index d535d42f8..85eee168b 100644 --- a/keep-ui/app/(keep)/alerts/alert-method-modal.tsx +++ b/keep-ui/app/(keep)/alerts/alert-method-modal.tsx @@ -21,7 +21,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useProviders } from "utils/hooks/useProviders"; import Modal from "@/components/ui/Modal"; import { useApi } from "@/shared/lib/hooks/useApi"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; const supportedParamTypes = ["datetime", "literal", "str"]; diff --git a/keep-ui/app/(keep)/alerts/alert-note-modal.tsx b/keep-ui/app/(keep)/alerts/alert-note-modal.tsx index ae403558b..09b57a9eb 100644 --- a/keep-ui/app/(keep)/alerts/alert-note-modal.tsx +++ b/keep-ui/app/(keep)/alerts/alert-note-modal.tsx @@ -6,10 +6,10 @@ const ReactQuill = typeof window === "object" ? require("react-quill") : () => false; import "react-quill/dist/quill.snow.css"; import { Button } from "@tremor/react"; -import { AlertDto } from "./models"; +import { AlertDto } from "@/entities/alerts/model"; import Modal from "@/components/ui/Modal"; import { useApi } from "@/shared/lib/hooks/useApi"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; interface AlertNoteModalProps { handleClose: () => void; diff --git a/keep-ui/app/(keep)/alerts/alert-pagination.tsx b/keep-ui/app/(keep)/alerts/alert-pagination.tsx index 659f4405d..2d3cb4c32 100644 --- a/keep-ui/app/(keep)/alerts/alert-pagination.tsx +++ b/keep-ui/app/(keep)/alerts/alert-pagination.tsx @@ -7,16 +7,11 @@ import { TableCellsIcon, } from "@heroicons/react/16/solid"; import { Button, Text } from "@tremor/react"; -import { - StylesConfig, - SingleValueProps, - components, - GroupBase, -} from "react-select"; -import Select from "react-select"; -import { AlertDto } from "./models"; +import { SingleValueProps, components, GroupBase } from "react-select"; +import { AlertDto } from "@/entities/alerts/model"; import { Table } from "@tanstack/react-table"; import { useAlerts } from "utils/hooks/useAlerts"; +import { Select } from "@/shared/ui"; interface Props { presetName: string; @@ -29,31 +24,6 @@ interface OptionType { label: string; } -const customStyles: StylesConfig> = { - control: (provided, state) => ({ - ...provided, - borderColor: state.isFocused ? "orange" : "rgb(229 231 235)", - borderRadius: "0.5rem", - "&:hover": { borderColor: "orange" }, - boxShadow: state.isFocused ? "0 0 0 1px orange" : provided.boxShadow, - }), - singleValue: (provided) => ({ - ...provided, - display: "flex", - alignItems: "center", - }), - menu: (provided) => ({ - ...provided, - color: "orange", - }), - option: (provided, state) => ({ - ...provided, - backgroundColor: state.isSelected ? "orange" : provided.backgroundColor, - "&:hover": { backgroundColor: state.isSelected ? "orange" : "#f5f5f5" }, - color: state.isSelected ? "white" : provided.color, - }), -}; - const SingleValue = ({ children, ...props @@ -82,7 +52,6 @@ export default function AlertPagination({
({ value: `${provider.type}_${provider.id}`, label: provider.details?.name || provider.id || "main", @@ -378,8 +391,10 @@ const DeduplicationSidebar: React.FC = ({ required: "At least one fingerprint field is required", }} render={({ field }) => ( - ({ value: fieldName, label: fieldName, @@ -416,7 +431,11 @@ const DeduplicationSidebar: React.FC = ({ name="full_deduplication" control={control} render={({ field }) => ( - + )} /> @@ -452,8 +471,10 @@ const DeduplicationSidebar: React.FC = ({ name="ignore_fields" control={control} render={({ field }) => ( - ({ value: fieldName, label: fieldName, @@ -502,7 +523,11 @@ const DeduplicationSidebar: React.FC = ({ > Cancel -
diff --git a/keep-ui/app/(keep)/deduplication/DeduplicationTable.tsx b/keep-ui/app/(keep)/deduplication/DeduplicationTable.tsx index a257ee433..909ea6ee4 100644 --- a/keep-ui/app/(keep)/deduplication/DeduplicationTable.tsx +++ b/keep-ui/app/(keep)/deduplication/DeduplicationTable.tsx @@ -13,7 +13,7 @@ import { Badge, SparkAreaChart, } from "@tremor/react"; -import { Tooltip } from "@/shared/ui/Tooltip"; +import { Tooltip } from "@/shared/ui"; import { useRouter, useSearchParams } from "next/navigation"; import { createColumnHelper, @@ -25,7 +25,6 @@ import { DeduplicationRule } from "@/app/(keep)/deduplication/models"; import DeduplicationSidebar from "@/app/(keep)/deduplication/DeduplicationSidebar"; import { TrashIcon, - PauseIcon, PlusIcon, QuestionMarkCircleIcon, } from "@heroicons/react/24/outline"; @@ -121,6 +120,18 @@ export const DeduplicationTable: React.FC = ({ "Represents the percentage of alerts successfully deduplicated. Higher values indicate better deduplication efficiency, meaning fewer redundant alerts.", }; + function resolveDeleteButtonTooltip(deduplicationRule: DeduplicationRule): string { + if (deduplicationRule.default) { + return "Cannot delete default rule"; + } + + if (deduplicationRule.is_provisioned) { + return "Cannot delete provisioned rule."; + } + + return "Delete Rule" + } + const DEDUPLICATION_TABLE_COLS = useMemo( () => [ columnHelper.accessor("provider_type", { @@ -266,11 +277,9 @@ export const DeduplicationTable: React.FC = ({ variant="secondary" icon={TrashIcon} tooltip={ - info.row.original.default - ? "Cannot delete default rule" - : "Delete Rule" + resolveDeleteButtonTooltip(info.row.original) } - disabled={info.row.original.default} + disabled={info.row.original.default || info.row.original.is_provisioned} onClick={(e) => handleDeleteRule(info.row.original, e)} />
diff --git a/keep-ui/app/(keep)/deduplication/models.tsx b/keep-ui/app/(keep)/deduplication/models.tsx index 8e5e0d3ec..a201cd513 100644 --- a/keep-ui/app/(keep)/deduplication/models.tsx +++ b/keep-ui/app/(keep)/deduplication/models.tsx @@ -17,4 +17,5 @@ export interface DeduplicationRule { // full_deduplication is true if the deduplication rule is a full deduplication rule full_deduplication: boolean; ignore_fields: string[]; + is_provisioned: boolean; } diff --git a/keep-ui/app/(keep)/extraction/create-or-update-extraction-rule.tsx b/keep-ui/app/(keep)/extraction/create-or-update-extraction-rule.tsx index 0e12afe75..bfca7040f 100644 --- a/keep-ui/app/(keep)/extraction/create-or-update-extraction-rule.tsx +++ b/keep-ui/app/(keep)/extraction/create-or-update-extraction-rule.tsx @@ -20,7 +20,7 @@ import { extractNamedGroups } from "./extractions-table"; import { useExtractions } from "utils/hooks/useExtractionRules"; import { AlertsRulesBuilder } from "@/app/(keep)/alerts/alerts-rules-builder"; import { useApi } from "@/shared/lib/hooks/useApi"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; interface Props { extractionToEdit: ExtractionRule | null; diff --git a/keep-ui/app/(keep)/extraction/extractions-table.tsx b/keep-ui/app/(keep)/extraction/extractions-table.tsx index a56d41639..78cc11d52 100644 --- a/keep-ui/app/(keep)/extraction/extractions-table.tsx +++ b/keep-ui/app/(keep)/extraction/extractions-table.tsx @@ -26,7 +26,7 @@ import { IoCheckmark } from "react-icons/io5"; import { HiMiniXMark } from "react-icons/hi2"; import { useState } from "react"; import { useApi } from "@/shared/lib/hooks/useApi"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; const columnHelper = createColumnHelper(); diff --git a/keep-ui/app/(keep)/incidents/[id]/activity/incident-activity.tsx b/keep-ui/app/(keep)/incidents/[id]/activity/incident-activity.tsx index 2249e6d1e..9cbdd9481 100644 --- a/keep-ui/app/(keep)/incidents/[id]/activity/incident-activity.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/activity/incident-activity.tsx @@ -1,6 +1,6 @@ "use client"; -import { AlertDto } from "@/app/(keep)/alerts/models"; +import { AlertDto } from "@/entities/alerts/model"; import { IncidentDto } from "@/entities/incidents/model"; import { useUsers } from "@/entities/users/model/useUsers"; import Image from "next/image"; diff --git a/keep-ui/app/(keep)/incidents/[id]/activity/ui/IncidentActivityComment.tsx b/keep-ui/app/(keep)/incidents/[id]/activity/ui/IncidentActivityComment.tsx index 19fdcdf00..149862ffb 100644 --- a/keep-ui/app/(keep)/incidents/[id]/activity/ui/IncidentActivityComment.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/activity/ui/IncidentActivityComment.tsx @@ -5,7 +5,7 @@ import { useState, useCallback, useEffect } from "react"; import { toast } from "react-toastify"; import { KeyedMutator } from "swr"; import { useApi } from "@/shared/lib/hooks/useApi"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; export function IncidentActivityComment({ incident, diff --git a/keep-ui/app/(keep)/incidents/[id]/activity/ui/IncidentActivityItem.tsx b/keep-ui/app/(keep)/incidents/[id]/activity/ui/IncidentActivityItem.tsx index 84ccdde64..fa24f3f25 100644 --- a/keep-ui/app/(keep)/incidents/[id]/activity/ui/IncidentActivityItem.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/activity/ui/IncidentActivityItem.tsx @@ -1,5 +1,5 @@ import AlertSeverity from "@/app/(keep)/alerts/alert-severity"; -import { AlertDto } from "@/app/(keep)/alerts/models"; +import { AlertDto } from "@/entities/alerts/model"; import TimeAgo from "react-timeago"; export function IncidentActivityItem({ activity }: { activity: any }) { diff --git a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-actions.tsx b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-actions.tsx new file mode 100644 index 000000000..c0c615328 --- /dev/null +++ b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-actions.tsx @@ -0,0 +1,51 @@ +import { Button } from "@/components/ui"; +import { useIncidentActions } from "@/entities/incidents/model/useIncidentActions"; +import { SplitIncidentAlertsModal } from "@/features/split-incident-alerts"; +import { useState } from "react"; +import { LiaUnlinkSolid } from "react-icons/lia"; + +export function IncidentAlertsActions({ + incidentId, + selectedFingerprints, + resetAlertsSelection, +}: { + incidentId: string; + selectedFingerprints: string[]; + resetAlertsSelection: () => void; +}) { + const [isSplitModalOpen, setIsSplitModalOpen] = useState(false); + const { unlinkAlertsFromIncident } = useIncidentActions(); + + return ( + <> +
+ + +
+ {isSplitModalOpen && ( + setIsSplitModalOpen(false)} + onSuccess={resetAlertsSelection} + /> + )} + + ); +} diff --git a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx index 0384ab7a3..e2b7fdd74 100644 --- a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-menu.tsx @@ -1,10 +1,10 @@ import { Badge } from "@tremor/react"; -import { AlertDto } from "@/app/(keep)/alerts/models"; +import { AlertDto } from "@/entities/alerts/model"; import { toast } from "react-toastify"; import { useIncidentAlerts } from "utils/hooks/useIncidents"; import { LiaUnlinkSolid } from "react-icons/lia"; import { useApi } from "@/shared/lib/hooks/useApi"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; interface Props { incidentId: string; diff --git a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-table-body-skeleton.tsx b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-table-body-skeleton.tsx new file mode 100644 index 000000000..aafb95bc6 --- /dev/null +++ b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alert-table-body-skeleton.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { TableBody, TableCell, TableRow } from "@tremor/react"; +import type { Table as ReactTable } from "@tanstack/react-table"; +import { AlertDto } from "@/entities/alerts/model"; +import { getCommonPinningStylesAndClassNames } from "@/shared/ui"; +import Skeleton from "react-loading-skeleton"; +import "react-loading-skeleton/dist/skeleton.css"; + +export function IncidentAlertsTableBodySkeleton({ + table, + pageSize, +}: { + table: ReactTable; + pageSize: number; +}) { + return ( + + {Array(pageSize) + .fill("") + .map((_, index) => ( + + {table.getVisibleFlatColumns().map((column) => { + const { style, className } = getCommonPinningStylesAndClassNames( + column, + table.getState().columnPinning.left?.length, + table.getState().columnPinning.right?.length + ); + return ( + + + + ); + })} + + ))} + + ); +} diff --git a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx index 36e78b2ee..eaafd6819 100644 --- a/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/alerts/incident-alerts.tsx @@ -6,6 +6,7 @@ import { getCoreRowModel, useReactTable, } from "@tanstack/react-table"; +import type { RowSelectionState } from "@tanstack/react-table"; import { Card, Icon, @@ -17,25 +18,29 @@ import { TableRow, } from "@tremor/react"; import Image from "next/image"; -import { AlertDto } from "@/app/(keep)/alerts/models"; -import Skeleton from "react-loading-skeleton"; -import "react-loading-skeleton/dist/skeleton.css"; +import { AlertDto } from "@/entities/alerts/model"; import { useIncidentAlerts, usePollIncidentAlerts, } from "utils/hooks/useIncidents"; -import AlertName from "@/app/(keep)/alerts/alert-name"; +import { AlertName } from "@/entities/alerts/ui"; import IncidentAlertMenu from "./incident-alert-menu"; import React, { useEffect, useMemo, useState } from "react"; import type { IncidentDto } from "@/entities/incidents/model"; -import { getCommonPinningStylesAndClassNames } from "@/components/ui/table/utils"; +import { getCommonPinningStylesAndClassNames, UISeverity } from "@/shared/ui"; import { EmptyStateCard } from "@/components/ui"; import { useRouter } from "next/navigation"; -import { TablePagination } from "@/shared/ui"; -import { AlertSeverityBorder } from "@/app/(keep)/alerts/alert-severity-border"; -import { getStatusIcon } from "@/shared/lib/status-utils"; -import { getStatusColor } from "@/shared/lib/status-utils"; +import { + TableIndeterminateCheckbox, + TablePagination, + TableSeverityCell, +} from "@/shared/ui"; +import { getStatusIcon, getStatusColor } from "@/shared/lib/status-utils"; import TimeAgo from "react-timeago"; +import clsx from "clsx"; +import { IncidentAlertsTableBodySkeleton } from "./incident-alert-table-body-skeleton"; +import { IncidentAlertsActions } from "./incident-alert-actions"; + interface Props { incident: IncidentDto; } @@ -91,39 +96,41 @@ export default function IncidentAlerts({ incident }: Props) { const columns = useMemo( () => [ - // TODO: Add back when we have Split action - // columnHelper.display({ - // id: "selected", - // size: 10, - // header: (context) => ( - // e.stopPropagation()} - // /> - // ), - // cell: (context) => ( - // e.stopPropagation()} - // /> - // ), - // }), columnHelper.display({ id: "severity", - maxSize: 4, header: () => <>, cell: (context) => ( - + ), + size: 4, + minSize: 4, + maxSize: 4, meta: { tdClassName: "p-0", thClassName: "p-0", }, }), + columnHelper.display({ + id: "selected", + minSize: 32, + maxSize: 32, + header: (context) => ( + + ), + cell: (context) => ( + + ), + }), columnHelper.display({ id: "name", header: "Name", @@ -184,7 +191,7 @@ export default function IncidentAlerts({ incident }: Props) { columnHelper.accessor("source", { id: "source", header: "Source", - minSize: 100, + maxSize: 100, cell: (context) => (context.getValue() ?? []).map((source, index) => ( incident.is_confirmed && ( ({}); + const table = useReactTable({ data: alerts?.items ?? [], columns: columns, rowCount: alerts?.count ?? 0, + getRowId: (row) => row.fingerprint, + onRowSelectionChange: setRowSelection, state: { + rowSelection, pagination, columnPinning: { - left: ["selected"], + left: ["severity", "selected", "name"], right: ["remove"], }, }, @@ -244,21 +257,38 @@ export default function IncidentAlerts({ incident }: Props) { ); } + const selectedFingerprints = Object.keys(rowSelection); + return ( <> - - + table.resetRowSelection()} + /> + +
{table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header, index) => { const { style, className } = - getCommonPinningStylesAndClassNames(header.column); + getCommonPinningStylesAndClassNames( + header.column, + table.getState().columnPinning.left?.length, + table.getState().columnPinning.right?.length + ); return ( {flexRender( header.column.columnDef.header, @@ -273,18 +303,22 @@ export default function IncidentAlerts({ incident }: Props) { {alerts && alerts?.items?.length > 0 && ( {table.getRowModel().rows.map((row, index) => ( - + {row.getVisibleCells().map((cell, index) => { const { style, className } = - getCommonPinningStylesAndClassNames(cell.column); + getCommonPinningStylesAndClassNames( + cell.column, + table.getState().columnPinning.left?.length, + table.getState().columnPinning.right?.length + ); return ( {flexRender( cell.column.columnDef.cell, @@ -297,24 +331,12 @@ export default function IncidentAlerts({ incident }: Props) { ))} )} - { - // Skeleton - (isLoading || (alerts?.items ?? []).length === 0) && ( - - {Array(pagination.pageSize) - .fill("") - .map((index, rowIndex) => ( - - {columns.map((c, cellIndex) => ( - - - - ))} - - ))} - - ) - } + {isLoading && ( + + )}
diff --git a/keep-ui/app/(keep)/incidents/[id]/getIncidentWithErrorHandling.tsx b/keep-ui/app/(keep)/incidents/[id]/getIncidentWithErrorHandling.tsx index fe7001508..29091a184 100644 --- a/keep-ui/app/(keep)/incidents/[id]/getIncidentWithErrorHandling.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/getIncidentWithErrorHandling.tsx @@ -1,4 +1,4 @@ -import { getIncident } from "@/entities/incidents/api/incidents"; +import { getIncident } from "@/entities/incidents/api"; import { createServerApiClient } from "@/shared/api/server"; import { notFound } from "next/navigation"; import { KeepApiError } from "@/shared/api"; diff --git a/keep-ui/app/(keep)/incidents/[id]/incident-overview.tsx b/keep-ui/app/(keep)/incidents/[id]/incident-overview.tsx index 8fa47b64d..17356df45 100644 --- a/keep-ui/app/(keep)/incidents/[id]/incident-overview.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/incident-overview.tsx @@ -31,7 +31,7 @@ import { useCopilotReadable, } from "@copilotkit/react-core"; import { IncidentOverviewSkeleton } from "../incident-overview-skeleton"; -import { AlertDto } from "../../alerts/models"; +import { AlertDto } from "@/entities/alerts/model"; import { useRouter } from "next/navigation"; interface Props { diff --git a/keep-ui/app/(keep)/incidents/[id]/incident-tabs-navigation.tsx b/keep-ui/app/(keep)/incidents/[id]/incident-tabs-navigation.tsx index db5391941..78b9d301d 100644 --- a/keep-ui/app/(keep)/incidents/[id]/incident-tabs-navigation.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/incident-tabs-navigation.tsx @@ -11,6 +11,7 @@ import { } from "@heroicons/react/24/outline"; import { CiViewTimeline } from "react-icons/ci"; import { IncidentDto } from "@/entities/incidents/model"; +import { useIncident } from "@/utils/hooks/useIncidents"; export const tabs = [ { icon: BellAlertIcon, label: "Alerts", path: "alerts" }, @@ -26,12 +27,15 @@ export const tabs = [ ]; export function IncidentTabsNavigation({ - incident, + incident: initialIncidentData, }: { incident?: IncidentDto; }) { // Using type assertion because this component only renders on the /incidents/[id] routes const { id } = useParams<{ id: string }>() as { id: string }; + const { data: incident } = useIncident(id, { + fallbackData: initialIncidentData, + }); const pathname = usePathname(); return ( diff --git a/keep-ui/app/(keep)/incidents/[id]/timeline/incident-timeline.tsx b/keep-ui/app/(keep)/incidents/[id]/timeline/incident-timeline.tsx index 3bc2faacb..737a65841 100644 --- a/keep-ui/app/(keep)/incidents/[id]/timeline/incident-timeline.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/timeline/incident-timeline.tsx @@ -7,7 +7,7 @@ import { AuditEvent, useAlerts } from "@/utils/hooks/useAlerts"; import { useIncidentAlerts } from "@/utils/hooks/useIncidents"; import { Card } from "@tremor/react"; import AlertSeverity from "@/app/(keep)/alerts/alert-severity"; -import { AlertDto } from "@/app/(keep)/alerts/models"; +import { AlertDto } from "@/entities/alerts/model"; import { format, parseISO, diff --git a/keep-ui/app/(keep)/incidents/page.tsx b/keep-ui/app/(keep)/incidents/page.tsx index 43dda250f..67ca38e1b 100644 --- a/keep-ui/app/(keep)/incidents/page.tsx +++ b/keep-ui/app/(keep)/incidents/page.tsx @@ -1,8 +1,5 @@ import { IncidentList } from "@/features/incident-list"; -import { - getIncidents, - GetIncidentsParams, -} from "@/entities/incidents/api/incidents"; +import { getIncidents, GetIncidentsParams } from "@/entities/incidents/api"; import { PaginatedIncidentsDto } from "@/entities/incidents/model"; import { createServerApiClient } from "@/shared/api/server"; diff --git a/keep-ui/app/(keep)/incidents/predicted-incidents-table.tsx b/keep-ui/app/(keep)/incidents/predicted-incidents-table.tsx index b07cb268a..d6ad32c2a 100644 --- a/keep-ui/app/(keep)/incidents/predicted-incidents-table.tsx +++ b/keep-ui/app/(keep)/incidents/predicted-incidents-table.tsx @@ -23,7 +23,7 @@ interface Props { editCallback: (rule: IncidentDto) => void; } -// Depricated? +// Deprecated export default function PredictedIncidentsTable({ incidents: incidents, }: Props) { diff --git a/keep-ui/app/(keep)/layout.tsx b/keep-ui/app/(keep)/layout.tsx index 836b13050..272267b26 100644 --- a/keep-ui/app/(keep)/layout.tsx +++ b/keep-ui/app/(keep)/layout.tsx @@ -11,10 +11,9 @@ import { PHProvider } from "../posthog-provider"; import dynamic from "next/dynamic"; import ReadOnlyBanner from "../read-only-banner"; import { auth } from "@/auth"; -import { ThemeScript } from "@/shared/ui/theme/ThemeScript"; +import { ThemeScript, WatchUpdateTheme } from "@/shared/ui"; import "@/app/globals.css"; import "react-toastify/dist/ReactToastify.css"; -import { WatchUpdateTheme } from "@/shared/ui/theme/WatchUpdateTheme"; const PostHogPageView = dynamic(() => import("@/shared/ui/PostHogPageView"), { ssr: false, diff --git a/keep-ui/app/(keep)/maintenance/create-or-update-maintenance-rule.tsx b/keep-ui/app/(keep)/maintenance/create-or-update-maintenance-rule.tsx index 44e62064d..7413e5a41 100644 --- a/keep-ui/app/(keep)/maintenance/create-or-update-maintenance-rule.tsx +++ b/keep-ui/app/(keep)/maintenance/create-or-update-maintenance-rule.tsx @@ -19,7 +19,7 @@ import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; import { useRouter } from "next/navigation"; import { useApi } from "@/shared/lib/hooks/useApi"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; interface Props { maintenanceToEdit: MaintenanceRule | null; diff --git a/keep-ui/app/(keep)/maintenance/maintenance-rules-table.tsx b/keep-ui/app/(keep)/maintenance/maintenance-rules-table.tsx index 83bc0cdd8..f4c3c4590 100644 --- a/keep-ui/app/(keep)/maintenance/maintenance-rules-table.tsx +++ b/keep-ui/app/(keep)/maintenance/maintenance-rules-table.tsx @@ -23,7 +23,7 @@ import { IoCheckmark } from "react-icons/io5"; import { HiMiniXMark } from "react-icons/hi2"; import { useState } from "react"; import { useApi } from "@/shared/lib/hooks/useApi"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; const columnHelper = createColumnHelper(); diff --git a/keep-ui/app/(keep)/mapping/create-or-edit-mapping.tsx b/keep-ui/app/(keep)/mapping/create-or-edit-mapping.tsx index 307a2b4e8..94e53d0fa 100644 --- a/keep-ui/app/(keep)/mapping/create-or-edit-mapping.tsx +++ b/keep-ui/app/(keep)/mapping/create-or-edit-mapping.tsx @@ -32,7 +32,7 @@ import { MappingRule } from "./models"; import { CreateableSearchSelect } from "@/components/ui/CreateableSearchSelect"; import { useTopology } from "@/app/(keep)/topology/model"; import { useApi } from "@/shared/lib/hooks/useApi"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; interface Props { editRule: MappingRule | null; diff --git a/keep-ui/app/(keep)/mapping/rules-table.tsx b/keep-ui/app/(keep)/mapping/rules-table.tsx index 8d2650b42..e326df658 100644 --- a/keep-ui/app/(keep)/mapping/rules-table.tsx +++ b/keep-ui/app/(keep)/mapping/rules-table.tsx @@ -22,7 +22,7 @@ import { useMappings } from "utils/hooks/useMappingRules"; import { toast } from "react-toastify"; import { useState } from "react"; import { useApi } from "@/shared/lib/hooks/useApi"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; const columnHelper = createColumnHelper(); diff --git a/keep-ui/app/(keep)/providers/page.client.tsx b/keep-ui/app/(keep)/providers/page.client.tsx index 83529eefe..ede753034 100644 --- a/keep-ui/app/(keep)/providers/page.client.tsx +++ b/keep-ui/app/(keep)/providers/page.client.tsx @@ -6,7 +6,11 @@ import Loading from "@/app/(keep)/loading"; import { useFilterContext } from "./filter-context"; import { toast } from "react-toastify"; import { useProviders } from "@/utils/hooks/useProviders"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; +import { Link } from "@/components/ui"; + +const EXTERNAL_URL_DOCS_URL = + "https://docs.keephq.dev/development/external-url"; export const useFetchProviders = () => { const [providers, setProviders] = useState([]); @@ -25,8 +29,14 @@ export const useFetchProviders = () => {
Webhooks are disabled because Keep is not accessible from the internet.
-
- Click for Keep docs on how to enabled it 📚 + + Read docs + {" "} + to learn how to enable it.
); @@ -38,11 +48,7 @@ export const useFetchProviders = () => { type: "info", position: toast.POSITION.TOP_CENTER, autoClose: 10000, - onClick: () => - window.open( - "https://docs.keephq.dev/development/external-url", - "_blank" - ), + onClick: () => window.open(EXTERNAL_URL_DOCS_URL, "_blank"), style: { width: "250%", // Set width marginLeft: "-75%", // Adjust starting position to left diff --git a/keep-ui/app/(keep)/providers/provider-form.tsx b/keep-ui/app/(keep)/providers/provider-form.tsx index f0c9b329a..a0267c8bd 100644 --- a/keep-ui/app/(keep)/providers/provider-form.tsx +++ b/keep-ui/app/(keep)/providers/provider-form.tsx @@ -20,6 +20,11 @@ import { AccordionHeader, AccordionBody, Badge, + Tab, + TabList, + TabGroup, + TabPanel, + TabPanels, } from "@tremor/react"; import { ExclamationCircleIcon, @@ -45,7 +50,7 @@ import { getZodSchema } from "./form-validation"; import TimeAgo from "react-timeago"; import { useApi } from "@/shared/lib/hooks/useApi"; import { KeepApiError, KeepApiReadOnlyError } from "@/shared/api"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; import { base64urlencode, generateRandomString, @@ -58,6 +63,7 @@ import { getRequiredConfigs, GroupFields, } from "./form-fields"; +import ProviderLogs from "./provider-logs"; type ProviderFormProps = { provider: Provider; @@ -417,18 +423,227 @@ const ProviderForm = ({ ?.filter((scope) => scope.mandatory_for_webhook) .every((scope) => providerValidatedScopes[scope.name] === true); + const [activeTab, setActiveTab] = useState(0); + + const renderFormContent = () => ( + <> +
+ {provider.oauth2_url && !provider.installed ? ( + <> + + + + ) : null} + {Object.keys(provider.config).length > 0 && ( + <> + + + )} +
+ + {/* Render required fields */} + {Object.entries(requiredConfigs).map(([field, config]) => ( +
+ +
+ ))} + + {/* Render grouped fields */} + {Object.entries(groupedConfigs).map(([name, fields]) => ( + + + + ))} + + {/* Render optional fields in a card */} + {Object.keys(optionalConfigs).length > 0 && ( + + Provider Optional Settings + + + {Object.entries(optionalConfigs).map(([field, config]) => ( +
+ +
+ ))} +
+
+
+ )} + +
+ {provider.can_setup_webhook && !installedProvidersMode && ( +
+
+ + + + +
+ {isLocalhost && ( + + + + Webhook installation is disabled because Keep is running + without an external URL. +
+
+ Click to learn more +
+
+
+ )} +
+ )} +
+ + {provider.can_setup_webhook && installedProvidersMode && ( + <> +
+ + +
+ + + )} + + {provider.supports_webhook && ( + + )} + + + + ); + return ( -
-
+
+
Connect to {provider.display_name} - {/* Display the Provisioned Badge if the provider is provisioned */} {provider.provisioned && ( Provisioned )} -
+ {installedProvidersMode && provider.last_pull_time && ( Provider last pull time:{" "} )} + {provider.provisioned && (
{provider.provider_description} )} + {Object.keys(provider.config).length > 0 && (
)} + {provider.scopes && provider.scopes.length > 0 && ( )} -
-
- {provider.oauth2_url && !provider.installed ? ( - <> - - - - ) : null} - {Object.keys(provider.config).length > 0 && ( - <> - - - )} -
- {/* Render required fields */} - {Object.entries(requiredConfigs).map(([field, config]) => ( -
- -
- ))} - - {/* Render grouped fields */} - {Object.entries(groupedConfigs).map(([name, fields]) => ( - - - - ))} - - {/* Render optional fields in a card */} - {Object.keys(optionalConfigs).length > 0 && ( - - Provider Optional Settings - - - {Object.entries(optionalConfigs).map(([field, config]) => ( -
- -
- ))} -
-
-
- )} -
- {provider.can_setup_webhook && !installedProvidersMode && ( -
-
- - - { - // This is here because pulling is only enabled for providers we can get alerts from (e.g., support webhook) - } - - -
- {isLocalhost && ( - - - - Webhook installation is disabled because Keep is running - without an external URL. -
-
- Click to learn more -
-
-
- )} -
- )} -
- {provider.can_setup_webhook && installedProvidersMode && ( - <> -
- - -
- - - )} - {provider.supports_webhook && ( - - )} - {formErrors && ( - - {formErrors} - - )} - {/* Hidden input for provider ID */} - - + {formErrors && ( + + {formErrors} + + )} + + {installedProvidersMode ? ( + + + Configuration + Logs + + + +
{renderFormContent()}
+
+ +
+ +
+
+
+
+ ) : ( +
{renderFormContent()}
+ )}
-
+
+
+ + +
+ {logs.map((log) => ( +
+ + {log.log_level} + +
+ {log.log_message} + {Object.keys(log.context).length > 0 && ( +
+                    {JSON.stringify(log.context, null, 2)}
+                  
+ )} +
+ + {new Date(log.timestamp).toLocaleString()} + +
+ ))} + + {logs.length === 0 && No logs found} +
+
+
+ ); +}; + +export default ProviderLogs; diff --git a/keep-ui/app/(keep)/rules/CorrelationSidebar/AlertsFoundBadge.tsx b/keep-ui/app/(keep)/rules/CorrelationSidebar/AlertsFoundBadge.tsx index 49b5ad3a1..495fa474c 100644 --- a/keep-ui/app/(keep)/rules/CorrelationSidebar/AlertsFoundBadge.tsx +++ b/keep-ui/app/(keep)/rules/CorrelationSidebar/AlertsFoundBadge.tsx @@ -1,6 +1,6 @@ import { Badge } from "@tremor/react"; import Image from "next/image"; -import { AlertDto } from "@/app/(keep)/alerts/models"; +import { AlertDto } from "@/entities/alerts/model"; type AlertsFoundBadgeProps = { alertsFound: AlertDto[]; diff --git a/keep-ui/app/(keep)/rules/CorrelationSidebar/CorrelationForm.tsx b/keep-ui/app/(keep)/rules/CorrelationSidebar/CorrelationForm.tsx index 21b3463d2..5490c7e71 100644 --- a/keep-ui/app/(keep)/rules/CorrelationSidebar/CorrelationForm.tsx +++ b/keep-ui/app/(keep)/rules/CorrelationSidebar/CorrelationForm.tsx @@ -10,7 +10,7 @@ import { TextInput, } from "@tremor/react"; import { Controller, get, useFormContext } from "react-hook-form"; -import { AlertDto } from "@/app/(keep)/alerts/models"; +import { AlertDto } from "@/entities/alerts/model"; import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline"; import React from "react"; import { CorrelationFormType } from "./types"; @@ -87,7 +87,7 @@ export const CorrelationForm = ({ /> -
+
-
+
( + render={({field: {value, onChange}}) => ( { groupedAttributes: selectedRule.grouping_criteria, requireApprove: selectedRule.require_approve, resolveOn: selectedRule.resolve_on, + createOn: selectedRule.create_on, query: queryInGroup, incidents: selectedRule.incidents, }; diff --git a/keep-ui/app/(keep)/settings/auth/api-key-settings.tsx b/keep-ui/app/(keep)/settings/auth/api-key-settings.tsx index 28c956fe7..16b5c2897 100644 --- a/keep-ui/app/(keep)/settings/auth/api-key-settings.tsx +++ b/keep-ui/app/(keep)/settings/auth/api-key-settings.tsx @@ -23,7 +23,7 @@ import { useRoles } from "utils/hooks/useRoles"; import { UpdateIcon } from "@radix-ui/react-icons"; import { useApi } from "@/shared/lib/hooks/useApi"; import { useConfig } from "@/utils/hooks/useConfig"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; import { ApiKey } from "@/app/(keep)/settings/auth/types"; interface Props { diff --git a/keep-ui/app/(keep)/settings/auth/users-settings.tsx b/keep-ui/app/(keep)/settings/auth/users-settings.tsx index a40661b9a..8555df4aa 100644 --- a/keep-ui/app/(keep)/settings/auth/users-settings.tsx +++ b/keep-ui/app/(keep)/settings/auth/users-settings.tsx @@ -12,7 +12,7 @@ import UsersSidebar from "./users-sidebar"; import { User } from "@/app/(keep)/settings/models"; import { UsersTable } from "./users-table"; import { useApi } from "@/shared/lib/hooks/useApi"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; interface Props { currentUser?: AuthUser; diff --git a/keep-ui/app/(keep)/settings/auth/users-sidebar.tsx b/keep-ui/app/(keep)/settings/auth/users-sidebar.tsx index 40b666642..cf960df4b 100644 --- a/keep-ui/app/(keep)/settings/auth/users-sidebar.tsx +++ b/keep-ui/app/(keep)/settings/auth/users-sidebar.tsx @@ -21,10 +21,9 @@ import { useGroups } from "utils/hooks/useGroups"; import { User, Group } from "@/app/(keep)/settings/models"; import { AuthType } from "utils/authenticationType"; import { useConfig } from "utils/hooks/useConfig"; -import Select from "@/components/ui/Select"; import { KeepApiError } from "@/shared/api"; import { useApi } from "@/shared/lib/hooks/useApi"; - +import { Select } from "@/shared/ui"; interface UserSidebarProps { isOpen: boolean; toggle: VoidFunction; diff --git a/keep-ui/app/(keep)/settings/create-api-key-modal.tsx b/keep-ui/app/(keep)/settings/create-api-key-modal.tsx index fc0e10efe..caa08bbe9 100644 --- a/keep-ui/app/(keep)/settings/create-api-key-modal.tsx +++ b/keep-ui/app/(keep)/settings/create-api-key-modal.tsx @@ -9,11 +9,10 @@ import { TextInput, Button, Subtitle, Icon } from "@tremor/react"; import { InfoCircledIcon } from "@radix-ui/react-icons"; import { Role } from "@/app/(keep)/settings/models"; import Modal from "@/components/ui/Modal"; -import Select from "@/components/ui/Select"; import { useApi } from "@/shared/lib/hooks/useApi"; import { KeepApiError } from "@/shared/api"; import { ApiKey } from "@/app/(keep)/settings/auth/types"; - +import { Select } from "@/shared/ui"; interface CreateApiKeyModalProps { isOpen: boolean; onClose: () => void; diff --git a/keep-ui/app/(keep)/settings/webhook-settings.tsx b/keep-ui/app/(keep)/settings/webhook-settings.tsx index 6e28c76b7..379721ac4 100644 --- a/keep-ui/app/(keep)/settings/webhook-settings.tsx +++ b/keep-ui/app/(keep)/settings/webhook-settings.tsx @@ -23,6 +23,7 @@ import { v4 as uuidv4 } from "uuid"; import { ExclamationCircleIcon } from "@heroicons/react/24/outline"; import * as Frigade from "@frigade/react"; import { useApi } from "@/shared/lib/hooks/useApi"; +import { useConfig } from "@/utils/hooks/useConfig"; interface Webhook { webhookApi: string; @@ -38,6 +39,7 @@ export default function WebhookSettings({ selectedTab }: Props) { const [codeTabIndex, setCodeTabIndex] = useState(0); const api = useApi(); + const { data: config } = useConfig(); const { data, error, isLoading } = useSWR( api.isReady() && selectedTab === "webhook" ? `/settings/webhook` : null, @@ -188,7 +190,9 @@ req.end(); > Click to create an example Alert - + {config?.FRIGADE_DISABLED ? null : ( + + )}
; } + +export const metadata: Metadata = { + title: "Keep - Workflow Executions", + description: "View and manage workflow executions.", +}; diff --git a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-execution-table.tsx b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-execution-table.tsx index bf5a882f6..9f0938922 100644 --- a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-execution-table.tsx +++ b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-execution-table.tsx @@ -21,7 +21,6 @@ import { formatDistanceToNowStrict } from "date-fns"; import { Menu, Transition } from "@headlessui/react"; import { Button, Icon } from "@tremor/react"; import { PiDiamondsFourFill } from "react-icons/pi"; -import { FaHandPointer } from "react-icons/fa"; import { HiBellAlert } from "react-icons/hi2"; import { useRouter } from "next/navigation"; import { CursorArrowRaysIcon } from "@heroicons/react/24/outline"; diff --git a/keep-ui/app/(keep)/workflows/builder/BuilderChanagesTracker.tsx b/keep-ui/app/(keep)/workflows/builder/BuilderChanagesTracker.tsx deleted file mode 100644 index 8ecc1feb5..000000000 --- a/keep-ui/app/(keep)/workflows/builder/BuilderChanagesTracker.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from "react"; -import useStore from "./builder-store"; -import { Button } from "@tremor/react"; -import { reConstructWorklowToDefinition } from "utils/reactFlow"; - -export default function BuilderChanagesTracker({ - onDefinitionChange, -}: { - onDefinitionChange: (def: Record) => void; -}) { - const { - nodes, - edges, - setEdges, - setNodes, - isLayouted, - setIsLayouted, - v2Properties, - changes, - setChanges, - lastSavedChanges, - setLastSavedChanges, - } = useStore(); - const handleDiscardChanges = (e: React.MouseEvent) => { - if (!isLayouted) return; - setEdges(lastSavedChanges.edges || []); - setNodes(lastSavedChanges.nodes || []); - setChanges(0); - setIsLayouted(false); - }; - - const handleSaveChanges = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - setLastSavedChanges({ nodes: nodes, edges: edges }); - const value = reConstructWorklowToDefinition({ - nodes: nodes, - edges: edges, - properties: v2Properties, - }); - onDefinitionChange(value); - setChanges(0); - }; - - return ( -
- - -
- ); -} diff --git a/keep-ui/app/(keep)/workflows/builder/CustomEdge.tsx b/keep-ui/app/(keep)/workflows/builder/CustomEdge.tsx index f1d17c72e..502f3b90b 100644 --- a/keep-ui/app/(keep)/workflows/builder/CustomEdge.tsx +++ b/keep-ui/app/(keep)/workflows/builder/CustomEdge.tsx @@ -1,6 +1,6 @@ import React from "react"; import { BaseEdge, EdgeLabelRenderer, getSmoothStepPath } from "@xyflow/react"; -import type { Edge, EdgeProps } from "@xyflow/react"; +import type { EdgeProps } from "@xyflow/react"; import useStore from "./builder-store"; import { PlusIcon } from "@radix-ui/react-icons"; import { Button } from "@tremor/react"; @@ -24,7 +24,7 @@ const CustomEdge: React.FC = ({ data, style, }: CustomEdgeProps) => { - const { deleteEdges, edges, setSelectedEdge, selectedEdge } = useStore(); + const { setSelectedEdge, selectedEdge } = useStore(); // Calculate the path and midpoint const [edgePath, labelX, labelY] = getSmoothStepPath({ @@ -35,9 +35,6 @@ const CustomEdge: React.FC = ({ borderRadius: 10, }); - const midpointX = (sourceX + targetX) / 2; - const midpointY = (sourceY + targetY) / 2; - let dynamicLabel = label; const isLayouted = !!data?.isLayouted; let showAddButton = @@ -54,8 +51,9 @@ const CustomEdge: React.FC = ({ dynamicLabel === "True" ? "left-0 bg-green-500" : dynamicLabel === "False" - ? "bg-red-500" - : "bg-orange-500"; + ? "bg-red-500" + : "bg-orange-500"; + return ( <> -
+
+
{!isLoading && ( ); } + +export const metadata: Metadata = { + title: "Keep - Workflow Builder", + description: "Build workflows with a UI builder.", +}; diff --git a/keep-ui/app/(keep)/workflows/builder/builder-card.tsx b/keep-ui/app/(keep)/workflows/builder/builder-card.tsx index 851f9fd00..a26c93c25 100644 --- a/keep-ui/app/(keep)/workflows/builder/builder-card.tsx +++ b/keep-ui/app/(keep)/workflows/builder/builder-card.tsx @@ -5,7 +5,6 @@ import { useEffect, useState } from "react"; import Loader from "./loader"; import { Provider } from "../../providers/providers"; import { useProviders } from "utils/hooks/useProviders"; -import clsx from "clsx"; const Builder = dynamic(() => import("./builder"), { ssr: false, // Prevents server-side rendering @@ -53,20 +52,14 @@ export function BuilderCard({ if (!providers || isLoading) return ( - + ); - return ( - - {error ? ( + if (error) { + return ( + Failed to load providers - ) : fileContents == "" && !workflow ? ( + + ); + } + + if (fileContents == "" && !workflow) { + return ( + - ) : ( - - )} - + + ); + } + + return ( + ); } diff --git a/keep-ui/app/(keep)/workflows/builder/builder-modal.tsx b/keep-ui/app/(keep)/workflows/builder/builder-modal.tsx index 1bbfd35ae..44eb01855 100644 --- a/keep-ui/app/(keep)/workflows/builder/builder-modal.tsx +++ b/keep-ui/app/(keep)/workflows/builder/builder-modal.tsx @@ -2,7 +2,7 @@ import { XMarkIcon } from "@heroicons/react/24/outline"; import { Button, Card, Subtitle, Title } from "@tremor/react"; import { CopyBlock, a11yLight } from "react-code-blocks"; import { stringify } from "yaml"; -import { Alert } from "./alert"; +import { Alert } from "./legacy-workflow.types"; import { useState } from "react"; import ReactLoading from "react-loading"; import { ArrowDownTrayIcon } from "@heroicons/react/20/solid"; @@ -56,8 +56,8 @@ export default function BuilderModalContent({ <>
- Generated Alert - Keep alert specification ready to use + Generated Workflow YAML + Keep workflow specification ready to use
{!hideCloseButton && ( diff --git a/keep-ui/app/(keep)/workflows/builder/builder-store.tsx b/keep-ui/app/(keep)/workflows/builder/builder-store.tsx index fd690b6fa..2bfccd9e7 100644 --- a/keep-ui/app/(keep)/workflows/builder/builder-store.tsx +++ b/keep-ui/app/(keep)/workflows/builder/builder-store.tsx @@ -67,32 +67,6 @@ export type FlowNode = Node & { isNested: boolean; }; -const initialNodes: Partial[] = [ - { - id: "a", - position: { x: 0, y: 0 }, - data: { label: "Node A", type: "custom" }, - type: "custom", - }, - { - id: "b", - position: { x: 0, y: 100 }, - data: { label: "Node B", type: "custom" }, - type: "custom", - }, - { - id: "c", - position: { x: 0, y: 200 }, - data: { label: "Node C", type: "custom" }, - type: "custom", - }, -]; - -const initialEdges: Edge[] = [ - { id: "a->b", type: "custom-edge", source: "a", target: "b" }, - { id: "b->c", type: "custom-edge", source: "b", target: "c" }, -]; - export type FlowState = { nodes: FlowNode[]; edges: Edge[]; diff --git a/keep-ui/app/(keep)/workflows/builder/builder-workflow-testrun-modal.tsx b/keep-ui/app/(keep)/workflows/builder/builder-workflow-testrun-modal.tsx index 94e0ae849..ca7c5d57e 100644 --- a/keep-ui/app/(keep)/workflows/builder/builder-workflow-testrun-modal.tsx +++ b/keep-ui/app/(keep)/workflows/builder/builder-workflow-testrun-modal.tsx @@ -1,5 +1,5 @@ import { XMarkIcon } from "@heroicons/react/24/outline"; -import { Button, Card, Subtitle, Title } from "@tremor/react"; +import { Button, Card, Title } from "@tremor/react"; import ReactLoading from "react-loading"; import { ExecutionResults } from "./workflow-execution-results"; import { WorkflowExecution, WorkflowExecutionFailure } from "./types"; diff --git a/keep-ui/app/(keep)/workflows/builder/builder.tsx b/keep-ui/app/(keep)/workflows/builder/builder.tsx index 7402bc60b..53649826e 100644 --- a/keep-ui/app/(keep)/workflows/builder/builder.tsx +++ b/keep-ui/app/(keep)/workflows/builder/builder.tsx @@ -14,7 +14,7 @@ import { } from "@heroicons/react/20/solid"; import { globalValidatorV2, stepValidatorV2 } from "./builder-validators"; import Modal from "react-modal"; -import { Alert } from "./alert"; +import { Alert } from "./legacy-workflow.types"; import BuilderModalContent from "./builder-modal"; import Loader from "./loader"; import { stringify } from "yaml"; @@ -34,9 +34,9 @@ import useStore from "./builder-store"; import { toast } from "react-toastify"; import { useApi } from "@/shared/lib/hooks/useApi"; import { KeepApiError } from "@/shared/api"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; -import "./page.css"; +import { showErrorToast } from "@/shared/ui"; import { YAMLException } from "js-yaml"; +import WorkflowDefinitionYAML from "../workflow-definition-yaml"; interface Props { loadedAlertFile: string | null; @@ -58,6 +58,15 @@ const INITIAL_DEFINITION = wrapDefinitionV2({ isValid: false, }); +const YAMLSidebar = ({ yaml }: { yaml?: string }) => { + return ( +
+

YAML

+ {yaml && } +
+ ); +}; + function Builder({ loadedAlertFile, fileName, @@ -366,29 +375,31 @@ function Builder({ {generateModalIsOpen || testRunModalOpen ? null : ( <> {getworkflowStatus()} -
- - { - setDefinition({ - value: { - sequence: def?.sequence || [], - properties: def?.properties || {}, - }, - isValid: def?.isValid || false, - }); - }} - toolboxConfiguration={getToolboxConfiguration( - providers, - installedProviders || [] - )} - /> - -
+ +
+
+ + { + setDefinition({ + value: { + sequence: def?.sequence || [], + properties: def?.properties || {}, + }, + isValid: def?.isValid || false, + }); + }} + toolboxConfiguration={getToolboxConfiguration(providers)} + /> + +
+ {/* TODO: Add AI chat sidebar */} +
+
)}
diff --git a/keep-ui/app/(keep)/workflows/builder/alert.tsx b/keep-ui/app/(keep)/workflows/builder/legacy-workflow.types.ts similarity index 91% rename from keep-ui/app/(keep)/workflows/builder/alert.tsx rename to keep-ui/app/(keep)/workflows/builder/legacy-workflow.types.ts index 47447bce9..4fa28c200 100644 --- a/keep-ui/app/(keep)/workflows/builder/alert.tsx +++ b/keep-ui/app/(keep)/workflows/builder/legacy-workflow.types.ts @@ -1,5 +1,3 @@ -import { V2Step } from "@/app/(keep)/workflows/builder/types"; - interface Provider { type: string; config: string; diff --git a/keep-ui/app/(keep)/workflows/builder/loader.tsx b/keep-ui/app/(keep)/workflows/builder/loader.tsx index 3775c32bb..48a485c4e 100644 --- a/keep-ui/app/(keep)/workflows/builder/loader.tsx +++ b/keep-ui/app/(keep)/workflows/builder/loader.tsx @@ -3,8 +3,10 @@ import { Title, Text } from "@tremor/react"; export default function Loader() { return (
- Please start by loading or creating a new alert - You can use the `Load` or `+` button from the top right menu + Please start by loading or creating a new workflow + + Load YAML or use the "New " button from the top right menu +
); } diff --git a/keep-ui/app/(keep)/workflows/builder/page.client.tsx b/keep-ui/app/(keep)/workflows/builder/page.client.tsx index dd77adfcd..24fcde10b 100644 --- a/keep-ui/app/(keep)/workflows/builder/page.client.tsx +++ b/keep-ui/app/(keep)/workflows/builder/page.client.tsx @@ -1,17 +1,16 @@ "use client"; -import { Title, Button, Subtitle, Badge } from "@tremor/react"; +import { Title, Button } from "@tremor/react"; import { useEffect, useRef, useState } from "react"; import { PlusIcon, - ArrowDownOnSquareIcon, BoltIcon, ArrowUpOnSquareIcon, PlayIcon, } from "@heroicons/react/20/solid"; import { BuilderCard } from "./builder-card"; import { loadWorkflowYAML } from "./utils"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; import { YAMLException } from "js-yaml"; export default function PageClient({ @@ -37,13 +36,17 @@ export default function PageClient({ setFileName(""); }, []); - function loadAlert() { - document.getElementById("alertFile")?.click(); + function loadWorkflow() { + document.getElementById("workflowFile")?.click(); } - function newAlert() { - const confirmed = confirm("Are you sure you want to create a new alert?"); - if (confirmed) window.location.reload(); + function createNewWorkflow() { + const confirmed = confirm( + "Are you sure you want to create a new workflow?" + ); + if (confirmed) { + window.location.reload(); + } } const enableButtons = () => setButtonsEnabled(true); @@ -87,7 +90,7 @@ export default function PageClient({ setTriggerGenerate(incrementState)} > - Generate + Get YAML )}
diff --git a/keep-ui/app/(keep)/workflows/builder/page.css b/keep-ui/app/(keep)/workflows/builder/page.css deleted file mode 100644 index 8d1c3cd58..000000000 --- a/keep-ui/app/(keep)/workflows/builder/page.css +++ /dev/null @@ -1,27 +0,0 @@ -.sqd-designer-react { - height: inherit; - border-radius: 10px; -} - -.sqd-designer { - height: 100%; -} - -.sqd-root-start-stop-circle, -.sqd-label-rect { - fill: rgb(255, 132, 16) !important; -} - -.sqd-smart-editor { - overflow: scroll; -} - -.sqd-toolbox-item-text { - text-transform: capitalize; -} - -.tooltip { - position: relative; - display: inline-block; - border-bottom: 1px dotted black; /* If you want dots under the hoverable text */ -} diff --git a/keep-ui/app/(keep)/workflows/builder/page.tsx b/keep-ui/app/(keep)/workflows/builder/page.tsx index 25ca84cde..cd81fc373 100644 --- a/keep-ui/app/(keep)/workflows/builder/page.tsx +++ b/keep-ui/app/(keep)/workflows/builder/page.tsx @@ -1,6 +1,7 @@ import PageClient from "./page.client"; import { Suspense } from "react"; import Loading from "@/app/(keep)/loading"; +import { Metadata } from "next"; type PageProps = { params: { workflow: string; workflowId: string }; @@ -15,7 +16,7 @@ export default function Page({ params, searchParams }: PageProps) { ); } -export const metadata = { - title: "Keep - Builder", - description: "Build alerts with a visual workflow designer.", +export const metadata: Metadata = { + title: "Keep - Workflow Builder", + description: "Build workflows with a UI builder.", }; diff --git a/keep-ui/app/(keep)/workflows/builder/utils.tsx b/keep-ui/app/(keep)/workflows/builder/utils.tsx index e60bc6817..c04f26c37 100644 --- a/keep-ui/app/(keep)/workflows/builder/utils.tsx +++ b/keep-ui/app/(keep)/workflows/builder/utils.tsx @@ -1,7 +1,6 @@ import { load, JSON_SCHEMA } from "js-yaml"; import { Provider } from "../../providers/providers"; -import { Action, Alert } from "./alert"; -import { stringify } from "yaml"; +import { Action, Alert } from "./legacy-workflow.types"; import { v4 as uuidv4 } from "uuid"; import { z, ZodObject } from "zod"; import { diff --git a/keep-ui/app/(keep)/workflows/manual-run-workflow-modal.tsx b/keep-ui/app/(keep)/workflows/manual-run-workflow-modal.tsx index eb32c7e6e..84ecc958b 100644 --- a/keep-ui/app/(keep)/workflows/manual-run-workflow-modal.tsx +++ b/keep-ui/app/(keep)/workflows/manual-run-workflow-modal.tsx @@ -6,9 +6,9 @@ import { useState } from "react"; import { toast } from "react-toastify"; import { useRouter } from "next/navigation"; import { IncidentDto } from "@/entities/incidents/model"; -import { AlertDto } from "@/app/(keep)/alerts/models"; +import { AlertDto } from "@/entities/alerts/model"; import { useApi } from "@/shared/lib/hooks/useApi"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; +import { showErrorToast } from "@/shared/ui"; interface Props { alert?: AlertDto | null | undefined; diff --git a/keep-ui/app/(keep)/workflows/workflow-menu.tsx b/keep-ui/app/(keep)/workflows/workflow-menu.tsx index ee89eb84d..f3438bdb4 100644 --- a/keep-ui/app/(keep)/workflows/workflow-menu.tsx +++ b/keep-ui/app/(keep)/workflows/workflow-menu.tsx @@ -4,16 +4,11 @@ import { EllipsisHorizontalIcon } from "@heroicons/react/20/solid"; import { Icon } from "@tremor/react"; import { EyeIcon, - PencilIcon, PlayIcon, TrashIcon, WrenchIcon, } from "@heroicons/react/24/outline"; -import { - DownloadIcon, - LockClosedIcon, - LockOpen1Icon, -} from "@radix-ui/react-icons"; +import { DownloadIcon } from "@radix-ui/react-icons"; import React from "react"; interface WorkflowMenuProps { diff --git a/keep-ui/app/(keep)/workflows/workflows.client.tsx b/keep-ui/app/(keep)/workflows/workflows.client.tsx index 43337c403..1e4b2d0d1 100644 --- a/keep-ui/app/(keep)/workflows/workflows.client.tsx +++ b/keep-ui/app/(keep)/workflows/workflows.client.tsx @@ -2,7 +2,7 @@ import { useRef, useState } from "react"; import useSWR from "swr"; -import { Callout, Subtitle, Switch } from "@tremor/react"; +import { Callout, Subtitle } from "@tremor/react"; import { ArrowUpOnSquareStackIcon, ExclamationCircleIcon, @@ -20,8 +20,7 @@ import Modal from "@/components/ui/Modal"; import MockWorkflowCardSection from "./mockworkflows"; import { useApi } from "@/shared/lib/hooks/useApi"; import { KeepApiError } from "@/shared/api"; -import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; -import { Input } from "@/shared/ui/Input"; +import { showErrorToast, Input } from "@/shared/ui"; export default function WorkflowsPage() { const api = useApi(); diff --git a/keep-ui/app/(signin)/layout.tsx b/keep-ui/app/(signin)/layout.tsx index c535b8c48..75a3a11ce 100644 --- a/keep-ui/app/(signin)/layout.tsx +++ b/keep-ui/app/(signin)/layout.tsx @@ -1,3 +1,6 @@ +import { Card, Text } from "@tremor/react"; +import Image from "next/image"; + export const metadata = { title: "Keep", description: "The open-source alert management and AIOps platform", @@ -9,8 +12,33 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - - {children} + + +
+
+
+ Keep Logo + + Keep + +
+ + {children} + +
+
+ ); } diff --git a/keep-ui/app/(signin)/signin/SignInForm.tsx b/keep-ui/app/(signin)/signin/SignInForm.tsx index e5a4ef1f4..faf6d1553 100644 --- a/keep-ui/app/(signin)/signin/SignInForm.tsx +++ b/keep-ui/app/(signin)/signin/SignInForm.tsx @@ -1,13 +1,12 @@ "use client"; import { signIn, getProviders } from "next-auth/react"; -import { Text, TextInput, Button, Card } from "@tremor/react"; -import Image from "next/image"; +import { Text, TextInput, Button } from "@tremor/react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; -import "../../globals.css"; import { authenticate, revalidateAfterAuth } from "@/app/actions/authactions"; import { useRouter } from "next/navigation"; +import "../../globals.css"; export interface Provider { id: string; @@ -109,129 +108,92 @@ export default function SignInForm({ params }: { params?: { amt: string } }) { // Show loading state during redirect if (isRedirecting) { return ( -
- -
-
- Keep Logo -
- - Authentication successful, redirecting... - -
-
-
+ + Authentication successful, redirecting... + ); } if (providers?.credentials) { return ( -
- -
-
- Keep Logo + <> + + Log in to your account + + +
+ {errors.root && ( +
+ + {errors.root.message} +
- - - Sign in to Keep + )} +
+ + Username + + {errors.username && ( + + {errors.username.message} + + )} +
- -
- - Username - - - {errors.username && ( - - {errors.username.message} - - )} -
- -
- - Password - - - {errors.password && ( - - {errors.password.message} - - )} -
- - - - {errors.root && ( -
- - {errors.root.message} - -
- )} - +
+ + Password + + + {errors.password && ( + + {errors.password.message} + + )}
- -
+ + + + ); } - return <>Redirecting to authentication...; + return ( + + Redirecting to authentication... + + ); } diff --git a/keep-ui/app/frigade-provider.tsx b/keep-ui/app/frigade-provider.tsx index 2ae5b4619..d4980844a 100644 --- a/keep-ui/app/frigade-provider.tsx +++ b/keep-ui/app/frigade-provider.tsx @@ -2,12 +2,20 @@ import * as Frigade from "@frigade/react"; import { useHydratedSession as useSession } from "@/shared/lib/hooks/useHydratedSession"; +import {useConfig} from "@/utils/hooks/useConfig"; export const FrigadeProvider = ({ children, }: { children: React.ReactNode; }) => { const { data: session } = useSession(); + const { data: config } = useConfig(); + + if (!config || config.FRIGADE_DISABLED === "true") { + return <> + {children} + ; + } return ( { + const { data: config } = useConfig(); + const { flow } = Frigade.useFlow(ONBOARDING_FLOW_ID); const [isOnboardingOpen, setIsOnboardingOpen] = useState(false); const isMounted = useMounted(); @@ -94,7 +96,7 @@ export const UserInfo = ({ session }: UserInfoProps) => { return ( <>
    - {isMounted && flow?.isCompleted === false && ( + {isMounted && !config?.FRIGADE_DISABLED && flow?.isCompleted === false && (
  • { - data: T[]; - columns: DisplayColumnDef[]; - rowCount: number; - offset: number; - limit: number; - onPaginationChange: ( limit: number, offset: number ) => void; - onRowClick?: (row: T) => void; - dataFetchedAtOneGO?: boolean + data: T[]; + columns: DisplayColumnDef[]; + rowCount: number; + offset: number; + limit: number; + onPaginationChange: (limit: number, offset: number) => void; + onRowClick?: (row: T) => void; + dataFetchedAtOneGO?: boolean; } export function GenericTable({ - data, - columns, - rowCount, - offset, - limit, - onPaginationChange, - onRowClick, - dataFetchedAtOneGO, + data, + columns, + rowCount, + offset, + limit, + onPaginationChange, + onRowClick, + dataFetchedAtOneGO, }: GenericTableProps) { - const [expanded, setExpanded] = useState({}); - const [pagination, setPagination] = useState({ - pageIndex: Math.floor(offset / limit), - pageSize: limit, - }); + const [expanded, setExpanded] = useState({}); + const [pagination, setPagination] = useState({ + pageIndex: Math.floor(offset / limit), + pageSize: limit, + }); - useEffect(() => { - setPagination({ - pageIndex: Math.floor(offset / limit), - pageSize: limit, - }); - }, [offset, limit]); + useEffect(() => { + setPagination({ + pageIndex: Math.floor(offset / limit), + pageSize: limit, + }); + }, [offset, limit]); - useEffect(() => { - const currentOffset = pagination.pageSize * pagination.pageIndex; - if (offset !== currentOffset || limit !== pagination.pageSize) { - onPaginationChange( - pagination.pageSize, - currentOffset - ); - } - }, [pagination]); + useEffect(() => { + const currentOffset = pagination.pageSize * pagination.pageIndex; + if (offset !== currentOffset || limit !== pagination.pageSize) { + onPaginationChange(pagination.pageSize, currentOffset); + } + }, [pagination]); - const finalData = (dataFetchedAtOneGO ? data.slice(pagination.pageSize * pagination.pageIndex, pagination.pageSize * (pagination.pageIndex + 1)) : data) as T[] + const finalData = ( + dataFetchedAtOneGO + ? data.slice( + pagination.pageSize * pagination.pageIndex, + pagination.pageSize * (pagination.pageIndex + 1) + ) + : data + ) as T[]; - const table = useReactTable({ - columns, - data: finalData, - state: { expanded, pagination }, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, - pageCount: Math.ceil(rowCount / limit), // Pass the total pages to React Table - onPaginationChange: (updater) => { - const nextPagination = typeof updater === "function" ? updater(pagination) : updater; - setPagination(nextPagination); - }, - onExpandedChange: setExpanded, - }); + const table = useReactTable({ + columns, + data: finalData, + state: { expanded, pagination }, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: Math.ceil(rowCount / limit), // Pass the total pages to React Table + onPaginationChange: (updater) => { + const nextPagination = + typeof updater === "function" ? updater(pagination) : updater; + setPagination(nextPagination); + }, + onExpandedChange: setExpanded, + }); - return ( -
    -
    - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - onRowClick?.(row.original)} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - ))} - - -
    -
    - {pagination&&} -
    -
    - ); + return ( +
    +
    + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + onRowClick?.(row.original)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + + +
    +
    + {pagination && } +
    +
    + ); } diff --git a/keep-ui/components/table/Pagination.tsx b/keep-ui/components/table/Pagination.tsx index d28d48f18..ce388d6eb 100644 --- a/keep-ui/components/table/Pagination.tsx +++ b/keep-ui/components/table/Pagination.tsx @@ -1,5 +1,4 @@ import { - ArrowPathIcon, ChevronDoubleLeftIcon, ChevronDoubleRightIcon, ChevronLeftIcon, @@ -7,9 +6,9 @@ import { TableCellsIcon, } from "@heroicons/react/24/outline"; import { Button, Text } from "@tremor/react"; -import { StylesConfig, SingleValueProps, components, GroupBase } from 'react-select'; -import Select from 'react-select'; +import { SingleValueProps, components, GroupBase } from "react-select"; import { Table } from "@tanstack/react-table"; +import { Select } from "@/shared/ui"; interface Props { table: Table; @@ -21,31 +20,10 @@ interface OptionType { label: string; } -const customStyles: StylesConfig> = { - control: (provided, state) => ({ - ...provided, - borderColor: state.isFocused ? 'orange' : provided.borderColor, - '&:hover': { borderColor: 'orange' }, - boxShadow: state.isFocused ? '0 0 0 1px orange' : provided.boxShadow, - }), - singleValue: (provided) => ({ - ...provided, - display: 'flex', - alignItems: 'center', - }), - menu: (provided) => ({ - ...provided, - color: 'orange', - }), - option: (provided, state) => ({ - ...provided, - backgroundColor: state.isSelected ? 'orange' : provided.backgroundColor, - '&:hover': { backgroundColor: state.isSelected ? 'orange' : '#f5f5f5' }, - color: state.isSelected ? 'white' : provided.color, - }), -}; - -const SingleValue = ({ children, ...props }: SingleValueProps>) => ( +const SingleValue = ({ + children, + ...props +}: SingleValueProps>) => ( {children} @@ -58,63 +36,64 @@ export default function Pagination({ table, isRefreshAllowed }: Props) { return (
    -
    - Rows per page - + table.setPageSize(Number(selectedOption!.value)) + } + options={[ + { value: "10", label: "10" }, + { value: "25", label: "25" }, + { value: "50", label: "50" }, + { value: "100", label: "100" }, + ]} + menuPlacement="top" + className="rounded-md" + /> +
    + + Page {pageCount === 0 ? 0 : pageIndex + 1} of {pageCount} + +
    +
    +
    ); } diff --git a/keep-ui/components/ui/AutocompleteInput.tsx b/keep-ui/components/ui/AutocompleteInput.tsx index 35dc2c3d8..95d0f0e8d 100644 --- a/keep-ui/components/ui/AutocompleteInput.tsx +++ b/keep-ui/components/ui/AutocompleteInput.tsx @@ -1,3 +1,5 @@ +"use client"; + import React, { useState, useEffect, useRef } from "react"; import { TextInput } from "./TextInput"; import { cn } from "utils/helpers"; diff --git a/keep-ui/components/ui/MultiSelect.tsx b/keep-ui/components/ui/MultiSelect.tsx deleted file mode 100644 index eb22a710d..000000000 --- a/keep-ui/components/ui/MultiSelect.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React from "react"; -import Select from "react-select"; -import { - components, - Props as SelectProps, - GroupBase, - StylesConfig, -} from "react-select"; -import { Badge } from "@tremor/react"; - -type OptionType = { value: string; label: string }; - -const customStyles: StylesConfig = { - control: (provided: any, state: any) => ({ - ...provided, - borderColor: state.isFocused ? "orange" : "#ccc", - "&:hover": { - borderColor: "orange", - }, - boxShadow: state.isFocused ? "0 0 0 1px orange" : null, - backgroundColor: "transparent", - }), - option: (provided, state) => ({ - ...provided, - backgroundColor: state.isSelected - ? "orange" - : state.isFocused - ? "rgba(255, 165, 0, 0.1)" - : "transparent", - color: state.isSelected ? "white" : "black", - "&:hover": { - backgroundColor: "rgba(255, 165, 0, 0.3)", - }, - }), - multiValue: (provided) => ({ - ...provided, - backgroundColor: "default", - }), - multiValueLabel: (provided) => ({ - ...provided, - color: "black", - }), - multiValueRemove: (provided) => ({ - ...provided, - color: "orange", - "&:hover": { - backgroundColor: "orange", - color: "white", - }, - }), - menuPortal: (base) => ({ - ...base, - zIndex: 9999, - }), - menu: (provided) => ({ - ...provided, - zIndex: 9999, - }), -}; - -type CustomSelectProps = SelectProps< - OptionType, - true, - GroupBase -> & { - components?: { - Option?: typeof components.Option; - MultiValue?: typeof components.MultiValue; - }; -}; - -const customComponents: CustomSelectProps["components"] = { - Option: ({ children, ...props }) => ( - - - {children} - - - ), - MultiValue: ({ children, ...props }) => ( - - - {children} - - - ), -}; - -type MultiSelectProps = SelectProps>; - -const MultiSelect: React.FC = ({ - value, - onChange, - options, - placeholder, - ...rest -}) => ( - ; @@ -51,8 +51,8 @@ const SortableHeaderCell = ({ header, children }: SortableHeaderCellProps) => { column.getNextSortingOrder() === "asc" ? "Sort ascending" : column.getNextSortingOrder() === "desc" - ? "Sort descending" - : "Clear sort" + ? "Sort descending" + : "Clear sort" } icon={ column.getIsSorted() @@ -100,29 +100,28 @@ export const IncidentTableComponent = (props: Props) => { {table.getRowModel().rows.map((row) => ( - <> - - {row.getVisibleCells().map((cell) => { - const { style, className } = - getCommonPinningStylesAndClassNames(cell.column); - return ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ); - })} - - + + {row.getVisibleCells().map((cell) => { + const { style, className } = getCommonPinningStylesAndClassNames( + cell.column + ); + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ); + })} + ))} diff --git a/keep-ui/features/incident-list/ui/incidents-table.tsx b/keep-ui/features/incident-list/ui/incidents-table.tsx index dd64e64c1..c46c619f8 100644 --- a/keep-ui/features/incident-list/ui/incidents-table.tsx +++ b/keep-ui/features/incident-list/ui/incidents-table.tsx @@ -25,7 +25,6 @@ import Markdown from "react-markdown"; import remarkRehype from "remark-rehype"; import rehypeRaw from "rehype-raw"; import ManualRunWorkflowModal from "@/app/(keep)/workflows/manual-run-workflow-modal"; -import AlertTableCheckbox from "@/app/(keep)/alerts/alert-table-checkbox"; import { Button, Link } from "@/components/ui"; import { MergeIncidentsModal } from "@/features/merge-incidents"; import { IncidentDropdownMenu } from "./incident-dropdown-menu"; @@ -34,7 +33,11 @@ import { IncidentChangeStatusSelect } from "@/features/change-incident-status/"; import { useIncidentActions } from "@/entities/incidents/model"; import { IncidentSeverityBadge } from "@/entities/incidents/ui"; import { getIncidentName } from "@/entities/incidents/lib/utils"; -import { DateTimeField, TablePagination } from "@/shared/ui"; +import { + DateTimeField, + TableIndeterminateCheckbox, + TablePagination, +} from "@/shared/ui"; import { UserStatefulAvatar } from "@/entities/users/ui"; function SelectedRowActions({ @@ -120,8 +123,10 @@ export default function IncidentsTable({ const columns = [ columnHelper.display({ id: "selected", + minSize: 32, + maxSize: 32, header: (context) => ( - ), cell: (context) => ( - -
    {STATUS_ICONS[incident.status]}
    -
    -
    {getIncidentName(incident)}
    -
    -
- ); -} - +import { Select, VerticalRoundedList } from "@/shared/ui"; +import { IncidentIconName } from "@/entities/incidents/ui"; interface Props { incidents: IncidentDto[]; handleClose: () => void; onSuccess?: () => void; } -interface OptionType { - value: string; - label: JSX.Element; -} - -// TODO: unify all selects into components/ui/Select.tsx -const customSelectStyles: StylesConfig< - OptionType, - false, - GroupBase -> = { - control: (provided, state) => ({ - ...provided, - borderColor: state.isFocused ? "orange" : "rgb(229 231 235)", - borderRadius: "0.5rem", - "&:hover": { borderColor: "orange" }, - boxShadow: state.isFocused ? "0 0 0 1px orange" : provided.boxShadow, - }), - singleValue: (provided) => ({ - ...provided, - display: "flex", - alignItems: "center", - }), - menu: (provided) => ({ - ...provided, - color: "orange", - }), - option: (provided, state) => ({ - ...provided, - backgroundColor: state.isSelected ? "orange" : provided.backgroundColor, - "&:hover": { backgroundColor: state.isSelected ? "orange" : "#f5f5f5" }, - color: state.isSelected ? "white" : "black", - }), -}; - export function MergeIncidentsModal({ incidents, handleClose, @@ -91,14 +29,14 @@ export function MergeIncidentsModal({ const incidentOptions = useMemo(() => { return incidents.map((incident) => ({ value: incident.id, - label: , + label: , })); }, [incidents]); const selectValue = useMemo(() => { return { value: destinationIncidentId, - label: , + label: , }; }, [destinationIncidentId, destinationIncident]); @@ -137,11 +75,11 @@ export function MergeIncidentsModal({

)}
-
+ {sourceIncidents.map((incident) => ( - + ))} -
+
@@ -152,7 +90,6 @@ export function MergeIncidentsModal({ value={selectValue} onChange={(option) => setDestinationIncidentId(option!.value)} placeholder="Select destination incident" - styles={customSelectStyles} />
diff --git a/keep-ui/features/same-incidents-in-the-past/ui/change-same-incident-in-the-past-form.tsx b/keep-ui/features/same-incidents-in-the-past/ui/change-same-incident-in-the-past-form.tsx index f97c165a4..592b0cd07 100644 --- a/keep-ui/features/same-incidents-in-the-past/ui/change-same-incident-in-the-past-form.tsx +++ b/keep-ui/features/same-incidents-in-the-past/ui/change-same-incident-in-the-past-form.tsx @@ -1,5 +1,4 @@ import { Button, Divider, Title } from "@tremor/react"; -import Select from "@/components/ui/Select"; import { useRouter } from "next/navigation"; import { FormEvent, useState } from "react"; import { useIncidents, usePollIncidents } from "@/utils/hooks/useIncidents"; @@ -7,6 +6,7 @@ import Loading from "@/app/(keep)/loading"; import type { IncidentDto } from "@/entities/incidents/model"; import { useIncidentActions } from "@/entities/incidents/model"; import { getIncidentName } from "@/entities/incidents/lib/utils"; +import { Select } from "@/shared/ui"; interface ChangeSameIncidentInThePastFormProps { incident: IncidentDto; diff --git a/keep-ui/features/split-incident-alerts/index.ts b/keep-ui/features/split-incident-alerts/index.ts new file mode 100644 index 000000000..f4e9e9ff8 --- /dev/null +++ b/keep-ui/features/split-incident-alerts/index.ts @@ -0,0 +1 @@ +export { SplitIncidentAlertsModal } from "./ui/split-incident-alerts-modal"; diff --git a/keep-ui/features/split-incident-alerts/ui/split-incident-alerts-modal.tsx b/keep-ui/features/split-incident-alerts/ui/split-incident-alerts-modal.tsx new file mode 100644 index 000000000..fdc7d286a --- /dev/null +++ b/keep-ui/features/split-incident-alerts/ui/split-incident-alerts-modal.tsx @@ -0,0 +1,114 @@ +import { Button, Title, Subtitle } from "@tremor/react"; +import Modal from "@/components/ui/Modal"; +import { useIncidentActions } from "@/entities/incidents/model"; +import { useMemo, useState } from "react"; +import { Select, VerticalRoundedList } from "@/shared/ui"; +import { IncidentIconName } from "@/entities/incidents/ui"; +import { + useIncident, + useIncidents, + usePollIncidents, +} from "@/utils/hooks/useIncidents"; +import Skeleton from "react-loading-skeleton"; + +interface Props { + sourceIncidentId: string; + alertFingerprints: string[]; + handleClose: () => void; + onSuccess?: () => void; +} + +export function SplitIncidentAlertsModal({ + sourceIncidentId, + handleClose, + onSuccess, + alertFingerprints, +}: Props) { + const { data: sourceIncident, isLoading: isSourceIncidentLoading } = + useIncident(sourceIncidentId); + const { data: incidents, isLoading, mutate, error } = useIncidents(true, 100); + usePollIncidents(mutate); + + const [destinationIncidentId, setDestinationIncidentId] = useState(); + const destinationIncident = incidents?.items.find( + (incident) => incident.id === destinationIncidentId + ); + + const incidentOptions = useMemo(() => { + if (!incidents) { + return []; + } + return incidents.items + .filter((incident) => incident.id !== sourceIncidentId) + .map((incident) => ({ + value: incident.id, + label: , + })); + }, [sourceIncidentId, incidents]); + + const selectValue = useMemo(() => { + if (!destinationIncident) { + return null; + } + return { + value: destinationIncidentId, + label: , + }; + }, [destinationIncidentId, destinationIncident]); + + const { splitIncidentAlerts } = useIncidentActions(); + const handleSplit = () => { + splitIncidentAlerts( + sourceIncidentId, + alertFingerprints, + destinationIncidentId! + ); + handleClose(); + onSuccess?.(); + }; + + return ( + +
+
+ Split Incident Alerts + + Alerts from the this incident will be moved into the destination + incident. + +
+
+
+ Source Incident +
+ + {isSourceIncidentLoading || !sourceIncident ? ( + + ) : ( + + )} + +
+
+
+ Destination Incident +
+
diff --git a/keep-ui/shared/ui/TableIndeterminateCheckbox/index.ts b/keep-ui/shared/ui/TableIndeterminateCheckbox/index.ts new file mode 100644 index 000000000..f0501c6be --- /dev/null +++ b/keep-ui/shared/ui/TableIndeterminateCheckbox/index.ts @@ -0,0 +1 @@ +export { TableIndeterminateCheckbox } from "./TableIndeterminateCheckbox"; diff --git a/keep-ui/shared/ui/TablePagination/TablePagination.tsx b/keep-ui/shared/ui/TablePagination/TablePagination.tsx index 8face106a..f90794c8c 100644 --- a/keep-ui/shared/ui/TablePagination/TablePagination.tsx +++ b/keep-ui/shared/ui/TablePagination/TablePagination.tsx @@ -1,3 +1,5 @@ +"use client"; + import { ChevronDoubleLeftIcon, ChevronDoubleRightIcon, @@ -7,8 +9,9 @@ import { } from "@heroicons/react/16/solid"; import { Button, Text } from "@tremor/react"; import type { Table } from "@tanstack/react-table"; -import type { GroupBase, StylesConfig, SingleValueProps } from "react-select"; -import Select, { components } from "react-select"; +import type { GroupBase, SingleValueProps } from "react-select"; +import { components } from "react-select"; +import { Select } from "@/shared/ui"; type Props = { table: Table; @@ -21,31 +24,6 @@ interface OptionType { label: string; } -const customStyles: StylesConfig> = { - control: (provided, state) => ({ - ...provided, - borderColor: state.isFocused ? "orange" : "rgb(229 231 235)", - borderRadius: "0.5rem", - "&:hover": { borderColor: "orange" }, - boxShadow: state.isFocused ? "0 0 0 1px orange" : provided.boxShadow, - }), - singleValue: (provided) => ({ - ...provided, - display: "flex", - alignItems: "center", - }), - menu: (provided) => ({ - ...provided, - color: "orange", - }), - option: (provided, state) => ({ - ...provided, - backgroundColor: state.isSelected ? "orange" : provided.backgroundColor, - "&:hover": { backgroundColor: state.isSelected ? "orange" : "#f5f5f5" }, - color: state.isSelected ? "white" : provided.color, - }), -}; - const SingleValue = ({ children, ...props @@ -67,7 +45,6 @@ export function TablePagination({ table }: Props) {