Skip to content

Commit

Permalink
Store integration trigger result (#1247)
Browse files Browse the repository at this point in the history
* feat(migrations): add column for trigger result

* feat(migrations): add default value to trigger result for integrations

* feat(backend): adapt to new db schema

* feat(backend): store integration trigger result

* feat(migrations): drop the last trigger on column

* feat(backend): change names of attributes

* feat(services/integration): handle when the sync itself fails

* feat(services/integration): store only 20 elements in the queue

* Merge branch 'main' into issue-1232

* feat(services/integration): use more efficient structure for trigger results

* chore(frontend): remove ref to invalid integration column

* chore(frontend): add separator between first row elements

* chore(frontend): add empty fragment

* fix(frontend): do not show paused if it is not

* feat(frontend): add btn to show integration trigger result

* refactor(frontend): change name of var

* feat(frontend): add modal for trigger result display

* feat(frontend): display integration logs in a table

* chore(frontend): remove useless text in header

* docs: add information about automatic disabling of integrations

* feat(migrations): add column for last finished at to integration table

* feat(backend): store the last finished at date for integration

* feat(frontend): display last finished on

* feat(migrations): set the last triggered at date correctly for integrations

* fix(frontend): change the font sizes to be consistent

* ci: Run CI

* chore(frontend): remove useless map

* refactor(services/integration): common function to select integrations

* fix(migrations): handle cases when integration has never been triggered

* chore(services/integration): order by created on when selecting integrations

* feat(services/misc): add fn to mark integrations as disabled

* feat(backend): add new notification type

* feat(frontend): allow disabling new integration

* feat(services/misc): complete implementation of automatically disabling integrations

* ci: Run CI

* refactor(services/misc): do not declare separate integrations
  • Loading branch information
IgnisDa authored Feb 10, 2025
1 parent c320391 commit 7d42679
Show file tree
Hide file tree
Showing 15 changed files with 395 additions and 164 deletions.
180 changes: 109 additions & 71 deletions apps/frontend/app/routes/_dashboard.settings.integrations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
Paper,
Select,
Stack,
Table,
type TableData,
Text,
TextInput,
Title,
Expand Down Expand Up @@ -46,7 +48,7 @@ import {
IconPencil,
IconTrash,
} from "@tabler/icons-react";
import { useState } from "react";
import { type ReactNode, useState } from "react";
import { Form, data, useActionData, useLoaderData } from "react-router";
import { match } from "ts-pattern";
import { withQuery } from "ufo";
Expand Down Expand Up @@ -326,88 +328,124 @@ const DisplayIntegration = (props: {
setUpdateIntegrationModalData: (data: Integration | null) => void;
}) => {
const [parent] = useAutoAnimate();
const [integrationInputOpened, { toggle: integrationInputToggle }] =
const [integrationUrlOpened, { toggle: integrationUrlToggle }] =
useDisclosure(false);
const [
integrationTriggerResultOpened,
{ toggle: integrationTriggerResultToggle },
] = useDisclosure(false);
const submit = useConfirmSubmit();

const integrationUrl = `${applicationBaseUrl}/_i/${props.integration.id}`;

const firstRow = [
<Text size="sm" fw="bold" key="name">
{props.integration.name || changeCase(props.integration.provider)}
</Text>,
props.integration.isDisabled ? (
<Text size="sm" key="isPaused">
Paused
</Text>
) : undefined,
props.integration.triggerResult.length > 0 ? (
<Anchor
size="sm"
key="triggerResult"
onClick={() => integrationTriggerResultToggle()}
>
Show logs
</Anchor>
) : undefined,
]
.filter(Boolean)
.map<ReactNode>((s) => s)
.reduce((prev, curr) => [prev, " • ", curr]);

const tableData: TableData = {
head: ["Triggered At", "Error"],
body: props.integration.triggerResult.map((tr) => [
dayjsLib(tr.finishedAt).format("lll"),
tr.error || "N/A",
]),
};

return (
<Paper p="xs" withBorder>
<Stack ref={parent}>
<Flex align="center" justify="space-between">
<Box>
<Group gap={4}>
<Text size="sm" fw="bold">
{props.integration.name ||
changeCase(props.integration.provider)}
</Text>
{props.integration.isDisabled ? (
<Text size="xs">(Paused)</Text>
) : null}
</Group>
<Text size="xs">
Created: {dayjsLib(props.integration.createdOn).fromNow()}
</Text>
{props.integration.lastTriggeredOn ? (
<>
<Modal
withCloseButton={false}
opened={integrationTriggerResultOpened}
onClose={() => integrationTriggerResultToggle()}
>
<Table data={tableData} />
</Modal>
<Paper p="xs" withBorder>
<Stack ref={parent}>
<Flex align="center" justify="space-between">
<Box>
<Group gap={4}>{firstRow}</Group>
<Text size="xs">
Triggered:{" "}
{dayjsLib(props.integration.lastTriggeredOn).fromNow()}
Created: {dayjsLib(props.integration.createdOn).fromNow()}
</Text>
) : null}
{props.integration.syncToOwnedCollection ? (
<Text size="xs">Being synced to "Owned" collection</Text>
) : null}
</Box>
<Group>
{!NO_SHOW_URL.includes(props.integration.provider) ? (
<ActionIcon color="blue" onClick={integrationInputToggle}>
{integrationInputOpened ? <IconEyeClosed /> : <IconEye />}
</ActionIcon>
) : null}
<ActionIcon
color="indigo"
variant="subtle"
onClick={() =>
props.setUpdateIntegrationModalData(props.integration)
}
>
<IconPencil />
</ActionIcon>
<Form method="POST" action={withQuery(".", { intent: "delete" })}>
<input
type="hidden"
name="integrationId"
defaultValue={props.integration.id}
/>
{props.integration.lastFinishedAt ? (
<Text size="xs">
Last finished:{" "}
{dayjsLib(props.integration.lastFinishedAt).fromNow()}
</Text>
) : null}
{props.integration.syncToOwnedCollection ? (
<Text size="xs">Being synced to "Owned" collection</Text>
) : null}
</Box>
<Group>
{!NO_SHOW_URL.includes(props.integration.provider) ? (
<ActionIcon color="blue" onClick={integrationUrlToggle}>
{integrationUrlOpened ? <IconEyeClosed /> : <IconEye />}
</ActionIcon>
) : null}
<ActionIcon
type="submit"
color="red"
color="indigo"
variant="subtle"
mt={4}
onClick={(e) => {
const form = e.currentTarget.form;
e.preventDefault();
openConfirmationModal(
"Are you sure you want to delete this integration?",
() => submit(form),
);
}}
onClick={() =>
props.setUpdateIntegrationModalData(props.integration)
}
>
<IconTrash />
<IconPencil />
</ActionIcon>
</Form>
</Group>
</Flex>
{integrationInputOpened ? (
<TextInput
value={integrationUrl}
readOnly
onClick={(e) => e.currentTarget.select()}
/>
) : null}
</Stack>
</Paper>
<Form method="POST" action={withQuery(".", { intent: "delete" })}>
<input
type="hidden"
name="integrationId"
defaultValue={props.integration.id}
/>
<ActionIcon
type="submit"
color="red"
variant="subtle"
mt={4}
onClick={(e) => {
const form = e.currentTarget.form;
e.preventDefault();
openConfirmationModal(
"Are you sure you want to delete this integration?",
() => submit(form),
);
}}
>
<IconTrash />
</ActionIcon>
</Form>
</Group>
</Flex>
{integrationUrlOpened ? (
<TextInput
value={integrationUrl}
readOnly
onClick={(e) => e.currentTarget.select()}
/>
) : null}
</Stack>
</Paper>
</>
);
};

Expand Down
4 changes: 4 additions & 0 deletions apps/frontend/app/routes/_dashboard.settings.preferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,10 @@ export default function Page() {
UserNotificationContent.NewWorkoutCreated,
() => "A new workout is created",
)
.with(
UserNotificationContent.IntegrationDisabledDueToTooManyErrors,
() => "Integration disabled due to too many errors",
)
.exhaustive()}
/>
))}
Expand Down
2 changes: 2 additions & 0 deletions crates/migrations/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ mod m20250201_changes_for_issue_1211;
mod m20250204_changes_for_issue_1231;
mod m20250208_changes_for_issue_1233;
mod m20250210_changes_for_issue_1217;
mod m20250210_changes_for_issue_1232;

