Skip to content

Commit

Permalink
Merge pull request #2029 from zetkin/main
Browse files Browse the repository at this point in the history
Release
  • Loading branch information
richardolsson authored Jun 18, 2024
2 parents 0ae9a9b + f2247a8 commit 6838c26
Show file tree
Hide file tree
Showing 212 changed files with 8,056 additions and 1,542 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Dockerfile
node_modules
.git
.github
.next
env
4 changes: 1 addition & 3 deletions .env.development
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# Client settings
NEXT_PUBLIC_APP_USE_TLS=0
ZETKIN_APP_DOMAIN=http://www.dev.zetkin.org

# Zetkin API settings
ZETKIN_API_DOMAIN=dev.zetkin.org
ZETKIN_API_HOST=api.dev.zetkin.org
NEXT_PUBLIC_ZETKIN_APP_DOMAIN=http://www.dev.zetkin.org
ZETKIN_CLIENT_ID=a0db63a12bae45ff83d12de70c8992c0
ZETKIN_CLIENT_SECRET=MWQyZmE2M2UtMzM3Yi00ODUyLWI2NGMtOWY5YTY5NTY3YjU5
ZETKIN_APP_HOST=localhost:3000
Expand Down
64 changes: 64 additions & 0 deletions .github/workflows/delivery.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Build docker image

on:
push:
branches:
- "release"

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4

# Buildx is necessary for GitHub Actions caching to work
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# The image will be tagged YYMMDD and YYMMDD-abcdef,
# where abcdef is the shortform hash of the last commit
tags: |
type=sha,prefix={{date 'YYMMDD'}}-
type=raw,value={{date 'YYMMDD'}}
type=raw,value=latest
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v5
with:
context: .
file: ./env/frontend/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
1 change: 1 addition & 0 deletions env/frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ COPY yarn.lock /var/app/yarn.lock
RUN yarn install --no-cache --frozen-lockfile

COPY . /var/app
RUN yarn build

CMD ./run.sh
1 change: 0 additions & 1 deletion run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

