Skip to content

Commit

Permalink
Various Report Buttons: View, Download + Run
Browse files Browse the repository at this point in the history
  • Loading branch information
gbdubs committed Jan 19, 2024
1 parent 8d8c66a commit 6315d2a
Show file tree
Hide file tree
Showing 24 changed files with 565 additions and 95 deletions.
6 changes: 6 additions & 0 deletions cmd/server/pactasrv/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ func (s *Server) ListAnalyses(ctx context.Context, request api.ListAnalysesReque
if err := s.populateBlobsInAnalysisArtifacts(ctx, artifacts...); err != nil {
return nil, err
}
if err := s.populateSnapshotsInAnalyses(ctx, as...); err != nil {
return nil, err
}
items, err := dereference(conv.AnalysesToOAPI(as))
if err != nil {
return nil, err
Expand Down Expand Up @@ -76,6 +79,9 @@ func (s *Server) FindAnalysisById(ctx context.Context, request api.FindAnalysisB
if err := s.populateBlobsInAnalysisArtifacts(ctx, a.Artifacts...); err != nil {
return nil, err
}
if err := s.populateSnapshotsInAnalyses(ctx, a); err != nil {
return nil, err
}
converted, err := conv.AnalysisToOAPI(a)
if err != nil {
return nil, err
Expand Down
1 change: 1 addition & 0 deletions cmd/server/pactasrv/conv/pacta_to_oapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@ func AnalysisToOAPI(a *pacta.Analysis) (*api.Analysis, error) {
FailureCode: fc,
FailureMessage: fm,
Artifacts: dereferenceAll(aas),
OwnerId: string(a.Owner.ID),
}, nil
}

Expand Down
1 change: 1 addition & 0 deletions cmd/server/pactasrv/pactasrv.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ type DB interface {
CreateSnapshotOfPortfolio(tx db.Tx, pID pacta.PortfolioID) (pacta.PortfolioSnapshotID, error)
CreateSnapshotOfPortfolioGroup(tx db.Tx, pgID pacta.PortfolioGroupID) (pacta.PortfolioSnapshotID, error)
CreateSnapshotOfInitiative(tx db.Tx, iID pacta.InitiativeID) (pacta.PortfolioSnapshotID, error)
PortfolioSnapshots(tx db.Tx, ids []pacta.PortfolioSnapshotID) (map[pacta.PortfolioSnapshotID]*pacta.PortfolioSnapshot, error)

GetOwnerForUser(tx db.Tx, uID pacta.UserID) (pacta.OwnerID, error)

Expand Down
19 changes: 19 additions & 0 deletions cmd/server/pactasrv/populate.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,25 @@ func (s *Server) populateArtifactsInAnalyses(
return nil
}

func (s *Server) populateSnapshotsInAnalyses(
ctx context.Context,
ts ...*pacta.Analysis,
) error {
getFn := func(a *pacta.Analysis) ([]*pacta.PortfolioSnapshot, error) {
return []*pacta.PortfolioSnapshot{a.PortfolioSnapshot}, nil
}
lookupFn := func(ids []pacta.PortfolioSnapshotID) (map[pacta.PortfolioSnapshotID]*pacta.PortfolioSnapshot, error) {
return s.DB.PortfolioSnapshots(s.DB.NoTxn(ctx), ids)
}
getIDFn := func(a *pacta.PortfolioSnapshot) pacta.PortfolioSnapshotID {
return a.ID
}
if err := populateAll(ts, getFn, getIDFn, lookupFn); err != nil {
return oapierr.Internal("populating portfolio snapshots in analysis failed", zap.Error(err))
}
return nil
}

func (s *Server) populateBlobsInPortfolios(
ctx context.Context,
ps ...*pacta.Portfolio,
Expand Down
12 changes: 10 additions & 2 deletions cmd/server/pactasrv/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,19 @@ func (s *Server) FindUserByMe(ctx context.Context, request api.FindUserByMeReque
if err != nil {
return nil, oapierr.Internal("failed to retrieve user", zap.Error(err))
}
result, err := conv.UserToOAPI(user)
ownerID, err := s.DB.GetOwnerForUser(s.DB.NoTxn(ctx), meID)
if err != nil {
return nil, oapierr.Internal("failed to retrieve owner for user", zap.Error(err))
}
apiUser, err := conv.UserToOAPI(user)
if err != nil {
return nil, err
}
return api.FindUserByMe200JSONResponse(*result), nil
result := api.FindUserByMe200JSONResponse{
User: apiUser,
OwnerId: ptr(string(ownerID)),
}
return result, nil
}

// a callback after login to create or return the user
Expand Down
3 changes: 3 additions & 0 deletions frontend/components/LinkButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface Props {
loadingIcon?: string
activeClass?: string
inactiveClass?: string
external?: boolean
}
const props = withDefaults(defineProps<Props>(), {
to: undefined,
Expand All @@ -49,6 +50,7 @@ const props = withDefaults(defineProps<Props>(), {
loadingIcon: 'pi pi-spinner pi-spin',
activeClass: '',
inactiveClass: '',
external: undefined,
})
const attrs = useAttrs()
Expand Down Expand Up @@ -156,6 +158,7 @@ const href = computed(() => {
:target="target"
:to="to"
:aria-disabled="disabled"
:external="props.external"
custom
>
<a
Expand Down
96 changes: 96 additions & 0 deletions frontend/components/analysis/AccessButtons.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<script setup lang="ts">
import { type Analysis, type AccessBlobContentReqItem, type AccessBlobContentResp } from '@/openapi/generated/pacta'
import JSZip from 'jszip'
const { t } = useI18n()
const { public: { apiServerURL } } = useRuntimeConfig()
const pactaClient = usePACTA()
const { getMaybeMe } = useSession()
const { isAdmin, isSuperAdmin, maybeMeOwnerId } = await getMaybeMe()
interface Props {
analysis: Analysis
}
const props = defineProps<Props>()
const prefix = 'components/analysis/AccessButtons'
const statePrefix = `${prefix}[${useStateIDGenerator().id()}]`
const tt = (key: string) => t(`${prefix}.${key}`)
const canAccessAsPublic = computed(() => props.analysis.artifacts.every((asset) => asset.sharedToPublic))
const canAccessAsAdmin = computed(() => {
if (isAdmin.value || isSuperAdmin.value) {
return props.analysis.artifacts.every((asset) => asset.adminDebugEnabled)
}
return false
})
const canAccessAsOwner = computed(() => {
if (maybeMeOwnerId.value) {
return maybeMeOwnerId.value === props.analysis.ownerId
}
return false
})
const canAccess = computed(() => {
return canAccessAsPublic.value || canAccessAsAdmin.value || canAccessAsOwner.value
})
const downloadInProgress = useState<boolean>(`${statePrefix}.downloadInProgress`, () => false)
const doDownload = async () => {
downloadInProgress.value = true
await pactaClient.accessBlobContent({
items: props.analysis.artifacts.map((asset): AccessBlobContentReqItem => ({
blobId: asset.blob.id,
})),
}).then(async (response: AccessBlobContentResp) => {
const zip = new JSZip()
await Promise.all(response.items.map(
async (item): Promise<void> => {
const response = await fetch(item.downloadUrl)
const data = await response.blob()
const blob = presentOrFileBug(props.analysis.artifacts.find((artifact) => artifact.blob.id === item.blobId)).blob
const fileName = `${blob.fileName}`
zip.file(fileName, data)
}),
)
return await zip.generateAsync({ type: 'blob' })
}).then((content) => {
const element = document.createElement('a')
element.href = URL.createObjectURL(content)
const fileName = `${props.analysis.name}.zip`
element.download = fileName
document.body.appendChild(element)
element.click()
document.body.removeChild(element)
downloadInProgress.value = false
})
}
const openReport = () => navigateTo(`${apiServerURL}/report/${props.analysis.id}/`, {
open: {
target: '_blank',
},
external: true,
})
</script>

<template>
<div
v-tooltip="canAccess ? undefined : tt('Denied')"
class="flex gap-1 align-items-center w-fit"
>
<PVButton
icon="pi pi-external-link"
:disabled="!canAccess"
class="p-button-secondary p-button-outlined p-button-xs"
:label="tt('View')"
@click="openReport"
/>
<PVButton
v-tooltip="canAccess ? tt('Download') : ''"
:disabled="downloadInProgress || !canAccess"
:loading="downloadInProgress"
:icon="downloadInProgress ? 'pi pi-spinner pi-spin' : 'pi pi-download'"
class="p-button-secondary p-button-text p-button-xs"
@click="doDownload"
/>
</div>
</template>
13 changes: 3 additions & 10 deletions frontend/components/analysis/ListView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ 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 { humanReadableTimeFromStandardString } = useTime()
const pactaClient = usePACTA()
const { loading: { withLoading } } = useModal()
const i18n = useI18n()
Expand Down Expand Up @@ -121,12 +118,8 @@ const deleteSelected = () => Promise.all([selectedRows.value.map((row) => delete
: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}/`"
new-tab
<AnalysisAccessButtons
:analysis="slotProps.data.currentValue.value"
/>
</template>
</PVColumn>
Expand Down
126 changes: 126 additions & 0 deletions frontend/components/analysis/RunButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<script setup lang="ts">
import { type RunAnalysisReq, type Analysis, type AnalysisType } from '@/openapi/generated/pacta'
const pactaClient = usePACTA()
const { loading: { withLoading } } = useModal()
const localePath = useLocalePath()
const i18n = useI18n()
const { t } = i18n
const prefix = 'components/analysis/RunButton'
const statePrefix = `${prefix}[${useStateIDGenerator().id()}]`
const tt = (key: string) => t(`${prefix}.${key}`)
interface Props {
analysisType: AnalysisType
name: string
portfolioGroupId?: string
portfolioId?: string
initiativeId?: string
}
const props = defineProps<Props>()
interface Emits {
(e: 'started'): void
(e: 'finished'): void
}
const emit = defineEmits<Emits>()
const clicked = useState<boolean>(`${statePrefix}.clicked`, () => false)
const analysisId = useState<string | null>(`${statePrefix}.analysisId`, () => null)
const analysis = useState<Analysis | null>(`${statePrefix}.analysis`, () => null)
const request = computed<RunAnalysisReq>(() => {
const common = {
analysisType: props.analysisType,
name: `${tt(props.analysisType)}: ${props.name}`,
description: `${tt(props.analysisType)} run at ${new Date().toLocaleString()}`,
}
if (props.portfolioId) {
return {
...common,
portfolioId: props.portfolioId,
}
} else if (props.portfolioGroupId) {
return {
...common,
portfolioGroupId: props.portfolioGroupId,
}
} else if (props.initiativeId) {
return {
...common,
initiativeId: props.initiativeId,
}
} else {
throw new Error('No portfolio, portfolio group or initiative ID provided')
}
})
const runAnalysis = () => {
emit('started')
return withLoading(
() => pactaClient.runAnalysis(request.value)
.then((resp) => { analysisId.value = resp.analysisId })
.then(() => { void refreshAnalysisState() }),
`${prefix}.runAnalysis`,
)
}
const refreshAnalysisState = async () => {
const aid = analysisId.value
if (!aid) {
console.warn('No analysis ID set, but refresh requested')
return
}
const resp = await pactaClient.findAnalysisById(aid)
analysis.value = resp
if (analysis.value?.completedAt) {
emit('finished')
return
}
setTimeout(() => { void refreshAnalysisState }, 2000)
}
const analysisCompleted = computed(() => analysis.value?.completedAt)
const runBtnVisible = computed(() => !analysisCompleted.value)
const runBtnDisabled = computed(() => analysisId.value !== null || clicked.value)
const runBtnLoading = computed(() => runBtnDisabled.value)
const runBtnIcon = computed(() => analysisId.value ? 'pi pi-spin pi-spinner' : 'pi pi-play')
const runBtnLabel = computed(() => {
if (!clicked.value) {
return tt('Run') + ' ' + tt(props.analysisType)
}
if (!analysisId.value) {
return tt('Starting') + ' ' + tt(props.analysisType) + '...'
}
if (analysis.value) {
return tt('Running') + ' ' + tt(props.analysisType) + '...'
}
return 'Should not happen'
})
const completeBtnTo = computed(() => {
if (!analysisId.value) {
return ''
}
return localePath(`/my-data?tab=a&analyses=${analysisId.value}`)
})
const completeBtnLabel = computed(() => {
if (!analysisId.value) {
return ''
}
return tt(props.analysisType) + ' ' + tt('Completed')
})
</script>

<template>
<PVButton
v-if="runBtnVisible"
:disabled="runBtnDisabled"
:loading="runBtnLoading"
:icon="runBtnIcon"
:label="runBtnLabel"
@click="runAnalysis"
/>
<LinkButton
v-else
:to="completeBtnTo"
icon="pi pi-check"
:label="completeBtnLabel"
/>
</template>
Loading

0 comments on commit 6315d2a

Please sign in to comment.