Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Barebones Analysis Display + Audit Logs #144

Merged
merged 2 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
10 changes: 10 additions & 0 deletions cmd/server/pactasrv/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ func (s *Server) ListAnalyses(ctx context.Context, request api.ListAnalysesReque
if err != nil {
return nil, oapierr.Internal("failed to query analyses", zap.Error(err))
}
if err := s.populateArtifactsInAnalyses(ctx, as...); err != nil {
return nil, err
}
artifacts := []*pacta.AnalysisArtifact{}
for _, a := range as {
artifacts = append(artifacts, a.Artifacts...)
}
if err := s.populateBlobsInAnalysisArtifacts(ctx, artifacts...); err != nil {
return nil, err
}
items, err := dereference(conv.AnalysesToOAPI(as))
if err != nil {
return nil, err
Expand Down
14 changes: 12 additions & 2 deletions cmd/server/pactasrv/conv/pacta_to_oapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,18 @@ func FileTypeToOAPI(ft pacta.FileType) (api.FileType, error) {
return api.FileTypeJSON, nil
case pacta.FileType_HTML:
return api.FileTypeHTML, nil
}
return "", fmt.Errorf("unknown file type: %q", ft)
case pacta.FileType_TEXT:
return api.FileTypeTEXT, nil
case pacta.FileType_CSS:
return api.FileTypeCSS, nil
case pacta.FileType_JS:
return api.FileTypeJS, nil
case pacta.FileType_TTF:
return api.FileTypeTTF, nil
case pacta.FileType_UNKNOWN:
return api.FileTypeUNKNOWN, nil
}
gbdubs marked this conversation as resolved.
Show resolved Hide resolved
return api.FileTypeUNKNOWN, nil
}

