Skip to content
This repository has been archived by the owner on Sep 30, 2024. It is now read-only.

Code insights: Implement delete insights with new dashboard functionality #22886

Merged
merged 2 commits into from
Jul 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { addInsightToDashboard, removeInsightFromDashboard } from '../dashboards'
import { addInsightToSettings, removeInsightFromSettings } from '../insights'

import { SettingsOperation, SettingsOperationType } from './edits'

/**
* Apply edit operation over jsonc settings string and return serialized final string
* with all applied edit operations.
*
* @param settings - original jsonc setting content
* @param operations - list of edit operations
*/
export function applyEditOperations(settings: string, operations: SettingsOperation[]): string {
vovakulikov marked this conversation as resolved.
Show resolved Hide resolved
let settingsContent: string = settings

for (const operation of operations) {
switch (operation.type) {
case SettingsOperationType.addInsight:
settingsContent = addInsightToSettings(settingsContent, operation.insight)
continue
case SettingsOperationType.removeInsight:
settingsContent = removeInsightFromSettings({
originalSettings: settingsContent,
insightID: operation.insightID,
})
continue
case SettingsOperationType.addInsightToDashboard:
settingsContent = addInsightToDashboard(
settingsContent,
operation.dashboardSettingKey,
operation.insightId
)
continue
case SettingsOperationType.removeInsightFromDashboard:
settingsContent = removeInsightFromDashboard(
settingsContent,
operation.dashboardSettingKey,
operation.insightId
)
}
}

return settingsContent
}
36 changes: 36 additions & 0 deletions client/web/src/insights/core/settings-action/edits/edits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Insight } from '../../types'

export enum SettingsOperationType {
addInsight,
removeInsight,
removeInsightFromDashboard,
addInsightToDashboard,
}

export interface RemoveInsight {
type: SettingsOperationType.removeInsight
subjectId: string
insightID: string
}

export interface AddInsight {
type: SettingsOperationType.addInsight
subjectId: string
insight: Insight
}

export interface RemoveInsightFromDashboard {
type: SettingsOperationType.removeInsightFromDashboard
subjectId: string
insightId: string
dashboardSettingKey: string
}

export interface AddInsightToDashboard {
type: SettingsOperationType.addInsightToDashboard
subjectId: string
insightId: string
dashboardSettingKey: string
}

export type SettingsOperation = AddInsight | RemoveInsight | AddInsightToDashboard | RemoveInsightFromDashboard
2 changes: 2 additions & 0 deletions client/web/src/insights/core/settings-action/edits/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './edits'
export { applyEditOperations } from './apply-edit-operations'
49 changes: 49 additions & 0 deletions client/web/src/insights/hooks/use-persist-edit-operations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { groupBy } from 'lodash'
import { useCallback, useContext } from 'react'

import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'

import { InsightsApiContext } from '../core/backend/api-provider'
import { applyEditOperations, SettingsOperation } from '../core/settings-action/edits'

interface UsePersistEditOperationsProps extends PlatformContextProps<'updateSettings'> {}

interface UsePersistEditOperationsOutput {
persist: (operations: SettingsOperation[]) => Promise<void>
}