pub use m20230404_create_user::User as AliasedUser;
pub use m20230410_create_metadata::Metadata as AliasedMetadata;
Expand Down Expand Up @@ -87,6 +88,7 @@ impl MigratorTrait for Migrator {
Box::new(m20250204_changes_for_issue_1231::Migration),
Box::new(m20250208_changes_for_issue_1233::Migration),
Box::new(m20250210_changes_for_issue_1217::Migration),
Box::new(m20250210_changes_for_issue_1232::Migration),
]
}
}
11 changes: 9 additions & 2 deletions crates/migrations/src/m20240607_create_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ pub enum Integration {
Provider,
CreatedOn,
IsDisabled,
TriggerResult,
LastFinishedAt,
MinimumProgress,
MaximumProgress,
LastTriggeredOn,
ProviderSpecifics,
SyncToOwnedCollection,
}
Expand All @@ -43,7 +44,13 @@ impl MigrationTrait for Migration {
.not_null()
.default(Expr::current_timestamp()),
)
.col(ColumnDef::new(Integration::LastTriggeredOn).timestamp_with_time_zone())
.col(
ColumnDef::new(Integration::TriggerResult)
.json_binary()
.not_null()
.default("[]"),
)
.col(ColumnDef::new(Integration::LastFinishedAt).timestamp_with_time_zone())
.col(ColumnDef::new(Integration::ProviderSpecifics).json_binary())
.col(ColumnDef::new(Integration::UserId).text().not_null())
.col(ColumnDef::new(Integration::MinimumProgress).decimal())
Expand Down
65 changes: 65 additions & 0 deletions crates/migrations/src/m20250210_changes_for_issue_1232.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
let db = manager.get_connection();
if !manager.has_column("integration", "trigger_result").await? {
db.execute_unprepared(
r#"
ALTER TABLE "integration" ADD COLUMN "trigger_result" JSONB NOT NULL DEFAULT '[]'::JSONB;
UPDATE "integration" i SET "trigger_result" = JSONB_BUILD_ARRAY(JSONB_BUILD_OBJECT('finished_at', i."last_triggered_on"))
WHERE i."last_triggered_on" IS NOT NULL;
"#,
)
.await?;
}
if !manager
.has_column("integration", "last_finished_at")
.await?
{
db.execute_unprepared(
r#"
ALTER TABLE "integration" ADD COLUMN "last_finished_at" TIMESTAMPTZ;
UPDATE "integration" i SET "last_finished_at" = i."last_triggered_on"
WHERE i."last_triggered_on" IS NOT NULL;
"#,
)
.await?;
}
if manager
.has_column("integration", "last_triggered_on")
.await?
{
db.execute_unprepared(r#"ALTER TABLE "integration" DROP COLUMN "last_triggered_on";"#)
.await?;
}
db.execute_unprepared(
r#"
UPDATE
"user"
SET
preferences = JSONB_SET(
preferences,
'{notifications,to_send}',
(preferences -> 'notifications' -> 'to_send') || '"IntegrationDisabledDueToTooManyErrors"'
)
where
NOT (
preferences -> 'notifications' -> 'to_send' ? 'IntegrationDisabledDueToTooManyErrors'
);
"#,
)
.await?;
Ok(())
}

