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

Report Buttons: View, Download + Run #149

Merged
merged 3 commits into from
Jan 20, 2024
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
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
94 changes: 94 additions & 0 deletions frontend/components/analysis/AccessButtons.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<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
const response: AccessBlobContentResp = await pactaClient.accessBlobContent({
items: props.analysis.artifacts.map((asset): AccessBlobContentReqItem => ({
blobId: asset.blob.id,
})),
})
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)
}),
)
const content = await zip.generateAsync({ type: 'blob' })
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