if [[ "$NODE_ENV" == "production" ]];
then
yarn build
yarn start
else
yarn dev
Expand Down
10 changes: 9 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,15 @@ export default async function RootLayout({
<html lang="en">
<body>
<AppRouterCacheProvider>
<ClientContext lang={lang} messages={messages} user={user}>
<ClientContext
envVars={{
MUIX_LICENSE_KEY: process.env.MUIX_LICENSE_KEY || null,
ZETKIN_APP_DOMAIN: process.env.ZETKIN_APP_DOMAIN || null,
}}
lang={lang}
messages={messages}
user={user}
>
{children}
</ClientContext>
</AppRouterCacheProvider>
Expand Down
53 changes: 36 additions & 17 deletions src/core/caching/cacheUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,14 @@ export function loadListIfNecessary<
actionOnError?: (err: unknown) => PayloadAction<unknown>;
actionOnLoad: () => PayloadAction<OnLoadPayload>;
actionOnSuccess: (items: DataType[]) => PayloadAction<OnSuccessPayload>;
isNecessary?: () => boolean;
loader: () => Promise<DataType[]>;
}
): IFuture<DataType[]> {
if (!remoteList || shouldLoad(remoteList)) {
dispatch(hooks.actionOnLoad());
const promise = hooks
.loader()
.then((val) => {
dispatch(hooks.actionOnSuccess(val));
return val;
})
.catch((err: unknown) => {
if (hooks.actionOnError) {
dispatch(hooks.actionOnError(err));
return null;
} else {
throw err;
}
});
const loadIsNecessary = hooks.isNecessary?.() ?? shouldLoad(remoteList);

return new PromiseFuture(promise);
if (!remoteList || loadIsNecessary) {
return loadList(dispatch, hooks);
}

return new RemoteListFuture({
Expand All @@ -49,6 +36,38 @@ export function loadListIfNecessary<
});
}

export function loadList<
DataType,
OnLoadPayload = void,
OnSuccessPayload = DataType[]
>(
dispatch: AppDispatch,
hooks: {
actionOnError?: (err: unknown) => PayloadAction<unknown>;
actionOnLoad: () => PayloadAction<OnLoadPayload>;
actionOnSuccess: (items: DataType[]) => PayloadAction<OnSuccessPayload>;
loader: () => Promise<DataType[]>;
}
): IFuture<DataType[]> {
dispatch(hooks.actionOnLoad());
const promise = hooks
.loader()
.then((val) => {
dispatch(hooks.actionOnSuccess(val));
return val;
})
.catch((err: unknown) => {
if (hooks.actionOnError) {
dispatch(hooks.actionOnError(err));
return null;
} else {
throw err;
}
});

return new PromiseFuture(promise);
}

export function loadItemIfNecessary<
DataType,
OnLoadPayload = void,
Expand Down
183 changes: 183 additions & 0 deletions src/core/caching/shouldLoad.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import shouldLoad from './shouldLoad';
import { remoteItem, remoteList } from 'utils/storeUtils';

describe('shouldLoad()', () => {
const dummyItemData = {
id: 1,
title: 'Dummy',
};

describe('with lists', () => {
it('returns true when list is undefined', () => {
const result = shouldLoad(undefined);
expect(result).toBeTruthy();
});

it('returns true when list has not loaded', () => {
const list = remoteList([dummyItemData]);
list.loaded = null;

const result = shouldLoad(list);
expect(result).toBeTruthy();
});

it('returns true when list has loaded but is stale', () => {
const list = remoteList([dummyItemData]);
list.loaded = new Date().toISOString();
list.isStale = true;

const result = shouldLoad(list);
expect(result).toBeTruthy();
});

it('returns true when list has loaded but too long ago', () => {
const loaded = new Date();
loaded.setMinutes(loaded.getMinutes() - 6);

const list = remoteList([dummyItemData]);
list.loaded = loaded.toISOString();

const result = shouldLoad(list);
expect(result).toBeTruthy();
});

it('returns false when list has loaded recently', () => {
const list = remoteList([dummyItemData]);
list.loaded = new Date().toISOString();

const result = shouldLoad(list);
expect(result).toBeFalsy();
});

it('returns false when list is already loading', () => {
const list = remoteList([dummyItemData]);
list.isLoading = true;

const result = shouldLoad(list);
expect(result).toBeFalsy();
});
});

describe('with items', () => {
it('returns true when item is undefined', () => {
const result = shouldLoad(undefined);
expect(result).toBeTruthy();
});

it('returns true when item has not loaded', () => {
const item = remoteItem(dummyItemData.id);
item.loaded = null;

const result = shouldLoad(item);
expect(result).toBeTruthy();
});

it('returns true when item has loaded but is stale', () => {
const item = remoteItem(dummyItemData.id);
item.loaded = new Date().toISOString();
item.isStale = true;

const result = shouldLoad(item);
expect(result).toBeTruthy();
});

it('returns true when item has loaded but too long ago', () => {
const loaded = new Date();
loaded.setMinutes(loaded.getMinutes() - 6);

const item = remoteItem(dummyItemData.id);
item.loaded = loaded.toISOString();
item.isStale = true;

const result = shouldLoad(item);
expect(result).toBeTruthy();
});

it('returns false when item has loaded recently', () => {
const item = remoteItem(dummyItemData.id);
item.loaded = new Date().toISOString();

const result = shouldLoad(item);
expect(result).toBeFalsy();
});

it('returns false when item is already loading', () => {
const item = remoteItem(dummyItemData.id);
item.isLoading = true;

const result = shouldLoad(item);
expect(result).toBeFalsy();
});

it('returns false when item is deleted', () => {
const item = remoteItem(dummyItemData.id);
item.deleted = true;

const result = shouldLoad(item);
expect(result).toBeFalsy();
});
});

describe('with map of lists', () => {
it('returns true when any needs loading', () => {
const map = {
'1': remoteList([dummyItemData]),
'2': remoteList([dummyItemData]),
};

// List for 1 needs loading
map[1].isStale = true;

const result = shouldLoad(map, [1, 2]);
expect(result).toBeTruthy();
});

it('returns false when none need loading', () => {
const map = {
'1': remoteList([dummyItemData]),
'2': remoteList([dummyItemData]),
};

// Both lists have loaded
map[1].loaded = new Date().toISOString();
map[2].loaded = new Date().toISOString();

const result = shouldLoad(map, [1, 2]);
expect(result).toBeFalsy();
});

it('returns true when ID is not in map', () => {
const result = shouldLoad({}, [1, 2]);
expect(result).toBeTruthy();
});
});

describe('with map of items', () => {
it('returns true when any needs loading', () => {
const map = {
'1': remoteItem(dummyItemData.id),
'2': remoteItem(dummyItemData.id),
};

// List for 1 needs loading
map[1].isStale = true;

const result = shouldLoad(map, [1, 2]);
expect(result).toBeTruthy();
});

it('returns false when none need loading', () => {
const map = {
'1': remoteItem(dummyItemData.id),
'2': remoteItem(dummyItemData.id),
};

// Both lists have loaded
map[1].loaded = new Date().toISOString();
map[2].loaded = new Date().toISOString();

const result = shouldLoad(map, [1, 2]);
expect(result).toBeFalsy();
});
});
});
Loading

0 comments on commit 6838c26

Please sign in to comment.