async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
Ok(())
}
}
1 change: 1 addition & 0 deletions crates/models/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ pub enum UserNotificationContent {
PersonMetadataGroupAssociated,
MetadataNumberOfSeasonsChanged,
MetadataChaptersOrEpisodesChanged,
IntegrationDisabledDueToTooManyErrors,
}

#[derive(Debug, Serialize, Deserialize, SimpleObject, Clone)]
Expand Down
6 changes: 4 additions & 2 deletions crates/models/database/src/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use async_graphql::{InputObject, SimpleObject};
use async_trait::async_trait;
use enum_models::{IntegrationLot, IntegrationProvider};
use media_models::IntegrationProviderSpecifics;
use media_models::{IntegrationProviderSpecifics, IntegrationTriggerResult};
use nanoid::nanoid;
use sea_orm::{entity::prelude::*, ActiveValue};

Expand All @@ -24,9 +24,11 @@ pub struct Model {
pub provider: IntegrationProvider,
pub minimum_progress: Option<Decimal>,
pub maximum_progress: Option<Decimal>,
pub last_finished_at: Option<DateTimeUtc>,
pub sync_to_owned_collection: Option<bool>,
#[sea_orm(column_type = "Json")]
#[graphql(skip_input)]
pub last_triggered_on: Option<DateTimeUtc>,
pub trigger_result: Vec<IntegrationTriggerResult>,
#[sea_orm(column_type = "Json")]
#[graphql(skip)]
pub provider_specifics: Option<IntegrationProviderSpecifics>,
Expand Down
9 changes: 9 additions & 0 deletions crates/models/media/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,15 @@ pub struct PersonStateChanges {
pub metadata_groups_associated: HashSet<MediaAssociatedPersonStateChanges>,
}

#[skip_serializing_none]
#[derive(
Debug, Serialize, Deserialize, Clone, FromJsonQueryResult, Eq, PartialEq, Default, SimpleObject,
)]
pub struct IntegrationTriggerResult {
pub error: Option<String>,
pub finished_at: DateTimeUtc,
}

#[skip_serializing_none]
#[derive(
Debug,
Expand Down
Loading

0 comments on commit 7d42679

Please sign in to comment.