func BlobToOAPI(b *pacta.Blob) (*api.Blob, error) {
Expand Down
3 changes: 3 additions & 0 deletions db/sqldb/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ func (d *DB) Blob(tx db.Tx, id pacta.BlobID) (*pacta.Blob, error) {
}

func (d *DB) Blobs(tx db.Tx, ids []pacta.BlobID) (map[pacta.BlobID]*pacta.Blob, error) {
if len(ids) == 0 {
return map[pacta.BlobID]*pacta.Blob{}, nil
}
ids = dedupeIDs(ids)
rows, err := d.query(tx, `
SELECT `+blobSelectColumns+`
Expand Down
44 changes: 44 additions & 0 deletions frontend/components/analysis/Editor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script setup lang="ts">
import {
type EditorAnalysisFields as EditorFields,
type EditorAnalysisValues as EditorValues,
} from '@/lib/editor'

interface Props {
editorValues: EditorValues
editorFields: EditorFields
}
interface Emits {
(e: 'update:editorValues', evs: EditorValues): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()

const efs = computed(() => props.editorFields)
const evs = computed({
get: () => props.editorValues,
set: (evs) => { emit('update:editorValues', evs) },
})
</script>

<template>
<div>
<FormEditorField
:editor-field="efs.name"
:editor-value="evs.name"
>
<PVInputText
v-model="evs.name.currentValue"
/>
</FormEditorField>
<FormEditorField
:editor-field="efs.description"
:editor-value="evs.description"
>
<PVTextarea
v-model="evs.description.currentValue"
auto-resize
/>
</FormEditorField>
</div>
</template>
194 changes: 194 additions & 0 deletions frontend/components/analysis/ListView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
<script setup lang="ts">
import { analysisEditor } from '@/lib/editor'
import { type Portfolio, type PortfolioGroup, type Initiative, type Analysis } from '@/openapi/generated/pacta'
import { selectedCountSuffix } from '@/lib/selection'

const { public: { apiServerURL } } = useRuntimeConfig()
const {
humanReadableTimeFromStandardString,
} = useTime()
const pactaClient = usePACTA()
const { loading: { withLoading } } = useModal()
const i18n = useI18n()
const { t } = i18n

interface Props {
portfolios: Portfolio[]
portfolioGroups: PortfolioGroup[]
initiatives: Initiative[]
analyses: Analysis[]
selectedPortfolioIds: string[]
selectedPortfolioGroupIds: string[]
selectedAnalysisIds: string[]
}
const props = defineProps<Props>()
interface Emits {
(e: 'update:selectedPortfolioIds', value: string[]): void
(e: 'update:selectedPortfolioGroupIds', value: string[]): void
(e: 'update:selectedAnalysisIds', value: string[]): void
(e: 'refresh'): void
}
const emit = defineEmits<Emits>()

const refresh = () => { emit('refresh') }

const selectedAnalysisIDs = computed({
get: () => props.selectedAnalysisIds ?? [],
set: (value: string[]) => { emit('update:selectedAnalysisIds', value) },
})

interface EditorObject extends ReturnType<typeof analysisEditor> {
id: string
}

const prefix = 'components/analysis/ListView'
const tt = (s: string) => t(`${prefix}.${s}`)
const expandedRows = useState<EditorObject[]>(`${prefix}.expandedRows`, () => [])
const selectedRows = computed<EditorObject[]>({
get: () => {
const ids = selectedAnalysisIDs.value
return editorObjects.value.filter((editorObject) => ids.includes(editorObject.id))
},
set: (value: EditorObject[]) => {
const ids = value.map((row) => row.id)
ids.sort()
selectedAnalysisIDs.value = ids
},
})

const editorObjects = computed<EditorObject[]>(() => props.analyses.map((item) => ({ ...analysisEditor(item, i18n), id: item.id })))

const selectedAnalyses = computed<Analysis[]>(() => selectedRows.value.map((row) => row.currentValue.value))

const saveChanges = (id: string) => {
const index = editorObjects.value.findIndex((editorObject) => editorObject.id === id)
const eo = presentOrFileBug(editorObjects.value[index])
return withLoading(
() => pactaClient.updateAnalysis(id, eo.changes.value).then(refresh),
`${prefix}.saveChanges`,
)
}

const deleteAnalysis = (id: string) => withLoading(
() => pactaClient.deleteAnalysis(id),
`${prefix}.deleteAnalysis`,
)
const deleteSelected = () => Promise.all([selectedRows.value.map((row) => deleteAnalysis(row.id))]).then(refresh)
</script>

<template>
<div class="flex flex-column gap-3">
<div class="flex gap-2 flex-wrap">
<PVButton
icon="pi pi-refresh"
class="p-button-outlined p-button-secondary p-button-sm"
:label="tt('Refresh')"
@click="refresh"
/>
<PVButton
:disabled="!selectedRows || selectedRows.length === 0"
icon="pi pi-trash"
class="p-button-outlined p-button-danger p-button-sm"
:label="tt('Delete') + selectedCountSuffix(selectedRows)"
@click="deleteSelected"
/>
</div>
<PVDataTable
v-model:selection="selectedRows"
v-model:expanded-rows="expandedRows"
:value="editorObjects"
data-key="id"
size="small"
sort-field="editorValues.value.createdAt.originalValue"
:sort-order="-1"
>
<PVColumn selection-mode="multiple" />
<PVColumn
field="editorValues.value.createdAt.originalValue"
:header="tt('Created At')"
sortable
>
<template #body="slotProps">
{{ humanReadableTimeFromStandardString(slotProps.data.editorValues.value.createdAt.originalValue).value }}
gbdubs marked this conversation as resolved.
Show resolved Hide resolved
</template>
</PVColumn>
<PVColumn
field="editorValues.value.name.originalValue"
sortable
:header="tt('Name')"
/>
<PVColumn
:header="tt('View')"
>
<template #body="slotProps">
<LinkButton
icon="pi pi-external-link"
class="p-button-outlined p-button-xs"
:label="tt('View')"
:to="`${apiServerURL}/report/${slotProps.data.id}`"
gbdubs marked this conversation as resolved.
Show resolved Hide resolved
new-tab
/>
</template>
</PVColumn>
<PVColumn
expander
:header="tt('Details')"
/>
<template
#expansion="slotProps"
>
<div class="surface-100 p-3">
<h2 class="mt-0">
{{ tt('Metadata') }}
</h2>
<StandardDebug
always
:value="slotProps.data.currentValue.value"
label="Raw Data"
/>
<h2 class="mt-5">
{{ tt('Editable Properties') }}
</h2>
<AnalysisEditor
v-model:editor-values="slotProps.data.editorValues.value"
:editor-fields="slotProps.data.editorFields.value"
/>
<div class="flex gap-3 justify-content-between">
<PVButton
icon="pi pi-trash"
class="p-button-danger p-button-outlined"
:label="tt('Delete')"
@click="() => deleteAnalysis(slotProps.data.id)"
/>
<div v-tooltip.bottom="slotProps.data.saveTooltip">
<PVButton
:disabled="!slotProps.data.canSave.value"
:label="tt('Save Changes')"
icon="pi pi-save"
icon-pos="right"
@click="() => saveChanges(slotProps.data.id)"
/>
</div>
</div>
</div>
</template>
</PVDataTable>
<div class="flex flex-wrap gap-3 w-full justify-content-between">
<!-- TODO(grady) Hook this up to something. -->
<PVButton
class="p-button-outlined"
:label="tt('How To Run a Report')"
icon="pi pi-question-circle"
icon-pos="right"
/>
</div>
<StandardDebug
:value="selectedAnalyses"
label="Selected Analyses"
/>
<StandardDebug
:value="props.analyses"
label="All Analyses"
/>
</div>
</template>
38 changes: 27 additions & 11 deletions frontend/components/portfolio/ListView.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { portfolioEditor } from '@/lib/editor'
import { AnalysisType, type Portfolio, type PortfolioGroup, type Initiative } from '@/openapi/generated/pacta'
import { AnalysisType, type Portfolio, type PortfolioGroup, type Initiative, type Analysis } from '@/openapi/generated/pacta'
import { selectedCountSuffix } from '@/lib/selection'

const {
Expand All @@ -16,13 +16,16 @@ interface Props {
portfolios: Portfolio[]
portfolioGroups: PortfolioGroup[]
initiatives: Initiative[]
analyses: Analysis[]
selectedPortfolioIds: string[]
selectedPortfolioGroupIds: string[]
selectedAnalysisIds: string[]
}
const props = defineProps<Props>()
interface Emits {
(e: 'update:selectedPortfolioIds', value: string[]): void
(e: 'update:selectedPortfolioGroupIds', value: string[]): void
(e: 'update:selectedAnalysisIds', value: string[]): void
(e: 'refresh'): void
}
const emit = defineEmits<Emits>()
Expand Down Expand Up @@ -66,17 +69,20 @@ const saveChanges = (id: string) => {
)
}

const runAnalysis = (id: string) => {
const runAnalysis = (id: string, analysisType: AnalysisType) => {
const n = props.portfolios.find((p) => p.id === id)?.name ?? 'unknown'
return withLoading(
() => pactaClient.runAnalysis({
analysisType: AnalysisType.ANALYSIS_TYPE_REPORT,
name: 'Test Analysis!',
description: 'this is a test',
analysisType,
name: `${tt(analysisType)}: ${n}`,
description: `${tt(analysisType)} run at ${new Date().toLocaleString()} for portfolio ${n} (${id})`,
portfolioId: id,
}).then(() => { emit('refresh') }),
`${prefix}.saveChanges`,
}),
`${prefix}.runPortfolioAnalysis`,
)
}
const runAudit = (id: string) => runAnalysis(id, AnalysisType.ANALYSIS_TYPE_AUDIT)
const runReport = (id: string) => runAnalysis(id, AnalysisType.ANALYSIS_TYPE_REPORT)

const deletePortfolio = (id: string) => withLoading(
() => pactaClient.deletePortfolio(id),
Expand Down Expand Up @@ -211,6 +217,20 @@ const deleteSelected = () => Promise.all([selectedRows.value.map((row) => delete
@changed-memberships="refresh"
/>
</div>
<h2 class="mt-5">
{{ tt('Analyses') }}
</h2>
<PVMessage severity="warn">
TODO - filter analyses by those that match this portfolio
</PVMessage>
<PVButton
:label="tt('Run Audit')"
@click="() => runAudit(slotProps.data.id)"
/>
<PVButton
:label="tt('Run Report')"
@click="() => runReport(slotProps.data.id)"
/>
<h2 class="mt-5">
{{ tt('Editable Properties') }}
</h2>
Expand All @@ -234,10 +254,6 @@ const deleteSelected = () => Promise.all([selectedRows.value.map((row) => delete
@click="() => saveChanges(slotProps.data.id)"
/>
</div>
<PVButton
label="Test Run Analysis"
@click="() => runAnalysis(slotProps.data.id)"
/>
</div>
</div>
</template>
Expand Down
5 changes: 4 additions & 1 deletion frontend/components/portfolio/group/ListView.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { portfolioGroupEditor } from '@/lib/editor'
import { type Portfolio, type PortfolioGroup, type PortfolioGroupMembershipPortfolio } from '@/openapi/generated/pacta'
import { type Portfolio, type PortfolioGroup, type PortfolioGroupMembershipPortfolio, type Analysis } from '@/openapi/generated/pacta'
import { selectedCountSuffix } from '@/lib/selection'

const {
Expand All @@ -15,13 +15,16 @@ const { t } = i18n
interface Props {
portfolios: Portfolio[]
portfolioGroups: PortfolioGroup[]
analyses: Analysis[]
selectedPortfolioIds: string[]
selectedPortfolioGroupIds: string[]
selectedAnalysisIds: string[]
}
const props = defineProps<Props>()
interface Emits {
(e: 'update:selectedPortfolioIds', value: string[]): void
(e: 'update:selectedPortfolioGroupIds', value: string[]): void
(e: 'update:selectedAnalysisIds', value: string[]): void
(e: 'refresh'): void
}
const emit = defineEmits<Emits>()
Expand Down
Loading