Skip to content

Commit

Permalink
Merge pull request #837 from posit-dev/sagerb-pinia-deployment-store-…
Browse files Browse the repository at this point in the history
…take2

Access deployments through pinia store
  • Loading branch information
sagerb authored Jan 23, 2024
2 parents ecd74da + db5c76c commit 9dd6c9f
Show file tree
Hide file tree
Showing 13 changed files with 325 additions and 157 deletions.
4 changes: 4 additions & 0 deletions web/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import AppHeader from 'src/components/AppHeader.vue';
import { onBeforeUnmount } from 'vue';
import { useEventStore } from 'src/stores/events';
import { useDeploymentStore } from 'src/stores/deployments';

const eventStore = useEventStore();

Expand All @@ -28,6 +29,9 @@ onBeforeUnmount(() => {
eventStore.closeEventStream();
});

// Let's start population of the deployments as quickly as possible
useDeploymentStore();

</script>

<style lang="scss">
Expand Down
6 changes: 3 additions & 3 deletions web/src/api/resources/Deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { AxiosInstance } from 'axios';

import { Deployment, DeploymentRecordError, PreDeployment } from 'src/api/types/deployments';
import { Deployment, DeploymentError, PreDeployment } from 'src/api/types/deployments';

export class Deployments {
private client: AxiosInstance;
Expand All @@ -15,7 +15,7 @@ export class Deployments {
// 200 - success
// 500 - internal server error
getAll() {
return this.client.get<Array<Deployment | PreDeployment | DeploymentRecordError>>(
return this.client.get<Array<Deployment | PreDeployment | DeploymentError>>(
'/deployments',
);
}
Expand All @@ -26,7 +26,7 @@ export class Deployments {
// 500 - internal server error
get(id: string) {
const encodedId = encodeURIComponent(id);
return this.client.get<Deployment | PreDeployment | DeploymentRecordError>(
return this.client.get<Deployment | PreDeployment | DeploymentError>(
`deployments/${encodedId}`,
);
}
Expand Down
13 changes: 7 additions & 6 deletions web/src/api/types/deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type DeploymentLocation = {
deploymentPath: string;
}

export type DeploymentRecordError = {
export type DeploymentError = {
error: AgentError,
state: DeploymentState.ERROR,
} & DeploymentLocation
Expand All @@ -41,22 +41,23 @@ export type Deployment = {
files: string[],
deployedAt: string,
state: DeploymentState.DEPLOYED,
deploymentError: AgentError | null,
} & DeploymentRecord & Configuration;

export function isDeploymentRecordError(
d: Deployment | PreDeployment | DeploymentRecordError
): d is DeploymentRecordError {
export function isDeploymentError(
d: Deployment | PreDeployment | DeploymentError
): d is DeploymentError {
return d.state === DeploymentState.ERROR;
}

export function isPreDeployment(
d: Deployment | PreDeployment | DeploymentRecordError
d: Deployment | PreDeployment | DeploymentError
): d is PreDeployment {
return d.state === DeploymentState.NEW;
}

export function isDeployment(
d: Deployment | PreDeployment | DeploymentRecordError
d: Deployment | PreDeployment | DeploymentError
): d is Deployment {
return d.state === DeploymentState.DEPLOYED;
}
1 change: 1 addition & 0 deletions web/src/api/types/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
export type AgentError = {
code: string;
msg: string;
operation: string;
data: {
[key: string]: unknown
};
Expand Down
1 change: 1 addition & 0 deletions web/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const routes = [
path: '/deployments/:name',
component: DeploymentPage,
props: (route: RouteLocationNormalizedLoaded) => ({
name: route.params.name,
preferredAccount: route.query.preferredAccount,
}),
},
Expand Down
120 changes: 120 additions & 0 deletions web/src/stores/deployments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright (C) 2023 by Posit Software, PBC.

import { defineStore } from 'pinia';
import { useApi } from 'src/api';
import { computed, ref } from 'vue';
import { router } from 'src/router';
import { getStatusFromError, newFatalErrorRouteLocation } from 'src/utils/errors';
import { sortByDateString } from 'src/utils/date';

import {
Deployment,
DeploymentError,
isDeploymentError,
isPreDeployment,
PreDeployment,
} from 'src/api/types/deployments';

export const useDeploymentStore = defineStore('deployment', () => {
// key is deployment saveName
type DeploymentMap = Record<string, Deployment | PreDeployment | DeploymentError>;

const deploymentMap = ref<DeploymentMap>({});

const api = useApi();

const getDeploymentRef = (deploymentName: string) => {
return computed(() => deploymentMap.value[deploymentName]);
};

const refreshDeployments = async() => {
try {
const newDeploymentMap: DeploymentMap = {};

// API Returns:
// 200 - success
// 500 - internal server error
const response = (await api.deployments.getAll()).data;
response.forEach((deployment) => {
if (isDeploymentError(deployment)) {
newDeploymentMap[deployment.deploymentName] = deployment;
} else {
newDeploymentMap[deployment.saveName] = deployment;
}
});
deploymentMap.value = newDeploymentMap;
} catch (error: unknown) {
router.push(newFatalErrorRouteLocation(error, 'deployments::getDeployments()'));
}
};

const hasDeployments = computed(() => Object.keys(deploymentMap.value).length > 0);

const refreshDeployment = async(deploymentName: string) => {
try {
// API Returns:
// 200 - success
// 404 - not found
// 500 - internal server error
const response = await api.deployments.get(deploymentName);
const deployment = response.data;
if (isDeploymentError(deployment)) {
deploymentMap.value[deployment.deploymentName] = deployment;
} else {
deploymentMap.value[deployment.saveName] = deployment;
}
} catch (error: unknown) {
if (getStatusFromError(error) === 404) {
// deployment no longer exists, remove from the map.
delete deploymentMap.value[deploymentName];
return;
}
// For this page, we send all errors to the fatal error page, including 404
router.push(newFatalErrorRouteLocation(error, `deployments::getDeployment(${name})`));
}
};

const sortedDeployments = computed(() => {
const result: Array<Deployment | PreDeployment | DeploymentError> = [];
const deploymentErrors: Array<DeploymentError> = [];
const preDeployments: Array<PreDeployment> = [];
const deployments: Array<Deployment> = [];
Object.keys(deploymentMap.value).forEach(
deploymentName => {
const deployment = deploymentMap.value[deploymentName];
if (isDeploymentError(deployment)) {
deploymentErrors.push(deployment);
} else if (isPreDeployment(deployment)) {
preDeployments.push(deployment);
} else {
deployments.push(deployment);
}
}
);
deploymentErrors.sort((a, b) => {
return a.deploymentName.localeCompare(b.deploymentName, undefined, { sensitivity: 'base' });
});
preDeployments.sort((a, b) => {
return a.deploymentName.localeCompare(b.deploymentName, undefined, { sensitivity: 'base' });
});
deployments.sort((a, b) => {
return sortByDateString(a.deployedAt, b.deployedAt);
});

return result.concat(deploymentErrors, preDeployments, deployments);
});

const init = () => {
refreshDeployments();
};
init();

return {
refreshDeployments,
refreshDeployment,
sortedDeployments,
hasDeployments,
deploymentMap,
getDeploymentRef,
};
});
9 changes: 9 additions & 0 deletions web/src/stores/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
} from 'src/plugins/eventStream';

import { computed, ref } from 'vue';
import { useDeploymentStore } from './deployments';

export type PublishStepCompletionStatus =
'notStarted' | 'inProgress' | 'success' | 'error';
Expand Down Expand Up @@ -317,6 +318,7 @@ export const useEventStore = defineStore('event', () => {
};

const onPublishSuccess = (msg: PublishSuccess) => {
const deployments = useDeploymentStore();
const localId = getLocalId(msg);

if (currentPublishStatus.value.localId === localId) {
Expand All @@ -328,9 +330,13 @@ export const useEventStore = defineStore('event', () => {
currentPublishStatus.value.contentId = msg.data.contentId;
}
publishInProgess.value = false;
if (currentPublishStatus.value.saveName) {
deployments.refreshDeployment(currentPublishStatus.value.saveName);
}
};

const onPublishFailure = (msg: PublishFailure) => {
const deployments = useDeploymentStore();
const localId = getLocalId(msg);

if (currentPublishStatus.value.localId === localId) {
Expand All @@ -339,6 +345,9 @@ export const useEventStore = defineStore('event', () => {
publishStatus.error = splitMsgIntoKeyValuePairs(msg.data);
}
publishInProgess.value = false;
if (currentPublishStatus.value.saveName) {
deployments.refreshDeployment(currentPublishStatus.value.saveName);
}
};

const onPublishCheckCapabilitiesStart = (msg: PublishCheckCapabilitiesStart) => {
Expand Down
72 changes: 43 additions & 29 deletions web/src/views/add-new-deployment/AddNewDeployment.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,22 @@
import { Account, useApi } from 'src/api';
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { Deployment, isDeploymentRecordError } from 'src/api/types/deployments';
import { isDeploymentError } from 'src/api/types/deployments';

import AccountRadio from 'src/views/add-new-deployment/AccountRadio.vue';
import { newFatalErrorRouteLocation } from 'src/utils/errors';
import PButton from 'src/components/PButton.vue';
import PLink from 'src/components/PLink.vue';
import { useDeploymentStore } from 'src/stores/deployments';

const accounts = ref<Account[]>([]);
const selectedAccountName = ref<string>('');
const deploymentName = ref<string>('');
const deployments = ref<Deployment[]>([]);
const navigationInProgress = ref<boolean>(false);

const api = useApi();
const router = useRouter();
const deployments = useDeploymentStore();

async function getAccounts() {
try {
Expand All @@ -90,27 +91,20 @@ async function getAccounts() {
}
}

async function getDeployments() {
try {
// API Returns:
// 200 - success
// 500 - internal server error
const response = (await api.deployments.getAll()).data;
deployments.value = response.filter<Deployment>((d): d is Deployment => {
return !isDeploymentRecordError(d);
});
} catch (error: unknown) {
router.push(newFatalErrorRouteLocation(error, 'ProjectPage::getDeployments()'));
}
}

const deploymentNameError = computed(() => {
if (!deploymentName.value) {
return 'A unique deployment name must be provided.';
}
if (
deployments.value.find(
(deployment) => deployment.saveName.toLowerCase() === deploymentName.value.toLowerCase()
deployments.sortedDeployments.find(
(deployment) => {
if (isDeploymentError(deployment)) {
return (
deployment.deploymentName.toLocaleLowerCase() === deploymentName.value.toLowerCase()
);
}
return deployment.saveName.toLowerCase() === deploymentName.value.toLowerCase();
}
)
) {
return 'Deployment name already in use. Please supply a unique name.';
Expand All @@ -128,23 +122,36 @@ const disableToDeploymentPage = computed(() => {

const navigateToDeploymentPage = async() => {
navigationInProgress.value = true;
let responseName = deploymentName.value;

// create a pre-deployment object
try {
const response = await api.deployments.createNew(
selectedAccountName.value,
deploymentName.value,
);
router.push({
name: 'deployments',
params: {
name: response.data.deploymentName,
},
query: {
preferredAccount: selectedAccountName.value,
}
});
responseName = response.data.deploymentName;
} catch (error: unknown) {
router.push(newFatalErrorRouteLocation(error, 'navigateToDeploymentPage::createNew()'));
}

// refresh the deployment store to include the new pre-deployment page
try {
await deployments.refreshDeployment(responseName);
} catch (error: unknown) {
router.push(newFatalErrorRouteLocation(error, 'navigateToDeploymentPage::refreshDeployment()'));
}

// navigate to the deployment page
router.push({
name: 'deployments',
params: {
name: responseName,
},
query: {
preferredAccount: selectedAccountName.value,
}
});
};

const generateDefaultName = () => {
Expand All @@ -153,7 +160,15 @@ const generateDefaultName = () => {
do {
id += 1;
const trialName = `Untitled-${id}`;
if (!deployments.value.find((deployment) => deployment.saveName === trialName)) {

if (!deployments.sortedDeployments.find(
(deployment) => {
if (isDeploymentError(deployment)) {
return deployment.deploymentName.toLocaleLowerCase() === trialName.toLowerCase();
}
return deployment.saveName.toLowerCase() === trialName.toLowerCase();
}
)) {
defaultName = trialName;
}
} while (!defaultName);
Expand All @@ -162,7 +177,6 @@ const generateDefaultName = () => {

const init = async() => {
getAccounts();
await getDeployments();
deploymentName.value = generateDefaultName();
};
init();
Expand Down
Loading

0 comments on commit 9dd6c9f

Please sign in to comment.