Skip to content

Commit

Permalink
Auto-read and prune notifications (#1160)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeljguarino authored Jul 17, 2023
1 parent fe34b9f commit 841bc70
Show file tree
Hide file tree
Showing 17 changed files with 145 additions and 13 deletions.
3 changes: 2 additions & 1 deletion apps/core/lib/core/schema/invite.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule Core.Schema.Invite do
schema "invites" do
field :email, :string
field :secure_id, :string
field :admin, :boolean

belongs_to :user, User
belongs_to :account, Account
Expand All @@ -26,7 +27,7 @@ defmodule Core.Schema.Invite do
from(i in query, where: i.account_id == ^aid)
end

@valid ~w(email account_id user_id)a
@valid ~w(email account_id user_id admin)a

def changeset(model, attrs \\ %{}) do
model
Expand Down
7 changes: 7 additions & 0 deletions apps/core/lib/core/schema/notification.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ defmodule Core.Schema.Notification do

defenum Type, message: 0, incident_update: 1, mention: 2, locked: 3, pending: 4

@expiry [months: -1]

schema "notifications" do
field :type, Type
field :msg, :binary
Expand All @@ -18,6 +20,11 @@ defmodule Core.Schema.Notification do
timestamps()
end

def expired(query \\ __MODULE__) do
expires_at = Timex.now() |> Timex.shift(@expiry)
from(n in query, where: n.inserted_at <= ^expires_at)
end

def for_type(query \\ __MODULE__, type) do
from(n in query, where: n.type == ^type)
end
Expand Down
5 changes: 4 additions & 1 deletion apps/core/lib/core/schema/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,10 @@ defmodule Core.Schema.User do
|> cast_embed(:onboarding_checklist, with: &onboarding_checklist_changeset/2)
end

def invite_changeset(model, attrs \\ %{}), do: base_changeset(model, attrs, :primary)
def invite_changeset(model, attrs \\ %{}) do
base_changeset(model, attrs, :primary)
|> cast_embed(:roles, with: &roles_changeset/2)
end

def base_changeset(model, attrs, mode) do
model
Expand Down
5 changes: 3 additions & 2 deletions apps/core/lib/core/services/accounts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,8 @@ defmodule Core.Services.Accounts do
"""
@spec realize_invite(map, binary) :: user_resp
def realize_invite(attributes, invite_id) do
invite = get_invite!(invite_id) |> Core.Repo.preload([:groups, user: :account])
invite = get_invite!(invite_id)
|> Core.Repo.preload([:groups, user: :account])

start_transaction()
|> add_operation(:user, fn _ ->
Expand All @@ -441,7 +442,7 @@ defmodule Core.Services.Accounts do
end)
|> add_operation(:upsert, fn %{user: user} ->
user
|> User.invite_changeset(Map.put(attributes, :email, invite.email))
|> User.invite_changeset(Map.merge(attributes, %{email: invite.email, roles: %{admin: invite.admin}}))
|> Ecto.Changeset.change(%{account_id: invite.account_id})
|> Core.Repo.insert_or_update()
end)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule Core.Repo.Migrations.InviteAdmin do
use Ecto.Migration

def change do
alter table(:invites) do
add :admin, :boolean, default: false
end
end
end
18 changes: 17 additions & 1 deletion apps/core/test/services/accounts_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,22 @@ defmodule Core.Services.AccountsTest do
assert_receive {:event, %PubSub.UserCreated{item: ^user}}
end

test "it can mark admins if selected", %{user: user, account: account} do
{:ok, invite} = Accounts.create_invite(%{email: "[email protected]", admin: true}, user)

{:ok, user} = Accounts.realize_invite(%{
password: "some long password",
name: "Some User"
}, invite.secure_id)

assert user.email == invite.email
assert user.account_id == account.id
assert user.name == "Some User"
assert user.roles.admin

assert_receive {:event, %PubSub.UserCreated{item: ^user}}
end

test "it can bind users to groups when realizing an invite", %{user: user, account: account} do
groups = insert_list(2, :group, account: account)
{:ok, invite} = Accounts.create_invite(%{
Expand Down Expand Up @@ -418,7 +434,7 @@ defmodule Core.Services.AccountsTest do
assert user.email == invite.email
assert user.account_id == account.id
assert user.name == "Some User"
refute user.roles
refute user.roles.admin

assert_receive {:event, %PubSub.UserCreated{item: ^user}}
end
Expand Down
12 changes: 12 additions & 0 deletions apps/cron/lib/cron/prune/notifications.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule Cron.Prune.Notifications do
@moduledoc """
Wipes invites beyond the expiration time
"""
use Cron
alias Core.Schema.Notification

def run() do
Notification.expired()
|> Core.Repo.delete_all()
end
end
6 changes: 3 additions & 3 deletions apps/cron/test/cron/prune/invites_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ defmodule Cron.Prune.InvitesTest do
alias Cron.Prune.Invites

describe "#run/0" do
test "it will prune old, unused upgrade queues" do
test "it will prune old, unused invites" do
old = insert_list(3, :invite, inserted_at: Timex.now() |> Timex.shift(days: -3))
inv = insert(:invite)

{3, _} = Invites.run()

for q <- old,
do: refute refetch(q)
for i <- old,
do: refute refetch(i)

assert refetch(inv)
end
Expand Down
18 changes: 18 additions & 0 deletions apps/cron/test/cron/prune/notifications_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
defmodule Cron.Prune.NotificationsTest do
use Core.SchemaCase
alias Cron.Prune.Notifications

describe "#run/0" do
test "it will prune old notifications" do
old = insert_list(3, :notification, inserted_at: Timex.now() |> Timex.shift(months: -2))
notif = insert(:notification)

{3, _} = Notifications.run()

for n <- old,
do: refute refetch(n)

assert refetch(notif)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<% end %>
<%= space() %>
<%= row do %>
<% text do %>
<%= text do %>
To apply manual upgrades, you'll need to run these commands in your git repo for each application,
replace APP with the name of each app:
<% end %>
Expand Down
4 changes: 3 additions & 1 deletion apps/graphql/lib/graphql/schema/account.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ defmodule GraphQl.Schema.Account do
end

input_object :invite_attributes do
field :email, :string
field :email, :string
field :admin, :boolean
field :invite_groups, list_of(:binding_attributes)
end

Expand Down Expand Up @@ -125,6 +126,7 @@ defmodule GraphQl.Schema.Account do

object :invite do
field :id, non_null(:id)
field :admin, :boolean
field :secure_id, :string, resolve: fn
%{user_id: id}, _, _ when is_binary(id) -> {:ok, nil} # obfuscate for existing users
%{secure_id: id}, _, _ -> {:ok, id}
Expand Down
2 changes: 1 addition & 1 deletion plural/helm/plural/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apiVersion: v2
name: plural
description: A helm chart for installing plural
appVersion: 0.10.25
version: 0.10.25
version: 0.10.26
dependencies:
- name: hydra
version: 0.26.5
Expand Down
4 changes: 4 additions & 0 deletions plural/helm/plural/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,10 @@ crons:
cronModule: Prune.Trials
cronTab: "40 0 * * *"
envVars: []
- cronName: plrl-prune-notifs
cronModule: Prune.Notifications
cronTab: "15 1 * * *"
envVars: []

hydraSecrets:
dsn: memory
Expand Down
2 changes: 2 additions & 0 deletions schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,7 @@ input FileAttributes {

input InviteAttributes {
email: String
admin: Boolean
inviteGroups: [BindingAttributes]
}

Expand Down Expand Up @@ -1176,6 +1177,7 @@ type DnsRecordEdge {

type Invite {
id: ID!
admin: Boolean
secureId: String
existing: Boolean!
email: String
Expand Down
16 changes: 14 additions & 2 deletions www/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import { clearLocalStorage } from '../../helpers/localStorage'

import Cookiebot from '../../utils/cookiebot'

import { useReadNotificationsMutation } from '../../generated/graphql'

import { useNotificationsCount } from './WithNotifications'
import { NotificationsPanelOverlay } from './NotificationsPanelOverlay'

Expand Down Expand Up @@ -157,7 +159,17 @@ function Sidebar(props: ComponentProps<typeof DSSidebar>) {
isActiveMenuItem(menuItem, pathname),
[pathname]
)
const [readNotifications] = useReadNotificationsMutation()
const notificationsCount = useNotificationsCount()
const toggleNotifPanel = useCallback(
(open) => {
setIsNotificationsPanelOpen(open)
if (!open) {
readNotifications()
}
},
[setIsNotificationsPanelOpen, readNotifications]
)

useOutsideClick(menuRef, (event) => {
if (!menuItemRef.current?.contains(event.target as any)) {
Expand Down Expand Up @@ -249,7 +261,7 @@ function Sidebar(props: ComponentProps<typeof DSSidebar>) {
className="sidebar-notifications"
onClick={(event) => {
event.stopPropagation()
setIsNotificationsPanelOpen((isOpen) => !isOpen)
toggleNotifPanel((isOpen) => !isOpen)
}}
backgroundColor={
isNotificationsPanelOpen
Expand Down Expand Up @@ -379,7 +391,7 @@ function Sidebar(props: ComponentProps<typeof DSSidebar>) {
<NotificationsPanelOverlay
leftOffset={sidebarWidth}
isOpen={isNotificationsPanelOpen}
setIsOpen={setIsNotificationsPanelOpen}
setIsOpen={toggleNotifPanel}
/>
{/* ---
CREATE PUBLISHER MODAL
Expand Down
41 changes: 41 additions & 0 deletions www/src/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1371,6 +1371,7 @@ export type IntegrationWebhookEdge = {
export type Invite = {
__typename?: 'Invite';
account?: Maybe<Account>;
admin?: Maybe<Scalars['Boolean']['output']>;
email?: Maybe<Scalars['String']['output']>;
existing: Scalars['Boolean']['output'];
expiresAt?: Maybe<Scalars['DateTime']['output']>;
Expand All @@ -1383,6 +1384,7 @@ export type Invite = {
};

export type InviteAttributes = {
admin?: InputMaybe<Scalars['Boolean']['input']>;
email?: InputMaybe<Scalars['String']['input']>;
inviteGroups?: InputMaybe<Array<InputMaybe<BindingAttributes>>>;
};
Expand Down Expand Up @@ -5859,6 +5861,13 @@ export type ResetTokenQueryVariables = Exact<{

export type ResetTokenQuery = { __typename?: 'RootQueryType', resetToken?: { __typename?: 'ResetToken', type: ResetTokenType, user: { __typename?: 'User', id: string, name: string, email: string, avatar?: string | null, provider?: Provider | null, demoed?: boolean | null, onboarding?: OnboardingState | null, emailConfirmed?: boolean | null, emailConfirmBy?: Date | null, backgroundColor?: string | null, serviceAccount?: boolean | null, onboardingChecklist?: { __typename?: 'OnboardingChecklist', dismissed?: boolean | null, status?: OnboardingChecklistState | null } | null, roles?: { __typename?: 'Roles', admin?: boolean | null } | null } } | null };

export type ReadNotificationsMutationVariables = Exact<{
incidentId?: InputMaybe<Scalars['ID']['input']>;
}>;


export type ReadNotificationsMutation = { __typename?: 'RootMutationType', readNotifications?: number | null };

export type VersionTagFragment = { __typename?: 'VersionTag', id: string, tag: string, version?: { __typename?: 'Version', id: string } | null };

export type VersionFragment = { __typename?: 'Version', id: string, helm?: Map<string, unknown> | null, readme?: string | null, valuesTemplate?: string | null, version: string, insertedAt?: Date | null, package?: string | null, crds?: Array<{ __typename?: 'Crd', id: string, name: string, blob?: string | null } | null> | null, chart?: { __typename?: 'Chart', id?: string | null, name: string, description?: string | null, latestVersion?: string | null, insertedAt?: Date | null, dependencies?: { __typename?: 'Dependencies', wait?: boolean | null, application?: boolean | null, providers?: Array<Provider | null> | null, secrets?: Array<string | null> | null, providerWirings?: Map<string, unknown> | null, outputs?: Map<string, unknown> | null, dependencies?: Array<{ __typename?: 'Dependency', name?: string | null, repo?: string | null, type?: DependencyType | null, version?: string | null, optional?: boolean | null } | null> | null, wirings?: { __typename?: 'Wirings', terraform?: Map<string, unknown> | null, helm?: Map<string, unknown> | null } | null } | null } | null, terraform?: { __typename?: 'Terraform', id?: string | null, name?: string | null } | null, dependencies?: { __typename?: 'Dependencies', wait?: boolean | null, application?: boolean | null, providers?: Array<Provider | null> | null, secrets?: Array<string | null> | null, providerWirings?: Map<string, unknown> | null, outputs?: Map<string, unknown> | null, dependencies?: Array<{ __typename?: 'Dependency', name?: string | null, repo?: string | null, type?: DependencyType | null, version?: string | null, optional?: boolean | null } | null> | null, wirings?: { __typename?: 'Wirings', terraform?: Map<string, unknown> | null, helm?: Map<string, unknown> | null } | null } | null };
Expand Down Expand Up @@ -10559,6 +10568,37 @@ export function useResetTokenLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions
export type ResetTokenQueryHookResult = ReturnType<typeof useResetTokenQuery>;
export type ResetTokenLazyQueryHookResult = ReturnType<typeof useResetTokenLazyQuery>;
export type ResetTokenQueryResult = Apollo.QueryResult<ResetTokenQuery, ResetTokenQueryVariables>;
export const ReadNotificationsDocument = gql`
mutation ReadNotifications($incidentId: ID) {
readNotifications(incidentId: $incidentId)
}
`;
export type ReadNotificationsMutationFn = Apollo.MutationFunction<ReadNotificationsMutation, ReadNotificationsMutationVariables>;

/**
* __useReadNotificationsMutation__
*
* To run a mutation, you first call `useReadNotificationsMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useReadNotificationsMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [readNotificationsMutation, { data, loading, error }] = useReadNotificationsMutation({
* variables: {
* incidentId: // value for 'incidentId'
* },
* });
*/
export function useReadNotificationsMutation(baseOptions?: Apollo.MutationHookOptions<ReadNotificationsMutation, ReadNotificationsMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ReadNotificationsMutation, ReadNotificationsMutationVariables>(ReadNotificationsDocument, options);
}
export type ReadNotificationsMutationHookResult = ReturnType<typeof useReadNotificationsMutation>;
export type ReadNotificationsMutationResult = Apollo.MutationResult<ReadNotificationsMutation>;
export type ReadNotificationsMutationOptions = Apollo.BaseMutationOptions<ReadNotificationsMutation, ReadNotificationsMutationVariables>;
export const UpdateVersionDocument = gql`
mutation UpdateVersion($spec: VersionSpec, $attributes: VersionAttributes!) {
updateVersion(spec: $spec, attributes: $attributes) {
Expand Down Expand Up @@ -10690,6 +10730,7 @@ export const namedOperations = {
AcceptLogin: 'AcceptLogin',
CreateResetToken: 'CreateResetToken',
RealizeResetToken: 'RealizeResetToken',
ReadNotifications: 'ReadNotifications',
UpdateVersion: 'UpdateVersion'
},
Fragment: {
Expand Down
4 changes: 4 additions & 0 deletions www/src/graph/users.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,7 @@ query ResetToken($id: ID!) {
}
}
}

mutation ReadNotifications($incidentId: ID) {
readNotifications(incidentId: $incidentId)
}

0 comments on commit 841bc70

Please sign in to comment.