/**
* This react hook simplifies persist (update logic) over the settings cascade subject's setting file.
*/
export function usePersistEditOperations(props: UsePersistEditOperationsProps): UsePersistEditOperationsOutput {
const { platformContext } = props
const { getSubjectSettings, updateSubjectSettings } = useContext(InsightsApiContext)

const persist = useCallback(
async (operations: SettingsOperation[]) => {
const subjectsToUpdate = groupBy(operations, operation => operation.subjectId)

const subjectUpdateRequests = Object.keys(subjectsToUpdate).map(subjectId => {
async function updateSettings(): Promise<void> {
const editOperations = subjectsToUpdate[subjectId]

// Get jsonc subject settings file.
const settings = await getSubjectSettings(subjectId).toPromise()

// Modify this jsonc file according to this subject's operations
const nextSubjectSettings = applyEditOperations(settings.contents, editOperations)

// Call the async update mutation for the new subject's settings file
await updateSubjectSettings(platformContext, subjectId, nextSubjectSettings).toPromise()
}

return updateSettings()
})

await Promise.all(subjectUpdateRequests)
},
[platformContext, getSubjectSettings, updateSubjectSettings]
)

return { persist }
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ export const DashboardsContent: React.FunctionComponent<DashboardsContentProps>
insightIds={currentDashboard.insightIds}
extensionsController={extensionsController}
telemetryService={telemetryService}
platformContext={platformContext}
settingsCascade={settingsCascade}
/>
) : (
<HeroPage icon={MapSearchIcon} title="Hmm, the dashboard wasn't found." />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,30 @@ import React, { useContext, useMemo } from 'react'
import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
import { haveInitialExtensionsLoaded } from '@sourcegraph/shared/src/api/features'
import { ExtensionsControllerProps } from '@sourcegraph/shared/src/extensions/controller'
import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
import { SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { useObservable } from '@sourcegraph/shared/src/util/useObservable'

import { Settings } from '../../../../../../../../schema/settings.schema'
import { CodeInsightsIcon, InsightsViewGrid } from '../../../../../../../components'
import { InsightsApiContext } from '../../../../../../../core/backend/api-provider'
import { useDeleteInsight } from '../../../../../../insights/insights-page/hooks/use-delete-insight'
import { EmptyInsightDashboard } from '../empty-insight-dashboard/EmptyInsightDashboard'

interface DashboardInsightsProps extends ExtensionsControllerProps, TelemetryProps {
interface DashboardInsightsProps
extends ExtensionsControllerProps,
TelemetryProps,
SettingsCascadeProps<Settings>,
PlatformContextProps<'updateSettings'> {
/**
* Dashboard specific insight ids.
*/
insightIds?: string[]
}

export const DashboardInsights: React.FunctionComponent<DashboardInsightsProps> = props => {
const { telemetryService, extensionsController, insightIds = [] } = props
const { telemetryService, extensionsController, insightIds = [], settingsCascade, platformContext } = props
const { getInsightCombinedViews } = useContext(InsightsApiContext)

const views = useObservable(
Expand All @@ -30,6 +38,8 @@ export const DashboardInsights: React.FunctionComponent<DashboardInsightsProps>
])
)

const { handleDelete } = useDeleteInsight({ settingsCascade, platformContext })

// Ensures that we don't show a misleading empty state when extensions haven't loaded yet.
const areExtensionsReady = useObservable(
useMemo(() => haveInitialExtensionsLoaded(props.extensionsController.extHostAPI), [props.extensionsController])
Expand Down Expand Up @@ -58,7 +68,12 @@ export const DashboardInsights: React.FunctionComponent<DashboardInsightsProps>
return (
<div>
{insightIds.length > 0 && views.length > 0 ? (
<InsightsViewGrid views={views} hasContextMenu={true} telemetryService={telemetryService} />
<InsightsViewGrid
views={views}
hasContextMenu={true}
telemetryService={telemetryService}
onDelete={handleDelete}
/>
) : (
<EmptyInsightDashboard />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const EditLangStatsInsight: React.FunctionComponent<EditLangStatsInsightP
}

const handleCancel = (): void => {
history.push('/insights')
history.push(`/insights/dashboards/${insight.visibility}`)
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const EditSearchBasedInsight: React.FunctionComponent<EditSearchBasedInsi
}

const handleCancel = (): void => {
history.push('/insights')
history.push(`/insights/dashboards/${insight.visibility}`)
}

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useContext } from 'react'
import { useHistory } from 'react-router-dom'

import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context'
Expand All @@ -7,10 +6,10 @@ import { asError } from '@sourcegraph/shared/src/util/errors'

import { Settings } from '../../../../../schema/settings.schema'
import { FORM_ERROR, SubmissionErrors } from '../../../../components/form/hooks/useForm'
import { InsightsApiContext } from '../../../../core/backend/api-provider'
import { Insight } from '../../../../core/types'
import { usePersistEditOperations } from '../../../../hooks/use-persist-edit-operations'

import { applyEditOperations, getUpdatedSubjectSettings } from './utils'
import { getUpdatedSubjectSettings } from './utils'

export interface UseHandleSubmitProps extends PlatformContextProps<'updateSettings'>, SettingsCascadeProps<Settings> {
originalInsight: Insight | null
Expand All @@ -26,39 +25,22 @@ export interface useHandleSubmitOutput {
*/
export function useHandleSubmit(props: UseHandleSubmitProps): useHandleSubmitOutput {
const { originalInsight, platformContext, settingsCascade } = props
const { getSubjectSettings, updateSubjectSettings } = useContext(InsightsApiContext)
const history = useHistory()
const { persist } = usePersistEditOperations({ platformContext })

const handleEditInsightSubmit = async (newInsight: Insight): Promise<SubmissionErrors> => {
if (!originalInsight) {
return
}

try {
const subjectsToUpdate = getUpdatedSubjectSettings({
const editOperations = getUpdatedSubjectSettings({
oldInsight: originalInsight,
newInsight,
settingsCascade,
})

const subjectUpdateRequests = Object.keys(subjectsToUpdate).map(subjectId => {
async function updateSettings(): Promise<void> {
const editOperations = subjectsToUpdate[subjectId]

// Get jsonc subject settings file.
const settings = await getSubjectSettings(subjectId).toPromise()

// Modify this jsonc file according to this subject's operations
const nextSubjectSettings = applyEditOperations(settings.contents, editOperations)

// Call the async update mutation for the new subject's settings file
await updateSubjectSettings(platformContext, subjectId, nextSubjectSettings).toPromise()
}

return updateSettings()
})

await Promise.all(subjectUpdateRequests)
await persist(editOperations)

// Navigate user to the dashboard page with new created dashboard
history.push(`/insights/dashboards/${newInsight.visibility}`)
Expand Down
Loading