Hide not owned repos from sidebar and repo list (#1453)

Co-authored-by: Lukas <lukas@slucky.de>
This commit is contained in:
Anbraten 2023-01-31 09:37:11 +01:00 committed by GitHub
parent 420ac54e62
commit 4c97a0104e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 188 additions and 219 deletions

View file

@ -49,6 +49,26 @@ module.exports = {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'error',
// SOURCE: https://github.com/iamturns/eslint-config-airbnb-typescript/blob/4aec5702be5b4e74e0e2f40bc78b4bc961681de1/lib/shared.js#L41
'@typescript-eslint/naming-convention': [
'error',
// Allow camelCase variables (23.2), PascalCase variables (23.8), and UPPER_CASE variables (23.10)
{
selector: 'variable',
format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
leadingUnderscore: 'allow',
},
// Allow camelCase functions (23.2), and PascalCase functions (23.8)
{
selector: 'function',
format: ['camelCase', 'PascalCase'],
},
// Airbnb recommends PascalCase for classes (23.3), and although Airbnb does not make TypeScript recommendations, we are assuming this rule would similarly apply to anything "type like", including interfaces, type aliases, and enums
{
selector: 'typeLike',
format: ['PascalCase'],
},
],
'import/no-unresolved': 'off', // disable as this is handled by tsc itself
'import/first': 'error',

42
web/components.d.ts vendored
View file

@ -22,51 +22,9 @@ declare module '@vue/runtime-core' {
FluidContainer: typeof import('./src/components/layout/FluidContainer.vue')['default']
GeneralTab: typeof import('./src/components/repo/settings/GeneralTab.vue')['default']
Header: typeof import('./src/components/layout/scaffold/Header.vue')['default']
IBiCheckCircleFill: typeof import('~icons/bi/check-circle-fill')['default']
IBiCircle: typeof import('~icons/bi/circle')['default']
IBiPlayCircleFill: typeof import('~icons/bi/play-circle-fill')['default']
IBiSlashCircleFill: typeof import('~icons/bi/slash-circle-fill')['default']
IBiStopCircleFill: typeof import('~icons/bi/stop-circle-fill')['default']
IBiXCircleFill: typeof import('~icons/bi/x-circle-fill')['default']
IBxBxPowerOff: typeof import('~icons/bx/bx-power-off')['default']
ICarbonCloseOutline: typeof import('~icons/carbon/close-outline')['default']
ICarbonInProgress: typeof import('~icons/carbon/in-progress')['default']
IClarityDeployLine: typeof import('~icons/clarity/deploy-line')['default']
IClaritySettingsSolid: typeof import('~icons/clarity/settings-solid')['default']
Icon: typeof import('./src/components/atomic/Icon.vue')['default']
IconButton: typeof import('./src/components/atomic/IconButton.vue')['default']
IGgTrash: typeof import('~icons/gg/trash')['default']
IIcBaselineDarkMode: typeof import('~icons/ic/baseline-dark-mode')['default']
IIcBaselineDownloadForOffline: typeof import('~icons/ic/baseline-download-for-offline')['default']
IIcBaselineEdit: typeof import('~icons/ic/baseline-edit')['default']
IIcBaselineFileDownload: typeof import('~icons/ic/baseline-file-download')['default']
IIcBaselineFileDownloadOff: typeof import('~icons/ic/baseline-file-download-off')['default']
IIcBaselineHealing: typeof import('~icons/ic/baseline-healing')['default']
IIcBaselinePlayArrow: typeof import('~icons/ic/baseline-play-arrow')['default']
IIconoirArrowLeft: typeof import('~icons/iconoir/arrow-left')['default']
IIconParkOutlineAlarmClock: typeof import('~icons/icon-park-outline/alarm-clock')['default']
IIcRoundLightMode: typeof import('~icons/ic/round-light-mode')['default']
IIcSharpTimelapse: typeof import('~icons/ic/sharp-timelapse')['default']
IIcTwotoneAdd: typeof import('~icons/ic/twotone-add')['default']
ILaTimes: typeof import('~icons/la/times')['default']
IMdiBitbucket: typeof import('~icons/mdi/bitbucket')['default']
IMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
IMdiClockTimeEightOutline: typeof import('~icons/mdi/clock-time-eight-outline')['default']
IMdiFormatListBulleted: typeof import('~icons/mdi/format-list-bulleted')['default']
IMdiGestureTap: typeof import('~icons/mdi/gesture-tap')['default']
IMdiGithub: typeof import('~icons/mdi/github')['default']
IMdiLoading: typeof import('~icons/mdi/loading')['default']
IMdiSync: typeof import('~icons/mdi/sync')['default']
IMdiSourceBranch: typeof import('~icons/mdi/source-branch')['default']
IMdisourceCommit: typeof import('~icons/mdi/source-commit')['default']
IMdiSourcePull: typeof import('~icons/mdi/source-pull')['default']
IMdiTagOutline: typeof import('~icons/mdi/tag-outline')['default']
InputField: typeof import('./src/components/form/InputField.vue')['default']
IPhGitlabLogoSimpleFill: typeof import('~icons/ph/gitlab-logo-simple-fill')['default']
ISimpleIconsGitea: typeof import('~icons/simple-icons/gitea')['default']
ITeenyiconsGitSolid: typeof import('~icons/teenyicons/git-solid')['default']
ITeenyiconsRefreshOutline: typeof import('~icons/teenyicons/refresh-outline')['default']
IVaadinQuestionCircleO: typeof import('~icons/vaadin/question-circle-o')['default']
ListItem: typeof import('./src/components/atomic/ListItem.vue')['default']
ManualPipelinePopup: typeof import('./src/components/layout/popups/ManualPipelinePopup.vue')['default']
Navbar: typeof import('./src/components/layout/header/Navbar.vue')['default']

View file

@ -14,26 +14,18 @@
</IconButton>
</template>
<script lang="ts">
import { defineComponent, onMounted } from 'vue';
<script lang="ts" setup>
import { onMounted, toRef } from 'vue';
import IconButton from '~/components/atomic/IconButton.vue';
import usePipelineFeed from '~/compositions/usePipelineFeed';
export default defineComponent({
name: 'ActivePipelines',
const pipelineFeed = usePipelineFeed();
const activePipelines = toRef(pipelineFeed, 'activePipelines');
const { toggle } = pipelineFeed;
components: { IconButton },
setup() {
const pipelineFeed = usePipelineFeed();
onMounted(() => {
pipelineFeed.load();
});
return pipelineFeed;
},
onMounted(async () => {
await pipelineFeed.load();
});
</script>

View file

@ -100,7 +100,7 @@ import { useAsyncAction } from '~/compositions/useAsyncAction';
import useAuthentication from '~/compositions/useAuthentication';
import useNotifications from '~/compositions/useNotifications';
import { Repo, RepoSettings, RepoVisibility, WebhookEvents } from '~/lib/api/types';
import RepoStore from '~/store/repos';
import { useRepoStore } from '~/store/repos';
export default defineComponent({
name: 'GeneralTab',
@ -111,7 +111,7 @@ export default defineComponent({
const apiClient = useApiClient();
const notifications = useNotifications();
const { user } = useAuthentication();
const repoStore = RepoStore();
const repoStore = useRepoStore();
const i18n = useI18n();
const repo = inject<Ref<Repo>>('repo');

View file

@ -1,6 +1,5 @@
import PipelineStore from '~/store/pipelines';
import RepoStore from '~/store/repos';
import { repoSlug } from '~/utils/helpers';
import { usePipelineStore } from '~/store/pipelines';
import { useRepoStore } from '~/store/repos';
import useApiClient from './useApiClient';
@ -11,8 +10,8 @@ export default () => {
if (initialized) {
return;
}
const repoStore = RepoStore();
const pipelineStore = PipelineStore();
const repoStore = useRepoStore();
const pipelineStore = usePipelineStore();
initialized = true;
@ -30,7 +29,6 @@ export default () => {
}
const { pipeline } = data;
pipelineStore.setPipeline(repo.owner, repo.name, pipeline);
pipelineStore.setPipelineFeedItem({ ...pipeline, name: repo.name, owner: repo.owner, full_name: repoSlug(repo) });
// contains step update
if (!data.step) {

View file

@ -1,14 +1,14 @@
import { computed, toRef } from 'vue';
import useUserConfig from '~/compositions/useUserConfig';
import PipelineStore from '~/store/pipelines';
import { usePipelineStore } from '~/store/pipelines';
import useAuthentication from './useAuthentication';
const { userConfig, setUserConfig } = useUserConfig();
export default () => {
const pipelineStore = PipelineStore();
const pipelineStore = usePipelineStore();
const { isAuthenticated } = useAuthentication();
const isOpen = computed(() => userConfig.value.isPipelineFeedOpen && !!isAuthenticated);
@ -17,7 +17,7 @@ export default () => {
setUserConfig('isPipelineFeedOpen', !userConfig.value.isPipelineFeedOpen);
}
const sortedPipelines = toRef(pipelineStore, 'sortedPipelineFeed');
const sortedPipelines = toRef(pipelineStore, 'pipelineFeed');
const activePipelines = toRef(pipelineStore, 'activePipelines');
return {

View file

@ -108,7 +108,6 @@ export default class ApiClient {
const query = encodeQueryString({
access_token: this.token || undefined,
});
// eslint-disable-next-line @typescript-eslint/naming-convention
let _path = this.server ? this.server + path : path;
_path = this.token ? `${path}?${query}` : path;

View file

@ -1,99 +1,105 @@
import { defineStore } from 'pinia';
import { computed, Ref, ref, toRef } from 'vue';
import { computed, reactive, Ref, ref } from 'vue';
import useApiClient from '~/compositions/useApiClient';
import { Pipeline, PipelineFeed, PipelineStep } from '~/lib/api/types';
import { useRepoStore } from '~/store/repos';
import { comparePipelines, isPipelineActive, repoSlug } from '~/utils/helpers';
const apiClient = useApiClient();
export const usePipelineStore = defineStore('pipelines', () => {
const apiClient = useApiClient();
const repoStore = useRepoStore();
export default defineStore({
id: 'pipelines',
const pipelines: Map<string, Map<number, Pipeline>> = reactive(new Map());
state: () => ({
pipelines: {} as Record<string, Record<number, Pipeline>>,
pipelineFeed: [] as PipelineFeed[],
}),
function setPipeline(owner: string, repo: string, pipeline: Pipeline) {
const _repoSlug = repoSlug(owner, repo);
const repoPipelines = pipelines.get(_repoSlug) || new Map();
repoPipelines.set(pipeline.number, pipeline);
pipelines.set(_repoSlug, repoPipelines);
}
getters: {
sortedPipelineFeed(state) {
return state.pipelineFeed.sort(comparePipelines);
},
activePipelines(state) {
return state.pipelineFeed.filter(isPipelineActive);
},
},
function getRepoPipelines(owner: Ref<string>, repo: Ref<string>) {
return computed(() => {
const slug = repoSlug(owner.value, repo.value);
return Array.from(pipelines.get(slug)?.values() || []).sort(comparePipelines);
});
}
actions: {
// setters
setPipeline(owner: string, repo: string, pipeline: Pipeline) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const _repoSlug = repoSlug(owner, repo);
if (!this.pipelines[_repoSlug]) {
this.pipelines[_repoSlug] = {};
}
function getPipeline(owner: Ref<string>, repo: Ref<string>, _pipelineNumber: Ref<string>) {
return computed(() => {
const slug = repoSlug(owner.value, repo.value);
const pipelineNumber = parseInt(_pipelineNumber.value, 10);
return pipelines.get(slug)?.get(pipelineNumber);
});
}
const repoPipelines = this.pipelines[_repoSlug];
function setStep(owner: string, repo: string, pipelineNumber: number, step: PipelineStep) {
const pipeline = getPipeline(ref(owner), ref(repo), ref(pipelineNumber.toString())).value;
if (!pipeline) {
throw new Error("Can't find pipeline");
}
// merge with available data
repoPipelines[pipeline.number] = { ...(repoPipelines[pipeline.number] || {}), ...pipeline };
if (!pipeline.steps) {
pipeline.steps = [];
}
this.pipelines = {
...this.pipelines,
[_repoSlug]: repoPipelines,
};
},
setStep(owner: string, repo: string, pipelineNumber: number, step: PipelineStep) {
const pipeline = this.getPipeline(ref(owner), ref(repo), ref(pipelineNumber.toString())).value;
if (!pipeline) {
throw new Error("Can't find pipeline");
}
pipeline.steps = [...pipeline.steps.filter((p) => p.pid !== step.pid), step];
setPipeline(owner, repo, pipeline);
}
if (!pipeline.steps) {
pipeline.steps = [];
}
async function loadRepoPipelines(owner: string, repo: string) {
const _pipelines = await apiClient.getPipelineList(owner, repo);
_pipelines.forEach((pipeline) => {
setPipeline(owner, repo, pipeline);
});
}
pipeline.steps = [...pipeline.steps.filter((p) => p.pid !== step.pid), step];
this.setPipeline(owner, repo, pipeline);
},
setPipelineFeedItem(pipeline: PipelineFeed) {
const pipelineFeed = this.pipelineFeed.filter((b) => b.id !== pipeline.id);
this.pipelineFeed = [...pipelineFeed, pipeline];
},
async function loadPipeline(owner: string, repo: string, pipelinesNumber: number) {
const pipeline = await apiClient.getPipeline(owner, repo, pipelinesNumber);
setPipeline(owner, repo, pipeline);
}
// getters
getPipelines(owner: Ref<string>, repo: Ref<string>) {
return computed(() => {
const slug = repoSlug(owner.value, repo.value);
return toRef(this.pipelines, slug).value;
});
},
getSortedPipelines(owner: Ref<string>, repo: Ref<string>) {
return computed(() => Object.values(this.getPipelines(owner, repo).value || []).sort(comparePipelines));
},
getActivePipelines(owner: Ref<string>, repo: Ref<string>) {
const pipelines = this.getPipelines(owner, repo);
return computed(() => Object.values(pipelines.value).filter(isPipelineActive));
},
getPipeline(owner: Ref<string>, repo: Ref<string>, pipelineNumber: Ref<string>) {
const pipelines = this.getPipelines(owner, repo);
return computed(() => (pipelines.value || {})[parseInt(pipelineNumber.value, 10)]);
},
const pipelineFeed = computed(() =>
Array.from(pipelines.entries())
.reduce<PipelineFeed[]>((acc, [_repoSlug, repoPipelines]) => {
const repoPipelinesArray = Array.from(repoPipelines.entries()).map(
([_pipelineNumber, pipeline]) =>
<PipelineFeed>{
...pipeline,
full_name: _repoSlug,
owner: _repoSlug.split('/')[0],
name: _repoSlug.split('/')[1],
number: _pipelineNumber,
},
);
return [...acc, ...repoPipelinesArray];
}, [])
.sort(comparePipelines)
.filter((pipeline) => repoStore.ownedRepoSlugs.includes(pipeline.full_name)),
);
// loading
async loadPipelines(owner: string, repo: string) {
const pipelines = await apiClient.getPipelineList(owner, repo);
pipelines.forEach((pipeline) => {
this.setPipeline(owner, repo, pipeline);
});
},
async loadPipeline(owner: string, repo: string, pipelinesNumber: number) {
const pipelines = await apiClient.getPipeline(owner, repo, pipelinesNumber);
this.setPipeline(owner, repo, pipelines);
},
async loadPipelineFeed() {
const pipelines = await apiClient.getPipelineFeed();
this.pipelineFeed = pipelines;
},
},
const activePipelines = computed(() => pipelineFeed.value.filter(isPipelineActive));
async function loadPipelineFeed() {
await repoStore.loadRepos();
const _pipelines = await apiClient.getPipelineFeed();
_pipelines.forEach((pipeline) => {
setPipeline(pipeline.owner, pipeline.name, pipeline);
});
}
return {
pipelines,
setPipeline,
setStep,
getRepoPipelines,
getPipeline,
loadRepoPipelines,
loadPipeline,
activePipelines,
pipelineFeed,
loadPipelineFeed,
};
});

View file

@ -1,44 +1,54 @@
import { defineStore } from 'pinia';
import { computed, Ref, toRef } from 'vue';
import { computed, reactive, Ref, ref } from 'vue';
import useApiClient from '~/compositions/useApiClient';
import { Repo } from '~/lib/api/types';
import { repoSlug } from '~/utils/helpers';
const apiClient = useApiClient();
export const useRepoStore = defineStore('repos', () => {
const apiClient = useApiClient();
export default defineStore({
id: 'repos',
const repos: Map<string, Repo> = reactive(new Map());
const ownedRepoSlugs = ref<string[]>([]);
state: () => ({
repos: {} as Record<string, Repo>,
}),
const ownedRepos = computed(() =>
Array.from(repos.entries())
.filter(([slug]) => ownedRepoSlugs.value.includes(slug))
.map(([, repo]) => repo),
);
actions: {
// getter
getRepo(owner: Ref<string>, name: Ref<string>) {
return computed(() => {
const slug = repoSlug(owner.value, name.value);
return toRef(this.repos, slug).value;
});
},
function getRepo(owner: Ref<string>, name: Ref<string>) {
return computed(() => {
const slug = repoSlug(owner.value, name.value);
return repos.get(slug);
});
}
// setter
setRepo(repo: Repo) {
this.repos[repoSlug(repo)] = repo;
},
function setRepo(repo: Repo) {
repos.set(repoSlug(repo), repo);
}
// loading
async loadRepo(owner: string, name: string) {
const repo = await apiClient.getRepo(owner, name);
this.repos[repoSlug(repo)] = repo;
return repo;
},
async loadRepos() {
const repos = await apiClient.getRepoList();
repos.forEach((repo) => {
this.repos[repoSlug(repo.owner, repo.name)] = repo;
});
},
},
async function loadRepo(owner: string, name: string) {
const repo = await apiClient.getRepo(owner, name);
repos.set(repoSlug(repo), repo);
return repo;
}
async function loadRepos() {
const _ownedRepos = await apiClient.getRepoList();
_ownedRepos.forEach((repo) => {
repos.set(repoSlug(repo), repo);
});
ownedRepoSlugs.value = _ownedRepos.map((repo) => repoSlug(repo));
}
return {
repos,
ownedRepos,
ownedRepoSlugs,
getRepo,
setRepo,
loadRepo,
loadRepos,
};
});

View file

@ -20,36 +20,22 @@
</Scaffold>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, ref } from 'vue';
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import Button from '~/components/atomic/Button.vue';
import ListItem from '~/components/atomic/ListItem.vue';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import { useRepoSearch } from '~/compositions/useRepoSearch';
import RepoStore from '~/store/repos';
import { useRepoStore } from '~/store/repos';
export default defineComponent({
name: 'Repos',
const repoStore = useRepoStore();
const repos = computed(() => Object.values(repoStore.ownedRepos));
const search = ref('');
components: {
Button,
ListItem,
Scaffold,
},
const { searchedRepos } = useRepoSearch(repos, search);
setup() {
const repoStore = RepoStore();
const repos = computed(() => Object.values(repoStore.repos));
const search = ref('');
const { searchedRepos } = useRepoSearch(repos, search);
onMounted(async () => {
await repoStore.loadRepos();
});
return { searchedRepos, search };
},
onMounted(async () => {
await repoStore.loadRepos();
});
</script>

View file

@ -37,7 +37,7 @@ import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import useApiClient from '~/compositions/useApiClient';
import { useRepoSearch } from '~/compositions/useRepoSearch';
import { OrgPermissions } from '~/lib/api/types';
import RepoStore from '~/store/repos';
import { useRepoStore } from '~/store/repos';
export default defineComponent({
name: 'ReposOwner',
@ -57,9 +57,9 @@ export default defineComponent({
setup(props) {
const apiClient = useApiClient();
const repoStore = RepoStore();
const repoStore = useRepoStore();
// TODO: filter server side
const repos = computed(() => Object.values(repoStore.repos).filter((v) => v.owner === props.repoOwner));
const repos = computed(() => Array.from(repoStore.repos.values()).filter((repo) => repo.owner === props.repoOwner));
const search = ref('');
const orgPermissions = ref<OrgPermissions>({ member: false, admin: false });

View file

@ -64,8 +64,8 @@ import useAuthentication from '~/compositions/useAuthentication';
import useConfig from '~/compositions/useConfig';
import useNotifications from '~/compositions/useNotifications';
import { RepoPermissions } from '~/lib/api/types';
import PipelineStore from '~/store/pipelines';
import RepoStore from '~/store/repos';
import { usePipelineStore } from '~/store/pipelines';
import { useRepoStore } from '~/store/repos';
const props = defineProps({
repoOwner: {
@ -81,8 +81,8 @@ const props = defineProps({
const repoOwner = toRef(props, 'repoOwner');
const repoName = toRef(props, 'repoName');
const repoStore = RepoStore();
const pipelineStore = PipelineStore();
const repoStore = useRepoStore();
const pipelineStore = usePipelineStore();
const apiClient = useApiClient();
const notifications = useNotifications();
const { isAuthenticated } = useAuthentication();
@ -93,7 +93,7 @@ const i18n = useI18n();
const { forge } = useConfig();
const repo = repoStore.getRepo(repoOwner, repoName);
const repoPermissions = ref<RepoPermissions>();
const pipelines = pipelineStore.getSortedPipelines(repoOwner, repoName);
const pipelines = pipelineStore.getRepoPipelines(repoOwner, repoName);
provide('repo', repo);
provide('repo-permissions', repoPermissions);
provide('pipelines', pipelines);
@ -121,7 +121,7 @@ async function loadRepo() {
});
return;
}
await pipelineStore.loadPipelines(repoOwner.value, repoName.value);
await pipelineStore.loadRepoPipelines(repoOwner.value, repoName.value);
}
onMounted(() => {
@ -132,7 +132,7 @@ watch([repoOwner, repoName], () => {
loadRepo();
});
const badgeUrl = computed(() => `/api/badges/${repo.value.owner}/${repo.value.name}/status.svg`);
const badgeUrl = computed(() => repo.value && `/api/badges/${repo.value.owner}/${repo.value.name}/status.svg`);
const activeTab = computed({
get() {

View file

@ -92,7 +92,7 @@ import useNotifications from '~/compositions/useNotifications';
import usePipeline from '~/compositions/usePipeline';
import { useRouteBackOrDefault } from '~/compositions/useRouteBackOrDefault';
import { Repo, RepoPermissions } from '~/lib/api/types';
import PipelineStore from '~/store/pipelines';
import { usePipelineStore } from '~/store/pipelines';
export default defineComponent({
name: 'PipelineWrapper',
@ -130,7 +130,7 @@ export default defineComponent({
const favicon = useFavicon();
const i18n = useI18n();
const pipelineStore = PipelineStore();
const pipelineStore = usePipelineStore();
const pipelineId = toRef(props, 'pipelineId');
const repoOwner = toRef(props, 'repoOwner');
const repoName = toRef(props, 'repoName');
@ -155,7 +155,7 @@ export default defineComponent({
await pipelineStore.loadPipeline(repo.value.owner, repo.value.name, parseInt(pipelineId.value, 10));
favicon.updateStatus(pipeline.value.status);
favicon.updateStatus(pipeline.value?.status);
}
const { doSubmit: cancelPipeline, isLoading: isCancelingPipeline } = useAsyncAction(async () => {
@ -163,7 +163,7 @@ export default defineComponent({
throw new Error('Unexpected: Repo is undefined');
}
if (!pipeline.value.steps) {
if (!pipeline.value?.steps) {
throw new Error('Unexpected: Pipeline steps not loaded');
}